From 44e651a3a6c6899f731d965fd33a0019ef5e1bd4 Mon Sep 17 00:00:00 2001 From: Florian Kinder Date: Sat, 12 Jul 2025 13:41:45 +0900 Subject: [PATCH] refactor: Replace build system with unified solution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove old build.py and build-rootless.py wrapper scripts - Rename build-unified.py to build.py as the main build script - Delete BUILD_MIGRATION.md (no longer needed) - Update CI workflow to use new build.py syntax - Update documentation in CLAUDE.md and README-ROOTLESS.md The new build system provides all functionality in a single script: - Default: builds regular images - --rootless: builds only rootless images - --both: builds both regular and rootless images - --multiarch and --push-tags: work as before This creates a cleaner, more maintainable build system. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .github/workflows/docker-build.yml | 2 +- BUILD_MIGRATION.md | 61 ----------- CLAUDE.md | 20 +++- README-ROOTLESS.md | 11 +- build-rootless.py | 15 --- build-unified.py | 157 ---------------------------- build.py | 160 +++++++++++++++++++++++++++-- 7 files changed, 175 insertions(+), 251 deletions(-) delete mode 100644 BUILD_MIGRATION.md delete mode 100755 build-rootless.py delete mode 100755 build-unified.py diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 367fadb..0ce97a9 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -26,4 +26,4 @@ jobs: DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} run: | - ./build-unified.py --push-tags --multiarch --both \ No newline at end of file + ./build.py --push-tags --multiarch --both \ No newline at end of file diff --git a/BUILD_MIGRATION.md b/BUILD_MIGRATION.md deleted file mode 100644 index 00c6b8f..0000000 --- a/BUILD_MIGRATION.md +++ /dev/null @@ -1,61 +0,0 @@ -# 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/CLAUDE.md b/CLAUDE.md index 0559297..642f824 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,8 +11,9 @@ This is a Docker image for running a Factorio headless server. It provides autom ### Key Components 1. **Docker Image Build System** - - `build.py` - Python script that builds Docker images from `buildinfo.json` + - `build.py` - Unified Python script that builds both regular and rootless Docker images from `buildinfo.json` - `docker/Dockerfile` - Main Dockerfile that creates the Factorio server image + - `docker/Dockerfile.rootless` - Dockerfile for rootless variant (runs as UID 1000) - `buildinfo.json` - Contains version info, SHA256 checksums, and tags for all supported versions - Supports multi-architecture builds (linux/amd64, linux/arm64) using Docker buildx @@ -38,11 +39,20 @@ This is a Docker image for running a Factorio headless server. It provides autom ### Building Images ```bash -# Build a single architecture image locally +# Build regular images locally (single architecture) python3 build.py -# Build and push multi-architecture images +# Build rootless images only +python3 build.py --rootless + +# Build both regular and rootless images +python3 build.py --both + +# Build and push multi-architecture images (regular only) python3 build.py --multiarch --push-tags + +# Build and push both regular and rootless multi-architecture images +python3 build.py --multiarch --push-tags --both ``` ### Running the Container @@ -109,6 +119,8 @@ Version updates are automated via GitHub Actions that run `update.sh` periodical ## Testing Changes 1. Modify `buildinfo.json` to test specific versions -2. Run `python3 build.py` to build locally +2. Run `python3 build.py` to build regular images locally + - Use `python3 build.py --rootless` for rootless images + - Use `python3 build.py --both` to build both variants 3. Test the container with your local data volume 4. For production changes, ensure `update.sh` handles version transitions correctly \ No newline at end of file diff --git a/README-ROOTLESS.md b/README-ROOTLESS.md index 260a8ab..6cfee42 100644 --- a/README-ROOTLESS.md +++ b/README-ROOTLESS.md @@ -120,11 +120,14 @@ docker run -d \ To build rootless images locally: ```bash -# Build for current architecture -python3 build-rootless.py +# Build rootless images for current architecture +python3 build.py --rootless -# Build and push multi-arch images -python3 build-rootless.py --multiarch --push-tags +# Build and push multi-arch rootless images +python3 build.py --rootless --multiarch --push-tags + +# Build both regular and rootless images +python3 build.py --both --multiarch --push-tags ``` ## Why Use Rootless Images? diff --git a/build-rootless.py b/build-rootless.py deleted file mode 100755 index 16fbad2..0000000 --- a/build-rootless.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 -""" -Legacy wrapper script for backwards compatibility. -This script now calls build-unified.py to build rootless images. -""" - -import subprocess -import sys - -# Convert arguments and pass to unified script with --rootless flag -args = ["./build-unified.py", "--rootless", "--only-stable-latest"] -args.extend(sys.argv[1:]) - -# 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 deleted file mode 100755 index 0ba872a..0000000 --- a/build-unified.py +++ /dev/null @@ -1,157 +0,0 @@ -#!/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 73aad02..0ba872a 100755 --- a/build.py +++ b/build.py @@ -1,15 +1,157 @@ #!/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 +import argparse -# Convert arguments and pass to unified script -args = ["./build-unified.py"] -args.extend(sys.argv[1:]) -# Execute the unified build script -sys.exit(subprocess.call(args)) \ No newline at end of file +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