Compare commits

...

10 Commits

Author SHA1 Message Date
Florian Kinder
708e9526ef docs: Consolidate rootless documentation and mark as experimental
- Remove separate README-ROOTLESS.md file
- Integrate rootless documentation into main README.md
- Mark rootless support as experimental
- Add clear documentation about limitations and use cases
- Include warning about experimental nature

This consolidates all documentation in one place and makes it clear
that rootless support is still experimental.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-12 13:44:28 +09:00
Florian Kinder
44e651a3a6 refactor: Replace build system with unified solution
- 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 <noreply@anthropic.com>
2025-07-12 13:41:45 +09:00
Florian Kinder
9a401315f5 chore: Add Python cache to .gitignore and remove from repo
- Add __pycache__/ and Python compiled files to .gitignore
- Remove accidentally committed __pycache__ directory
- Prevent future Python cache files from being tracked

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-12 13:17:22 +09:00
Florian Kinder
f86d9ef67f refactor: Unify build system for regular and rootless images
- Create build-unified.py that handles both regular and rootless builds
- Convert build.py and build-rootless.py to wrapper scripts for backwards compatibility
- Update CI workflow to use unified build command
- Add BUILD_MIGRATION.md documentation
- Eliminate code duplication between build scripts
- Support flexible build options: --rootless, --both, --only-stable-latest

This maintains all existing functionality while providing a cleaner, more maintainable build system.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-12 13:12:50 +09:00
Florian Kinder
4460fb51e6 feat: Add rootless image building to CI pipeline
- Update docker-build.yml workflow to build rootless variants
- Rootless images are built after regular images with -rootless suffix
- Both use the same multi-architecture build process
- Triggered automatically when buildinfo.json changes

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-12 13:02:56 +09:00
Florian Kinder
445a3291cc fix: Address linting issues in rootless Docker implementation
- Add --no-install-recommends to apt-get install in Dockerfile
- Consolidate consecutive RUN instructions in Dockerfile
- Fix shellcheck warnings: quote variables and use -n instead of \! -z
- These changes improve best practices without affecting functionality

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-06 19:35:51 +09:00
Florian Kinder
7e59315e3d feat: Add rootless Docker support
Implements #547 - Add support for rootless Docker images to avoid permission issues.

Key changes:
- Add Dockerfile.rootless that runs as UID 1000 by default
- Create simplified entrypoint script without chown operations
- Add build-rootless.py to build rootless variants with -rootless suffix
- Document rootless usage in README-ROOTLESS.md
- Update main README with rootless section

The rootless images eliminate common permission problems by:
- Running as non-root from the start (USER 1000:1000)
- Avoiding recursive chown operations that can cause race conditions
- Using open permissions (777) on directories during build
- Not supporting PUID/PGID environment variables

