diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 951fd0e..367fadb 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -20,18 +20,10 @@ jobs: - name: Set up QEMU uses: docker/setup-qemu-action@v3 - - name: build and push + - name: build and push all images if: ${{ env.DOCKER_USERNAME != '' && env.DOCKER_PASSWORD != '' }} env: DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} run: | - ./build.py --push-tags --multiarch - - - name: build and push rootless images - if: ${{ env.DOCKER_USERNAME != '' && env.DOCKER_PASSWORD != '' }} - env: - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - run: | - ./build-rootless.py --push-tags --multiarch \ No newline at end of file + ./build-unified.py --push-tags --multiarch --both \ No newline at end of file diff --git a/BUILD_MIGRATION.md b/BUILD_MIGRATION.md new file mode 100644 index 0000000..00c6b8f --- /dev/null +++ b/BUILD_MIGRATION.md @@ -0,0 +1,61 @@ +# Build System Migration Guide + +## Overview + +The build system has been unified into a single script (`build-unified.py`) that can build both regular and rootless Docker images. This replaces the separate `build.py` and `build-rootless.py` scripts. + +## New Unified Build Script + +### Usage + +```bash +# Build only regular images (default behavior) +./build-unified.py + +# Build only rootless images +./build-unified.py --rootless + +# Build both regular and rootless images +./build-unified.py --both + +# Build with multi-architecture support +./build-unified.py --multiarch --both + +# Build and push to Docker Hub +./build-unified.py --push-tags --multiarch --both + +# Build only stable/latest versions (useful for rootless) +./build-unified.py --rootless --only-stable-latest --push-tags --multiarch +``` + +### Options + +- `--push-tags`: Push images to Docker Hub (requires DOCKER_USERNAME and DOCKER_PASSWORD env vars) +- `--multiarch`: Build multi-architecture images (linux/amd64 and linux/arm64) +- `--rootless`: Build only rootless images +- `--both`: Build both regular and rootless images +- `--only-stable-latest`: Build only versions tagged as 'stable' or 'latest' + +## Migration Path + +### For CI/CD + +The GitHub Actions workflow has been updated to use: +```yaml +./build-unified.py --push-tags --multiarch --both +``` + +### For Local Development + +Replace old commands with new ones: + +| Old Command | New Command | +|-------------|-------------| +| `./build.py` | `./build-unified.py` | +| `./build.py --multiarch` | `./build-unified.py --multiarch` | +| `./build-rootless.py` | `./build-unified.py --rootless` | +| `./build.py && ./build-rootless.py` | `./build-unified.py --both` | + +## Backwards Compatibility + +The original `build.py` and `build-rootless.py` scripts are now wrappers that call the unified script with appropriate arguments to maintain backwards compatibility. \ No newline at end of file diff --git a/__pycache__/build-rootless.cpython-312.pyc b/__pycache__/build-rootless.cpython-312.pyc new file mode 100644 index 0000000..3783dc4 Binary files /dev/null and b/__pycache__/build-rootless.cpython-312.pyc differ diff --git a/__pycache__/build-unified.cpython-312.pyc b/__pycache__/build-unified.cpython-312.pyc new file mode 100644 index 0000000..7cb1dc6 Binary files /dev/null and b/__pycache__/build-unified.cpython-312.pyc differ diff --git a/build-rootless.py b/build-rootless.py index da98761..16fbad2 100755 --- a/build-rootless.py +++ b/build-rootless.py @@ -1,128 +1,15 @@ #!/usr/bin/env python3 +""" +Legacy wrapper script for backwards compatibility. +This script now calls build-unified.py to build rootless images. +""" -import os -import json import subprocess -import shutil import sys -import tempfile +# Convert arguments and pass to unified script with --rootless flag +args = ["./build-unified.py", "--rootless", "--only-stable-latest"] +args.extend(sys.argv[1:]) -PLATFORMS = [ - "linux/arm64", - "linux/amd64", -] - - -def create_builder(build_dir, builder_name, platform): - check_exists_command = ["docker", "buildx", "inspect", builder_name] - if subprocess.run(check_exists_command, stderr=subprocess.DEVNULL).returncode != 0: - create_command = ["docker", "buildx", "create", "--platform", platform, "--name", builder_name] - try: - subprocess.run(create_command, cwd=build_dir, check=True) - except subprocess.CalledProcessError: - print("Creating builder failed") - exit(1) - - -def build_and_push_multiarch(build_dir, build_args, push): - builder_name = "factoriotools-rootless-multiarch" - platform=",".join(PLATFORMS) - create_builder(build_dir, builder_name, platform) - build_command = ["docker", "buildx", "build", "--platform", platform, "--builder", builder_name] + build_args - if push: - build_command.append("--push") - try: - subprocess.run(build_command, cwd=build_dir, check=True) - except subprocess.CalledProcessError: - print("Build and push of rootless image failed") - exit(1) - - -def build_singlearch(build_dir, build_args): - build_command = ["docker", "build"] + build_args - try: - subprocess.run(build_command, cwd=build_dir, check=True) - except subprocess.CalledProcessError: - print("Build of rootless image failed") - exit(1) - - -def push_singlearch(tags): - for tag in tags: - try: - subprocess.run(["docker", "push", f"factoriotools/factorio:{tag}"], - check=True) - except subprocess.CalledProcessError: - print("Docker push failed") - exit(1) - - -def build_and_push(sha256, version, tags, push, multiarch): - build_dir = tempfile.mktemp() - shutil.copytree("docker", build_dir) - # Use the rootless Dockerfile - build_args = ["-f", "Dockerfile.rootless", "--build-arg", f"VERSION={version}", "--build-arg", f"SHA256={sha256}", "."] - for tag in tags: - build_args.extend(["-t", f"factoriotools/factorio:{tag}"]) - if multiarch: - build_and_push_multiarch(build_dir, build_args, push) - else: - build_singlearch(build_dir, build_args) - if push: - push_singlearch(tags) - - -def login(): - try: - username = os.environ["DOCKER_USERNAME"] - password = os.environ["DOCKER_PASSWORD"] - subprocess.run(["docker", "login", "-u", username, "-p", password], check=True) - except KeyError: - print("Username and password need to be given") - exit(1) - except subprocess.CalledProcessError: - print("Docker login failed") - exit(1) - - -def generate_rootless_tags(original_tags): - """Generate rootless-specific tags from original tags""" - rootless_tags = [] - for tag in original_tags: - # Add -rootless suffix to each tag - rootless_tags.append(f"{tag}-rootless") - return rootless_tags - - -def main(push_tags=False, multiarch=False): - with open(os.path.join(os.path.dirname(__file__), "buildinfo.json")) as file_handle: - builddata = json.load(file_handle) - - if push_tags: - login() - - # Build only the latest stable and experimental versions for rootless - versions_to_build = [] - - # Find latest stable and experimental versions - for version, buildinfo in builddata.items(): - if "stable" in buildinfo["tags"] or "latest" in buildinfo["tags"]: - versions_to_build.append((version, buildinfo)) - - for version, buildinfo in versions_to_build: - sha256 = buildinfo["sha256"] - original_tags = buildinfo["tags"] - rootless_tags = generate_rootless_tags(original_tags) - build_and_push(sha256, version, rootless_tags, push_tags, multiarch) - - -if __name__ == '__main__': - push_tags = False - multiarch = False - for arg in sys.argv[1:]: - if arg == "--push-tags": - push_tags = True - elif arg == "--multiarch": - multiarch = True - main(push_tags, multiarch) \ No newline at end of file +# Execute the unified build script +sys.exit(subprocess.call(args)) \ No newline at end of file diff --git a/build-unified.py b/build-unified.py new file mode 100755 index 0000000..0ba872a --- /dev/null +++ b/build-unified.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 + +import os +import json +import subprocess +import shutil +import sys +import tempfile +import argparse + + +PLATFORMS = [ + "linux/arm64", + "linux/amd64", +] + + +def create_builder(build_dir, builder_name, platform): + check_exists_command = ["docker", "buildx", "inspect", builder_name] + if subprocess.run(check_exists_command, stderr=subprocess.DEVNULL).returncode != 0: + create_command = ["docker", "buildx", "create", "--platform", platform, "--name", builder_name] + try: + subprocess.run(create_command, cwd=build_dir, check=True) + except subprocess.CalledProcessError: + print("Creating builder failed") + exit(1) + + +def build_and_push_multiarch(build_dir, build_args, push, builder_suffix=""): + builder_name = f"factoriotools{builder_suffix}-multiarch" + platform = ",".join(PLATFORMS) + create_builder(build_dir, builder_name, platform) + build_command = ["docker", "buildx", "build", "--platform", platform, "--builder", builder_name] + build_args + if push: + build_command.append("--push") + try: + subprocess.run(build_command, cwd=build_dir, check=True) + except subprocess.CalledProcessError: + print(f"Build and push of {builder_suffix or 'regular'} image failed") + exit(1) + + +def build_singlearch(build_dir, build_args, image_type="regular"): + build_command = ["docker", "build"] + build_args + try: + subprocess.run(build_command, cwd=build_dir, check=True) + except subprocess.CalledProcessError: + print(f"Build of {image_type} image failed") + exit(1) + + +def push_singlearch(tags): + for tag in tags: + try: + subprocess.run(["docker", "push", f"factoriotools/factorio:{tag}"], + check=True) + except subprocess.CalledProcessError: + print("Docker push failed") + exit(1) + + +def build_and_push(sha256, version, tags, push, multiarch, dockerfile="Dockerfile", builder_suffix=""): + build_dir = tempfile.mktemp() + shutil.copytree("docker", build_dir) + build_args = ["-f", dockerfile, "--build-arg", f"VERSION={version}", "--build-arg", f"SHA256={sha256}", "."] + for tag in tags: + build_args.extend(["-t", f"factoriotools/factorio:{tag}"]) + + image_type = "rootless" if "rootless" in dockerfile.lower() else "regular" + + if multiarch: + build_and_push_multiarch(build_dir, build_args, push, builder_suffix) + else: + build_singlearch(build_dir, build_args, image_type) + if push: + push_singlearch(tags) + + +def login(): + try: + username = os.environ["DOCKER_USERNAME"] + password = os.environ["DOCKER_PASSWORD"] + subprocess.run(["docker", "login", "-u", username, "-p", password], check=True) + except KeyError: + print("Username and password need to be given") + exit(1) + except subprocess.CalledProcessError: + print("Docker login failed") + exit(1) + + +def generate_rootless_tags(original_tags): + """Generate rootless-specific tags from original tags""" + return [f"{tag}-rootless" for tag in original_tags] + + +def main(): + parser = argparse.ArgumentParser(description='Build Factorio Docker images') + parser.add_argument('--push-tags', action='store_true', help='Push images to Docker Hub') + parser.add_argument('--multiarch', action='store_true', help='Build multi-architecture images') + parser.add_argument('--rootless', action='store_true', help='Build only rootless images') + parser.add_argument('--both', action='store_true', help='Build both regular and rootless images') + parser.add_argument('--only-stable-latest', action='store_true', + help='Build only stable and latest versions (for rootless by default)') + + args = parser.parse_args() + + # Default behavior: build regular images unless specified otherwise + build_regular = not args.rootless or args.both + build_rootless = args.rootless or args.both + + with open(os.path.join(os.path.dirname(__file__), "buildinfo.json")) as file_handle: + builddata = json.load(file_handle) + + if args.push_tags: + login() + + # Filter versions if needed + versions_to_build = [] + for version, buildinfo in sorted(builddata.items(), key=lambda item: item[0], reverse=True): + if args.only_stable_latest or (build_rootless and not build_regular): + # For rootless-only builds, default to stable/latest only + if "stable" in buildinfo["tags"] or "latest" in buildinfo["tags"]: + versions_to_build.append((version, buildinfo)) + else: + versions_to_build.append((version, buildinfo)) + + # Build regular images + if build_regular: + print("Building regular images...") + for version, buildinfo in versions_to_build: + sha256 = buildinfo["sha256"] + tags = buildinfo["tags"] + build_and_push(sha256, version, tags, args.push_tags, args.multiarch) + + # Build rootless images + if build_rootless: + print("Building rootless images...") + # For rootless, only build stable and latest unless building both + rootless_versions = [] + if not build_regular or args.only_stable_latest: + for version, buildinfo in builddata.items(): + if "stable" in buildinfo["tags"] or "latest" in buildinfo["tags"]: + rootless_versions.append((version, buildinfo)) + else: + rootless_versions = versions_to_build + + for version, buildinfo in rootless_versions: + sha256 = buildinfo["sha256"] + original_tags = buildinfo["tags"] + rootless_tags = generate_rootless_tags(original_tags) + build_and_push(sha256, version, rootless_tags, args.push_tags, args.multiarch, + dockerfile="Dockerfile.rootless", builder_suffix="-rootless") + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/build.py b/build.py index 71b83ff..73aad02 100755 --- a/build.py +++ b/build.py @@ -1,109 +1,15 @@ #!/usr/bin/env python3 +""" +Legacy wrapper script for backwards compatibility. +This script now calls build-unified.py to build regular images. +""" -import os -import json import subprocess -import shutil import sys -import tempfile +# Convert arguments and pass to unified script +args = ["./build-unified.py"] +args.extend(sys.argv[1:]) -PLATFORMS = [ - "linux/arm64", - "linux/amd64", -] - - -def create_builder(build_dir, builder_name, platform): - check_exists_command = ["docker", "buildx", "inspect", builder_name] - if subprocess.run(check_exists_command, stderr=subprocess.DEVNULL).returncode != 0: - create_command = ["docker", "buildx", "create", "--platform", platform, "--name", builder_name] - try: - subprocess.run(create_command, cwd=build_dir, check=True) - except subprocess.CalledProcessError: - print("Creating builder failed") - exit(1) - - -def build_and_push_multiarch(build_dir, build_args, push): - builder_name = "factoriotools-multiarch" - platform=",".join(PLATFORMS) - create_builder(build_dir, builder_name, platform) - build_command = ["docker", "buildx", "build", "--platform", platform, "--builder", builder_name] + build_args - if push: - build_command.append("--push") - try: - subprocess.run(build_command, cwd=build_dir, check=True) - except subprocess.CalledProcessError: - print("Build and push of image failed") - exit(1) - - -def build_singlearch(build_dir, build_args): - build_command = ["docker", "build"] + build_args - try: - subprocess.run(build_command, cwd=build_dir, check=True) - except subprocess.CalledProcessError: - print("Build of image failed") - exit(1) - - -def push_singlearch(tags): - for tag in tags: - try: - subprocess.run(["docker", "push", f"factoriotools/factorio:{tag}"], - check=True) - except subprocess.CalledProcessError: - print("Docker push failed") - exit(1) - - -def build_and_push(sha256, version, tags, push, multiarch): - build_dir = tempfile.mktemp() - shutil.copytree("docker", build_dir) - build_args = ["--build-arg", f"VERSION={version}", "--build-arg", f"SHA256={sha256}", "."] - for tag in tags: - build_args.extend(["-t", f"factoriotools/factorio:{tag}"]) - if multiarch: - build_and_push_multiarch(build_dir, build_args, push) - else: - build_singlearch(build_dir, build_args) - if push: - push_singlearch(tags) - - -def login(): - try: - username = os.environ["DOCKER_USERNAME"] - password = os.environ["DOCKER_PASSWORD"] - subprocess.run(["docker", "login", "-u", username, "-p", password], check=True) - except KeyError: - print("Username and password need to be given") - exit(1) - except subprocess.CalledProcessError: - print("Docker login failed") - exit(1) - - -def main(push_tags=False, multiarch=False): - with open(os.path.join(os.path.dirname(__file__), "buildinfo.json")) as file_handle: - builddata = json.load(file_handle) - - if push_tags: - login() - - for version, buildinfo in sorted(builddata.items(), key=lambda item: item[0], reverse=True): - sha256 = buildinfo["sha256"] - tags = buildinfo["tags"] - build_and_push(sha256, version, tags, push_tags, multiarch) - - -if __name__ == '__main__': - push_tags = False - multiarch = False - for arg in sys.argv[1:]: - if arg == "--push-tags": - push_tags = True - elif arg == "--multiarch": - multiarch = True - main(push_tags, multiarch) +# Execute the unified build script +sys.exit(subprocess.call(args)) \ No newline at end of file