This provides a cleaner solution for rootless Docker users and those
experiencing permission issues with volumes.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-03 20:07:42 +09:00
Victor9401
9464758a7b Updated mods updater script to correctly process mod dependencies (#557)
Co-authored-by: Victor <victor@blockbank.ai>
2025-07-03 19:48:23 +09:00
Florian Kinder
50f04fb096 Fix README tag pollution from update script (#573)
* fix: Improve README tag generation to reduce clutter

- Modified update.sh to only show the latest and stable versions
- Removed duplicate major.minor version tags
- Changed from listing all versions to showing only the most relevant tags
- Fixed jq query to properly detect stable version using index() instead of contains()

This significantly reduces README pollution by showing only:
- The latest experimental version with its tags
- The current stable version with its tags
- One entry per major.minor version for older releases (removed from this commit)

Before: 60+ lines of tags with many duplicates
After: 2 lines showing only latest and stable versions

* fix: Address shellcheck warnings about subshell variable modifications

- Changed from pipeline to process substitution to avoid SC2030/SC2031 warnings
- Variables modified in the while loop are now properly preserved
- This ensures readme_tags modifications are not lost in subshells
2025-07-03 19:45:29 +09:00
Florian Kinder
15d31c9a2e docs: Add documentation for PRESET environment variable (#572)
- Add PRESET to the environment variables table
- Include detailed explanation of available preset values
- Add example showing how to use PRESET when generating a new map
- Document that PRESET is optional and only used with GENERATE_NEW_SAVE=true

Fixes #571

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-07-03 19:32:40 +09:00
9 changed files with 584 additions and 89 deletions

View File

@@ -20,10 +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
./build.py --push-tags --multiarch --both

5
.gitignore vendored
View File

@@ -1,2 +1,7 @@
# IDE
.idea
# Python
__pycache__/
*.py[cod]
*$py.class

View File

@@ -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

140
README.md
View File

@@ -6,59 +6,9 @@
[中文](./README_zh_CN.md)
<!-- start autogeneration tags -->
* `2.0.58`, `latest`
* `2.0.57`
* `2`, `2.0`, `2.0.55`, `stable`, `stable-2.0.55`
* `2.0.54`
* `2.0.53`
* `2.0.52`
* `2.0.51`
* `2.0.50`
* `2.0.49`
* `2.0.48`
* `2.0`, `2.0.47`, `stable-2.0.47`
* `2.0.46`
* `2.0.45`
* `2.0.44`
* `2.0`, `2.0.43`, `stable-2.0.43`
* `2.0`, `2.0.42`, `stable-2.0.42`
* `2.0`, `2.0.41`, `stable-2.0.41`
* `2.0.40`
* `2.0`, `2.0.39`, `stable-2.0.39`
* `2.0.38`
* `2.0.37`
* `2.0.36`
* `2.0.35`
* `2.0.34`
* `2.0.33`
* `2.0`, `2.0.32`, `stable-2.0.32`
* `2.0.31`
* `2.0`, `2.0.30`, `stable-2.0.30`
* `2.0.29`
* `2.0`, `2.0.28`, `stable-2.0.28`
* `2.0.27`
* `2.0.26`
* `2.0.25`
* `2.0.24`
* `2.0`, `2.0.23`, `stable-2.0.23`
* `2.0.22`
* `2.0`, `2.0.21`, `stable-2.0.21`
* `2.0`, `2.0.20`, `stable-2.0.20`
* `2.0.19`
* `2.0.18`
* `2.0.17`
* `2.0.16`
* `2.0`, `2.0.15`, `stable-2.0.15`
* `2.0`, `2.0.14`, `stable-2.0.14`
* `2.0`, `2.0.13`, `stable-2.0.13`
* `1`, `1.1`, `1.1.110`, `stable-1.1.110`
* `1.0`, `1.0.0`
* `0.17`, `0.17.79`
* `0.16`, `0.16.51`
* `0.15`, `0.15.40`
* `0.14`, `0.14.23`
* `0.13`, `0.13.20`
* `0.12`, `0.12.35`<!-- end autogeneration tags -->
* `latest, 2.0.58`
* `2, 2.0, 2.0.55, stable, stable-2.0.55`
<!-- end autogeneration tags -->
## Tag descriptions
@@ -203,6 +153,22 @@ sudo docker run -d \
factoriotools/factorio
```
To generate a new map with a specific preset (e.g., death-world):
```shell
sudo docker run -d \
-p 34197:34197/udp \
-p 27015:27015/tcp \
-v /opt/factorio:/factorio \
-e LOAD_LATEST_SAVE=false \
-e GENERATE_NEW_SAVE=true \
-e SAVE_NAME=replaceme \
-e PRESET=death-world \
--name factorio \
--restart=unless-stopped \
factoriotools/factorio
```
### Mods
Copy mods into the mods folder and restart the server.
@@ -321,6 +287,7 @@ These are the environment variables which can be specified at container run time
| BIND | IP address (v4 or v6) the server listens on (IP\[:PORT]) | | 0.15+ |
| RCON_PORT | TCP port the rcon server listens on | 27015 | 0.15+ |
| SAVE_NAME | Name to use for the save file | _autosave1 | 0.17+ |
| PRESET | Map generation preset when GENERATE_NEW_SAVE is true | | 0.17+ |
| TOKEN | factorio.com token | | 0.17+ |
| UPDATE_MODS_ON_START | If mods should be updated before starting the server | | 0.17+ |
| USERNAME | factorio.com username | | 0.17+ |
@@ -330,6 +297,20 @@ These are the environment variables which can be specified at container run time
**Note:** All environment variables are compared as strings
#### PRESET Values
The `PRESET` environment variable is used when generating a new map (when `GENERATE_NEW_SAVE=true`). It corresponds to Factorio's built-in map generation presets. Common values include:
- `default` - Normal settings
- `rich-resources` - Resources are more abundant
- `marathon` - Recipes and technologies are more expensive
- `death-world` - Biters are more aggressive and numerous
- `death-world-marathon` - Combines death-world and marathon settings
- `rail-world` - Resources are further apart, encouraging train usage
- `ribbon-world` - Map height is limited for a unique challenge
If PRESET is not specified or left empty, the map will be generated using the settings from `map-gen-settings.json` and `map-settings.json` without a preset.
## Container Details
The philosophy is to [keep it simple](http://wiki.c2.com/?KeepItSimple).
@@ -458,6 +439,59 @@ stream {
If your factorio host uses multiple IP addresses (very common with IPv6), you might additionally need to bind Factorio to a single IP (otherwise the UDP proxy might get confused with IP mismatches). To do that pass the `BIND` envvar to the container: `docker run --network=host -e BIND=2a02:1234::5678 ...`
## Rootless Docker Support (Experimental)
> **Note**: Rootless support is currently experimental. Please report any issues you encounter.
If you're experiencing permission issues or want better security, consider using the rootless images. These images are designed to work seamlessly with rootless Docker installations and avoid common permission problems.
### What are Rootless Images?
The rootless images differ from regular images in several ways:
- Run as UID 1000 (non-root) by default
- No dynamic UID/GID mapping (PUID/PGID not supported)
- No runtime chown operations
- All directories created with open permissions during build
### Rootless Image Tags
Each regular tag has a corresponding rootless version with the `-rootless` suffix:
- `latest-rootless` (experimental)
- `stable-rootless` (experimental)
- `2.0.55-rootless` (experimental)
### Quick Start with Rootless
```shell
docker run -d \
-p 34197:34197/udp \
-p 27015:27015/tcp \
-v ~/factorio:/factorio \
--name factorio \
--restart=unless-stopped \
factoriotools/factorio:stable-rootless
```
Key differences:
- No `chown` command needed
- No PUID/PGID environment variables
- Runs as UID 1000 by default
- No permission issues with volumes
### When to Use Rootless Images
Consider using rootless images if you:
- Are running Docker in rootless mode
- Experience permission issues with volume mounts
- Want to avoid containers running as root
- Don't need dynamic UID/GID mapping via PUID/PGID
### Limitations
- PUID/PGID environment variables are not supported
- Fixed to UID 1000 (may not match your host user)
- Experimental feature - may have undiscovered issues
## Troubleshooting
### My server is listed in the server browser, but nobody can connect

View File

@@ -6,6 +6,7 @@ import subprocess
import shutil
import sys
import tempfile
import argparse
PLATFORMS = [
@@ -25,9 +26,9 @@ def create_builder(build_dir, builder_name, platform):
exit(1)
def build_and_push_multiarch(build_dir, build_args, push):
builder_name = "factoriotools-multiarch"
platform=",".join(PLATFORMS)
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:
@@ -35,16 +36,16 @@ def build_and_push_multiarch(build_dir, build_args, push):
try:
subprocess.run(build_command, cwd=build_dir, check=True)
except subprocess.CalledProcessError:
print("Build and push of image failed")
print(f"Build and push of {builder_suffix or 'regular'} image failed")
exit(1)
def build_singlearch(build_dir, build_args):
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("Build of image failed")
print(f"Build of {image_type} image failed")
exit(1)
@@ -58,16 +59,19 @@ def push_singlearch(tags):
exit(1)
def build_and_push(sha256, version, tags, push, multiarch):
def build_and_push(sha256, version, tags, push, multiarch, dockerfile="Dockerfile", builder_suffix=""):
build_dir = tempfile.mktemp()
shutil.copytree("docker", build_dir)
build_args = ["--build-arg", f"VERSION={version}", "--build-arg", f"SHA256={sha256}", "."]
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)
build_and_push_multiarch(build_dir, build_args, push, builder_suffix)
else:
build_singlearch(build_dir, build_args)
build_singlearch(build_dir, build_args, image_type)
if push:
push_singlearch(tags)
@@ -85,25 +89,69 @@ def login():
exit(1)
def main(push_tags=False, multiarch=False):
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 push_tags:
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):
sha256 = buildinfo["sha256"]
tags = buildinfo["tags"]
build_and_push(sha256, version, tags, push_tags, multiarch)
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__':
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)
main()

View File

@@ -0,0 +1,91 @@
# build rcon client
FROM debian:stable-slim AS rcon-builder
RUN apt-get -q update \
&& DEBIAN_FRONTEND=noninteractive apt-get -qy install build-essential --no-install-recommends
WORKDIR /src
COPY rcon/ /src
RUN make
# build factorio image
FROM debian:stable-slim
LABEL maintainer="https://github.com/factoriotools/factorio-docker"
ARG BOX64_VERSION=v0.2.4
# optionally utilize a built-in map-gen-preset (see data/base/prototypes/map-gen-presets
ARG PRESET
# number of retries that curl will use when pulling the headless server tarball
ARG CURL_RETRIES=8
ENV PORT=34197 \
RCON_PORT=27015 \
SAVES=/factorio/saves \
PRESET="$PRESET" \
CONFIG=/factorio/config \
MODS=/factorio/mods \
SCENARIOS=/factorio/scenarios \
SCRIPTOUTPUT=/factorio/script-output \
DLC_SPACE_AGE="true"
SHELL ["/bin/bash", "-eo", "pipefail", "-c"]
RUN apt-get -q update \
&& DEBIAN_FRONTEND=noninteractive apt-get -qy install ca-certificates curl jq pwgen xz-utils procps gettext-base --no-install-recommends \
&& if [[ "$(uname -m)" == "aarch64" ]]; then \
echo "installing ARM compatability layer" \
&& DEBIAN_FRONTEND=noninteractive apt-get -qy install unzip --no-install-recommends \
&& curl -LO https://github.com/ptitSeb/box64/releases/download/${BOX64_VERSION}/box64-GENERIC_ARM-RelWithDebInfo.zip \
&& unzip box64-GENERIC_ARM-RelWithDebInfo.zip -d /bin \
&& rm -f box64-GENERIC_ARM-RelWithDebInfo.zip \
&& chmod +x /bin/box64; \
fi \
&& rm -rf /var/lib/apt/lists/*
# version checksum of the archive to download
ARG VERSION
ARG SHA256
LABEL factorio.version=${VERSION}
ENV VERSION=${VERSION} \
SHA256=${SHA256}
RUN set -ox pipefail \
&& if [[ "${VERSION}" == "" ]]; then \
echo "build-arg VERSION is required" \
&& exit 1; \
fi \
&& if [[ "${SHA256}" == "" ]]; then \
echo "build-arg SHA256 is required" \
&& exit 1; \
fi \
&& archive="/tmp/factorio_headless_x64_$VERSION.tar.xz" \
&& mkdir -p /opt /factorio \
&& curl -sSL "https://www.factorio.com/get-download/$VERSION/headless/linux64" -o "$archive" --retry $CURL_RETRIES \
&& echo "$SHA256 $archive" | sha256sum -c \
|| (sha256sum "$archive" && file "$archive" && exit 1) \
&& tar xf "$archive" --directory /opt \
&& chmod ugo=rwx /opt/factorio \
&& rm "$archive" \
&& ln -s "$SCENARIOS" /opt/factorio/scenarios \
&& ln -s "$SAVES" /opt/factorio/saves \
&& mkdir -p /opt/factorio/config/
COPY files/*.sh /
COPY files/docker-entrypoint-rootless.sh /docker-entrypoint.sh
COPY files/config.ini /opt/factorio/config/config.ini
COPY --from=rcon-builder /src/rcon /bin/rcon
# Make all scripts executable and set proper permissions for the factorio directory
RUN chmod +x /*.sh \
&& chmod -R 777 /opt/factorio /factorio
VOLUME /factorio
EXPOSE $PORT/udp $RCON_PORT/tcp
# Run as non-root user (UID 1000 is common for the first user in rootless containers)
USER 1000:1000
ENTRYPOINT ["/docker-entrypoint.sh"]

View File

@@ -0,0 +1,124 @@
#!/bin/bash
set -eoux pipefail
INSTALLED_DIRECTORY=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")
FACTORIO_VOL=/factorio
LOAD_LATEST_SAVE="${LOAD_LATEST_SAVE:-true}"
GENERATE_NEW_SAVE="${GENERATE_NEW_SAVE:-false}"
PRESET="${PRESET:-""}"
SAVE_NAME="${SAVE_NAME:-""}"
BIND="${BIND:-""}"
CONSOLE_LOG_LOCATION="${CONSOLE_LOG_LOCATION:-""}"
# Create directories if they don't exist
# In rootless mode, these should be writable by the container user
mkdir -p "$FACTORIO_VOL"
mkdir -p "$SAVES"
mkdir -p "$CONFIG"
mkdir -p "$MODS"
mkdir -p "$SCENARIOS"
mkdir -p "$SCRIPTOUTPUT"
# Generate RCON password if needed
if [[ ! -f $CONFIG/rconpw ]]; then
pwgen 15 1 >"$CONFIG/rconpw"
fi
# Copy default configs if they don't exist
if [[ ! -f $CONFIG/server-settings.json ]]; then
cp /opt/factorio/data/server-settings.example.json "$CONFIG/server-settings.json"
fi
if [[ ! -f $CONFIG/map-gen-settings.json ]]; then
cp /opt/factorio/data/map-gen-settings.example.json "$CONFIG/map-gen-settings.json"
fi
if [[ ! -f $CONFIG/map-settings.json ]]; then
cp /opt/factorio/data/map-settings.example.json "$CONFIG/map-settings.json"
fi
# Clean up incomplete saves
NRTMPSAVES=$( find -L "$SAVES" -iname \*.tmp.zip -mindepth 1 | wc -l )
if [[ $NRTMPSAVES -gt 0 ]]; then
rm -f "$SAVES"/*.tmp.zip
fi
# Update mods if requested
if [[ ${UPDATE_MODS_ON_START:-} == "true" ]]; then
"${INSTALLED_DIRECTORY}"/docker-update-mods.sh
fi
# Handle DLC
"${INSTALLED_DIRECTORY}"/docker-dlc.sh
# In rootless mode, we don't need to handle user switching or chown
# The container runs as the specified user from the start
EXEC=""
if [[ -f /bin/box64 ]]; then
# Use emulator for ARM hosts
EXEC="/bin/box64"
fi
# Update config path
sed -i '/write-data=/c\write-data=\/factorio/' /opt/factorio/config/config.ini
# Generate new save if needed
NRSAVES=$(find -L "$SAVES" -iname \*.zip -mindepth 1 | wc -l)
if [[ $GENERATE_NEW_SAVE != true && $NRSAVES == 0 ]]; then
GENERATE_NEW_SAVE=true
SAVE_NAME=_autosave1
fi
if [[ $GENERATE_NEW_SAVE == true ]]; then
if [[ -z "$SAVE_NAME" ]]; then
echo "If \$GENERATE_NEW_SAVE is true, you must specify \$SAVE_NAME"
exit 1
fi
if [[ -f "$SAVES/$SAVE_NAME.zip" ]]; then
echo "Map $SAVES/$SAVE_NAME.zip already exists, skipping map generation"
else
if [[ -n "$PRESET" ]]; then
$EXEC /opt/factorio/bin/x64/factorio \
--create "$SAVES/$SAVE_NAME.zip" \
--preset "$PRESET" \
--map-gen-settings "$CONFIG/map-gen-settings.json" \
--map-settings "$CONFIG/map-settings.json"
else
$EXEC /opt/factorio/bin/x64/factorio \
--create "$SAVES/$SAVE_NAME.zip" \
--map-gen-settings "$CONFIG/map-gen-settings.json" \
--map-settings "$CONFIG/map-settings.json"
fi
fi
fi
# Build command flags
FLAGS=(\
--port "$PORT" \
--server-settings "$CONFIG/server-settings.json" \
--server-banlist "$CONFIG/server-banlist.json" \
--rcon-port "$RCON_PORT" \
--server-whitelist "$CONFIG/server-whitelist.json" \
--use-server-whitelist \
--server-adminlist "$CONFIG/server-adminlist.json" \
--rcon-password "$(cat "$CONFIG/rconpw")" \
--server-id /factorio/config/server-id.json \
--mod-directory "$MODS" \
)
if [ -n "$CONSOLE_LOG_LOCATION" ]; then
FLAGS+=( --console-log "$CONSOLE_LOG_LOCATION" )
fi
if [ -n "$BIND" ]; then
FLAGS+=( --bind "$BIND" )
fi
if [[ $LOAD_LATEST_SAVE == true ]]; then
FLAGS+=( --start-server-load-latest )
else
FLAGS+=( --start-server "$SAVE_NAME" )
fi
# Execute factorio
# In rootless mode, we run directly without user switching
exec $EXEC /opt/factorio/bin/x64/factorio "${FLAGS[@]}" "$@"

View File

@@ -23,6 +23,128 @@ print_failure()
echo "$1"
}
# Checks game version vs version in mod.
# Returns 0 if major version differs or mod minor version is less than game version, 1 if ok
check_game_version() {
local game_version="$1"
local mod_version="$2"
local game_major mod_major game_minor mod_minor
game_major=$(echo "$game_version" | cut -d '.' -f1)
game_minor=$(echo "$game_version" | cut -d '.' -f2)
mod_major=$(echo "$mod_version" | cut -d '.' -f1)
mod_minor=$(echo "$mod_version" | cut -d '.' -f2)
if [[ "$game_major" -ne "$mod_major" ]]; then
echo 0
return
fi
if [[ "$mod_minor" -ge "$game_minor" ]]; then
echo 1
else
echo 0
fi
}
# Checks dependency string with provided version.
# Only checks for operator based string, ignoring everything else
# Returns 1 if check is ok, 0 if not
check_dependency_version()
{
local dependency="$1"
local mod_version="$2"
if [[ "$dependency" =~ ^(\?|!|~|\(~\)) ]]; then
echo 1
fi
local condition
condition=$(echo "$dependency" | grep -oE '(>=|<=|>|<|=) [0-9]+(\.[0-9]+)*')
if [[ -z "$condition" ]]; then
echo 1
fi
local operator required_version
operator=$(echo "$condition" | awk '{print $1}')
required_version=$(echo "$condition" | awk '{print $2}')
case "$operator" in
">=")
if [[ "$(printf '%s\n%s\n' "$required_version" "$mod_version" | sort -V | head -n1)" == "$required_version" ]]; then
echo 1
else
echo 0
fi
;;
">")
if [[ "$(printf '%s\n%s\n' "$required_version" "$mod_version" | sort -V | head -n1)" == "$required_version" && "$required_version" != "$FACTORIO_VERSION" ]]; then
echo 1
else
echo 0
fi
;;
"<=")
if [[ "$(printf '%s\n%s\n' "$required_version" "$mod_version" | sort -V | tail -n1)" == "$required_version" ]]; then
echo 1
else
echo 0
fi
;;
"<")
if [[ "$(printf '%s\n%s\n' "$required_version" "$mod_version" | sort -V | tail -n1)" == "$required_version" && "$required_version" != "$FACTORIO_VERSION" ]]; then
echo 1
else
echo 0
fi
;;
"=")
if [[ "$mod_version" == "$required_version" ]]; then
echo 1
else
echo 0
fi
;;
*)
echo 0
;;
esac
}
get_mod_info()
{
local mod_info_json="$1"
while IFS= read -r mod_release_info; do
local mod_version mod_factorio_version
mod_version=$(echo "$mod_release_info" | jq -r ".version")
mod_factorio_version=$(echo "$mod_release_info" | jq -r ".info_json.factorio_version")
if [[ $(check_game_version "$mod_factorio_version" "$FACTORIO_VERSION") == 0 ]]; then
echo " Skipping mod version $mod_version because of factorio version mismatch" >&2
continue
fi
# If we found 'dependencies' element, we also check versions there
if [[ $(echo "$mod_release_info" | jq -e '.info_json | has("dependencies") and (.dependencies | length > 0)') == true ]]; then
while IFS= read -r dependency; do
# We only check for 'base' dependency
if [[ "$dependency" == base* ]] && [[ $(check_dependency_version "$dependency" "$FACTORIO_VERSION") == 0 ]]; then
echo " Skipping mod version $mod_version, unsatisfied base dependency: $dependency" >&2
continue 2
fi
done < <(echo "$mod_release_info" | jq -r '.info_json.dependencies[]')
fi
echo "$mod_release_info" | jq -j ".file_name, \";\", .download_url, \";\", .sha1"
break
done < <(echo "$mod_info_json" | jq -c ".releases|sort_by(.released_at)|reverse|.[]")
}
update_mod()
{
MOD_NAME="$1"
@@ -30,7 +152,7 @@ update_mod()
print_step "Checking for update of mod $MOD_NAME for factorio $FACTORIO_VERSION ..."
MOD_INFO_URL="$MOD_BASE_URL/api/mods/$MOD_NAME_ENCODED"
MOD_INFO_URL="$MOD_BASE_URL/api/mods/$MOD_NAME_ENCODED/full"
MOD_INFO_JSON=$(curl --silent "$MOD_INFO_URL")
if ! echo "$MOD_INFO_JSON" | jq -e .name >/dev/null; then
@@ -38,7 +160,12 @@ update_mod()
return 0
fi
MOD_INFO=$(echo "$MOD_INFO_JSON" | jq -j --arg version "$FACTORIO_VERSION" ".releases|reverse|map(select(.info_json.factorio_version as \$mod_version | \$version | startswith(\$mod_version)))[0]|.file_name, \";\", .download_url, \";\", .sha1")
MOD_INFO=$(get_mod_info "$MOD_INFO_JSON")
if [[ "$MOD_INFO" == "" ]]; then
print_failure " Not compatible with version"
return 0
fi
MOD_FILENAME=$(echo "$MOD_INFO" | cut -f1 -d";")
MOD_URL=$(echo "$MOD_INFO" | cut -f2 -d";")
@@ -90,7 +217,7 @@ update_mod()
if [[ -f $MOD_DIR/mod-list.json ]]; then
jq -r ".mods|map(select(.enabled))|.[].name" "$MOD_DIR/mod-list.json" | while read -r mod; do
if [[ $mod != base ]]; then
update_mod "$mod"
update_mod "$mod" || true
fi
done
fi

View File

@@ -91,10 +91,64 @@ if [[ $experimental_online_version != "$stable_online_version" ]]; then
fi
rm -f -- "$tmpfile"
readme_tags=$(jq --sort-keys 'keys[]' buildinfo.json | tac | (while read -r line
do
tags="$tags\n* "$(jq --sort-keys ".$line.tags | sort | .[]" buildinfo.json | sed 's/"/`/g' | sed ':a; /$/N; s/\n/, /; ta')
done && printf "%s\n\n" "$tags"))
# Generate README tags with logical sorting and de-duplication
# First, collect all unique tags with their versions
declare -A tag_versions
while IFS= read -r version; do
while IFS= read -r tag; do
# If this tag is already seen, compare versions to keep the latest
if [[ -n "${tag_versions[$tag]}" ]]; then
# Compare version strings - keep the higher one
if [[ "$version" > "${tag_versions[$tag]}" ]]; then
tag_versions[$tag]="$version"
fi
else
tag_versions[$tag]="$version"
fi
done < <(jq -r ".\"$version\".tags[]" buildinfo.json)
done < <(jq -r 'keys[]' buildinfo.json | sort -V -r)
# Build the tags list for README
readme_tags=""
# First add the current latest and stable tags
latest_version=$(jq -r 'to_entries | map(select(.value.tags | contains(["latest"]))) | .[0].key' buildinfo.json)
stable_version=$(jq -r 'to_entries | map(select(.value.tags | index("stable"))) | .[0].key' buildinfo.json)
if [[ -n "$latest_version" ]]; then
latest_tags=$(jq -r ".\"$latest_version\".tags | map(select(. == \"latest\" or . == \"$latest_version\")) | join(\", \")" buildinfo.json | sed 's/"/`/g')
readme_tags="${readme_tags}\n* \`${latest_tags}\`"
fi
if [[ -n "$stable_version" ]] && [[ "$stable_version" != "$latest_version" ]]; then
stable_tags=$(jq -r ".\"$stable_version\".tags | sort | join(\", \")" buildinfo.json | sed 's/"/`/g')
readme_tags="${readme_tags}\n* \`${stable_tags}\`"
fi
# Add major.minor tags (e.g., 2.0, 1.1) - only the latest version for each
declare -A major_minor_seen
while IFS= read -r version; do
if [[ "$version" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
major="${BASH_REMATCH[1]}"
minor="${BASH_REMATCH[2]}"
major_minor="$major.$minor"
# Skip if this is the latest or stable version (already added above)
if [[ "$version" == "$latest_version" ]] || [[ "$version" == "$stable_version" ]]; then
continue
fi
# Only add if we haven't seen this major.minor yet
if [[ -z "${major_minor_seen[$major_minor]}" ]]; then
major_minor_seen[$major_minor]=1
tags=$(jq -r ".\"$version\".tags | join(\", \")" buildinfo.json | sed 's/"/`/g')
if [[ -n "$tags" ]]; then
readme_tags="${readme_tags}\n* \`${tags}\`"
fi
fi
fi
done < <(jq -r 'keys[]' buildinfo.json | sort -V -r)
readme_tags="${readme_tags}\n"
perl -i -0777 -pe "s/<!-- start autogeneration tags -->.+<!-- end autogeneration tags -->/<!-- start autogeneration tags -->$readme_tags<!-- end autogeneration tags -->/s" README.md