diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml new file mode 100644 index 00000000..d98100a3 --- /dev/null +++ b/.github/workflows/cd.yaml @@ -0,0 +1,117 @@ +name: CD + +on: + workflow_dispatch: + inputs: + publish: + description: "Publish artifacts from electron-builder" + required: true + default: "never" + type: choice + options: + - never + - always + linux_setup_id: + description: "Config setup ID used to source Linux engine resource" + required: true + default: "latest-linux" + type: string + windows_setup_id: + description: "Config setup ID used to source Windows engine resource" + required: true + default: "latest-win" + type: string + +defaults: + run: + shell: bash + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - os: ubuntu-latest + build_cmd: "build-linux" + arch: "--x64" + platform: "linux" + + - os: windows-latest + build_cmd: "build-win" + arch: "--x64" + platform: "win32" + + steps: + - uses: actions/checkout@v6 + with: + # This should fix git rev-list --count HEAD + # https://stackoverflow.com/a/65056108 + fetch-depth: 0 + submodules: recursive + path: repo-folder + + - uses: actions/checkout@v6 + with: + repository: gajop/spring-launcher + submodules: recursive + path: spring-launcher + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: "24" + + - name: Setup uv + uses: astral-sh/setup-uv@v7 + with: + python-version: "3.13" + + - name: Prepare folder structure + run: | + mkdir build + cp spring-launcher/* -r build/ + cp repo-folder/dist_cfg/* -r build/src/ + mkdir -p build/{bin,files,build} + [ -d build/src/bin/ ] && mv build/src/bin/* build/bin/ + [ -d build/src/files/ ] && mv build/src/files/* build/files/ + [ -d build/src/build/ ] && mv build/src/build/* build/build/ + rm -rf build/src/{bin,files,build} + + tree -a -L 3 build + + - name: Prepare packaged assets + run: | + GIT_HASH=$(git -C repo-folder rev-parse --short=12 HEAD) + PACKAGE_VERSION=1.$(git -C repo-folder rev-list --count HEAD).0 + + uv run --project ./repo-folder/build --locked sbc-packager-package \ + --repo-root ./repo-folder \ + --config-in ./repo-folder/dist_cfg/config.json \ + --config-out ./build/src/config.json \ + --files-dir ./build/files \ + --meta-out ./build/build/package-assets.json \ + --package-json ./build/package.json \ + --repo-full-name Spring-SpringBoard/SpringBoard-Core \ + --platform "${{ matrix.platform }}" \ + --git-hash "$GIT_HASH" \ + --package-version "$PACKAGE_VERSION" \ + --linux-setup-id "${{ github.event.inputs.linux_setup_id }}" \ + --windows-setup-id "${{ github.event.inputs.windows_setup_id }}" + + cat build/package.json + + - name: Build + run: | + cd build + npm install + npm run ${{ matrix.build_cmd }} -- ${{ matrix.arch }} --publish "${{ github.event.inputs.publish }}" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload build artifacts + uses: actions/upload-artifact@v6 + with: + name: springboard-${{ matrix.platform }}-bundle + path: build/dist/** + if-no-files-found: error diff --git a/.github/workflows/launcher.yml b/.github/workflows/launcher.yml deleted file mode 100644 index 40f88344..00000000 --- a/.github/workflows/launcher.yml +++ /dev/null @@ -1,78 +0,0 @@ -name: Launcher - -on: - push: - paths: - - 'dist_cfg/**' - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -defaults: - run: - shell: bash - -jobs: - build: - runs-on: ${{ matrix.os }} - strategy: - matrix: - include: - - os: ubuntu-20.04 - build_cmd: "build-linux" - arch: "--x64" - - - os: windows-latest - build_cmd: "build-win" - arch: "--x64" - - steps: - - uses: actions/checkout@v2 - with: - # This should fix git rev-list --count HEAD - # https://stackoverflow.com/a/65056108 - fetch-depth: 0 - path: repo-folder - - - uses: actions/checkout@v2 - with: - repository: gajop/spring-launcher - path: spring-launcher - - - name: Setup NodeJs - uses: actions/setup-node@v1 - with: - node-version: '17.x' - - - name: Prepare folder structure - run: | - mkdir build - cp spring-launcher/* -r build/ - cp repo-folder/dist_cfg/* -r build/src/ - mkdir -p build/{bin,files,build} - [ -d build/src/bin/ ] && mv build/src/bin/* build/bin/ - [ -d build/src/files/ ] && mv build/src/files/* build/files/ - [ -d build/src/build/ ] && mv build/src/build/* build/build/ - rm -rf build/src/{bin,files,build} - - find build/ - - exit 0 - - - name: Make package.json - run: | - cd repo-folder - export PACKAGE_VERSION=1.$(git rev-list --count HEAD).0 - echo "Making build for version: $PACKAGE_VERSION" - cd .. - node ./repo-folder/build/make_package_json.js build/package.json repo-folder/dist_cfg/config.json Spring-SpringBoard/SpringBoard-Core $PACKAGE_VERSION - - cat build/package.json - - - name: Build - run: | - cd build - npm install - npm run ${{ matrix.build_cmd }} -- ${{ matrix.arch }} --publish always - env: - GH_TOKEN: ${{ secrets.github_token }} \ No newline at end of file diff --git a/.github/workflows/luacheck.yml b/.github/workflows/luacheck.yml index e03821c5..178970ea 100644 --- a/.github/workflows/luacheck.yml +++ b/.github/workflows/luacheck.yml @@ -7,15 +7,15 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 with: - submodules: 'true' + submodules: true - name: Install luacheck run: | - pip install hererocks - hererocks env --lua 5.1 -rlatest + python3 -m pip install hererocks + python3 -m hererocks env --lua 5.1 --no-readline -rlatest source env/bin/activate luarocks install luacheck @@ -23,4 +23,3 @@ jobs: run: | source env/bin/activate luacheck scen_edit triggers libs_sb/utils libs_sb/savetable.lua --enable 1 - diff --git a/.github/workflows/packager-ci.yaml b/.github/workflows/packager-ci.yaml new file mode 100644 index 00000000..f488469d --- /dev/null +++ b/.github/workflows/packager-ci.yaml @@ -0,0 +1,33 @@ +name: Packager CI + +on: + push: + paths: + - "build/**" + - ".github/workflows/packager-ci.yaml" + pull_request: + paths: + - "build/**" + - ".github/workflows/packager-ci.yaml" + workflow_dispatch: + +defaults: + run: + shell: bash + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Setup uv + uses: astral-sh/setup-uv@v7 + with: + python-version: "3.13" + + - name: Ruff + run: uv run --project ./build --locked ruff check ./build/sbc_packager + + - name: Pyright + run: uv run --project ./build --locked pyright ./build/sbc_packager diff --git a/build/README.md b/build/README.md new file mode 100644 index 00000000..708f1be2 --- /dev/null +++ b/build/README.md @@ -0,0 +1,79 @@ +# SBC Packager + +This folder contains the Python packager used by CD to produce a fully standalone SpringBoard build. + +## Tooling + +- Python: `3.13` +- Runner: `uv` +- CLI: `typer` +- Validation: `pydantic` +- Lint/typecheck: `ruff`, `pyright` + +## Main command + +Use `sbc-packager-package` to run the full packager pipeline: + +1. Rewrite packaged config and game files (`prepare`) +2. Download and extract the platform engine (`download-engine`) +3. Generate Electron `package.json` (`make-package-json`) + +The command requires explicit git-derived values from the caller: + +- `--git-hash` +- `--package-version` + +This keeps packager behavior deterministic and avoids hidden git subprocess logic inside the package tool. + +## Local usage + +From repository root: + +```bash +git clone --depth 1 https://github.com/gajop/spring-launcher.git /tmp/spring-launcher + +mkdir -p /tmp/sbc-local-build +cp -r /tmp/spring-launcher/* /tmp/sbc-local-build/ +cp -r ./dist_cfg/* /tmp/sbc-local-build/src/ +mkdir -p /tmp/sbc-local-build/{bin,files,build} +[ -d /tmp/sbc-local-build/src/bin/ ] && mv /tmp/sbc-local-build/src/bin/* /tmp/sbc-local-build/bin/ +[ -d /tmp/sbc-local-build/src/files/ ] && mv /tmp/sbc-local-build/src/files/* /tmp/sbc-local-build/files/ +[ -d /tmp/sbc-local-build/src/build/ ] && mv /tmp/sbc-local-build/src/build/* /tmp/sbc-local-build/build/ +rm -rf /tmp/sbc-local-build/src/{bin,files,build} + +GIT_HASH=$(git rev-parse --short=12 HEAD) +PACKAGE_VERSION=1.$(git rev-list --count HEAD).0 + +uv run --project ./build --locked sbc-packager-package \ + --repo-root . \ + --config-in ./dist_cfg/config.json \ + --config-out /tmp/sbc-local-build/src/config.json \ + --files-dir /tmp/sbc-local-build/files \ + --meta-out /tmp/sbc-local-build/build/package-assets.json \ + --package-json /tmp/sbc-local-build/package.json \ + --repo-full-name Spring-SpringBoard/SpringBoard-Core \ + --platform linux \ + --git-hash "$GIT_HASH" \ + --package-version "$PACKAGE_VERSION" \ + --linux-setup-id latest-linux \ + --windows-setup-id latest-win +``` + +`/tmp/sbc-local-build` must be a launcher build tree (same structure as CD creates in `./build` job workspace). + +## Linting + +```bash +uv run --project ./build --locked ruff check ./build/sbc_packager +uv run --project ./build --locked pyright ./build/sbc_packager +``` + +## Linux AppImage sandbox note + +`make-package-json` injects `afterPack: build/sbc_after_pack.cjs`. +That hook rewrites the Linux launcher binary to ensure direct `AppImage` execution uses: + +- `--no-sandbox` +- `--disable-setuid-sandbox` + +without requiring users to pass flags manually. diff --git a/build/make_package_json.js b/build/make_package_json.js deleted file mode 100644 index ff997c9f..00000000 --- a/build/make_package_json.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const assert = require('assert'); - -function createPackagejson (packageJson, configJson, repoFullName, version) { - const configStr = fs.readFileSync(configJson); - const config = JSON.parse(configStr); - - assert(config.title != null, 'Missing config title'); - - const repoDotName = repoFullName.replace(/\//g, '.'); - - const packageTemplate = JSON.parse(fs.readFileSync(packageJson).toString()); - packageTemplate.name = config.title.replace(/ /g, '-'); - // eslint-disable-next-line no-template-curly-in-string - packageTemplate.build.artifactName = config.title + '-${version}.${ext}'; // '' is used on purpose, we want the spring to contain ${ext} as text - packageTemplate.version = version; - packageTemplate.repository = `github:${repoFullName}`; - packageTemplate.build.appId = `com.springrts.launcher.${repoDotName}`; - packageTemplate.build.publish = undefined; - if (config.dependencies != null) { - for (const dependency in config.dependencies) { - packageTemplate.dependencies[dependency] = config.dependencies[dependency]; - } - } - - fs.writeFileSync(packageJson, JSON.stringify(packageTemplate), 'utf8'); -} - -if (require.main === module) { - const args = process.argv; - if (args.length < 6) { - console.log('Wrong arguments'); - process.exit(-1); - } - - createPackagejson(args[2], args[3], args[4], args[5]) -} \ No newline at end of file diff --git a/build/pyproject.toml b/build/pyproject.toml new file mode 100644 index 00000000..03a89aa6 --- /dev/null +++ b/build/pyproject.toml @@ -0,0 +1,32 @@ +[project] +name = "sbc-packager" +version = "0.1.0" +description = "CI packaging helpers for SpringBoard Core" +requires-python = ">=3.13,<3.14" +dependencies = ["pydantic>=2.12.5", "typer>=0.23.1"] + +[project.scripts] +sbc-packager = "sbc_packager.cli:main" +sbc-packager-prepare = "sbc_packager.cli_prepare:main" +sbc-packager-download-engine = "sbc_packager.cli_download_engine:main" +sbc-packager-make-package-json = "sbc_packager.cli_make_package_json:main" +sbc-packager-package = "sbc_packager.cli_package:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[dependency-groups] +dev = ["pyright>=1.1.408", "ruff>=0.15.1"] + +[tool.ruff] +line-length = 120 +target-version = "py313" + +[tool.ruff.lint] +select = ["E", "F", "I", "UP", "B", "SIM"] + +[tool.pyright] +pythonVersion = "3.13" +typeCheckingMode = "basic" +reportMissingTypeStubs = false diff --git a/build/sbc_packager/__init__.py b/build/sbc_packager/__init__.py new file mode 100644 index 00000000..82ece9ab --- /dev/null +++ b/build/sbc_packager/__init__.py @@ -0,0 +1,2 @@ +"""Packaging CLI for SpringBoard Core.""" + diff --git a/build/sbc_packager/assets/sbc_after_pack.cjs b/build/sbc_packager/assets/sbc_after_pack.cjs new file mode 100644 index 00000000..c3eb5f57 --- /dev/null +++ b/build/sbc_packager/assets/sbc_after_pack.cjs @@ -0,0 +1,30 @@ +const fs = require("fs"); +const path = require("path"); + +module.exports = async function sbcAfterPack(context) { + if (context.electronPlatformName !== "linux") { + return; + } + + const appOutDir = context.appOutDir; + const executableName = context.packager.executableName; + const launcherPath = path.join(appOutDir, executableName); + const realBinaryPath = path.join(appOutDir, `${executableName}-bin`); + + if (!fs.existsSync(launcherPath)) { + return; + } + + if (!fs.existsSync(realBinaryPath)) { + fs.renameSync(launcherPath, realBinaryPath); + } + + const wrapper = `#!/bin/sh +set -eu +APPDIR="$(CDPATH= cd -- \\"$(dirname -- \\"$0\\")\\" && pwd)" +exec "$APPDIR/${executableName}-bin" --no-sandbox --disable-setuid-sandbox "$@" +`; + + fs.writeFileSync(launcherPath, wrapper, { encoding: "utf-8", mode: 0o755 }); + fs.chmodSync(launcherPath, 0o755); +}; diff --git a/build/sbc_packager/cli.py b/build/sbc_packager/cli.py new file mode 100644 index 00000000..a7e30ce1 --- /dev/null +++ b/build/sbc_packager/cli.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +import typer + +from .cli_download_engine import download_engine_command +from .cli_make_package_json import make_package_json_command +from .cli_package import package_command +from .cli_prepare import prepare_command + +app = typer.Typer(add_completion=False, no_args_is_help=True) + +app.command("prepare")(prepare_command) +app.command("download-engine")(download_engine_command) +app.command("make-package-json")(make_package_json_command) +app.command("package")(package_command) + + +def main() -> None: + app() diff --git a/build/sbc_packager/cli_download_engine.py b/build/sbc_packager/cli_download_engine.py new file mode 100644 index 00000000..05de1750 --- /dev/null +++ b/build/sbc_packager/cli_download_engine.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import os +import shutil +import subprocess +import tempfile +import time +import urllib.error +import urllib.request +from pathlib import Path +from typing import Annotated + +import typer + +from .json_io import read_json_dict +from .models import PackageAssetsMeta + +app = typer.Typer(add_completion=False, no_args_is_help=True) + + +@app.command(name="download-engine") +def download_engine_command( + meta: Annotated[Path, typer.Option(..., "--meta", exists=True, dir_okay=False)], + files_dir: Annotated[Path, typer.Option(..., "--files-dir")], +) -> None: + download_engine(meta_file=meta, files_dir=files_dir) + + +def download_engine(meta_file: Path, files_dir: Path) -> None: + meta = PackageAssetsMeta.model_validate(read_json_dict(meta_file.resolve())) + engine_resource = meta.engineResource + + if engine_resource.url is None or engine_resource.destination is None: + raise RuntimeError(f"Invalid metadata: missing engine resource fields in {meta_file}") + + destination_path = (files_dir.resolve() / engine_resource.destination).resolve() + if directory_has_content(destination_path): + print(f"Skipping engine extraction, destination already exists: {destination_path}") + return + + destination_path.mkdir(parents=True, exist_ok=True) + archive_path = download_with_retries(engine_resource.url) + + try: + print(f"Extracting engine to: {destination_path}") + subprocess.run( + ["7z", "x", "-y", str(archive_path), f"-o{destination_path}"], + check=True, + ) + finally: + archive_path.unlink(missing_ok=True) + + print("Engine extraction finished") + + +def directory_has_content(path: Path) -> bool: + if not path.is_dir(): + return False + return any(path.iterdir()) + + +def download_with_retries(url: str, max_attempts: int = 4) -> Path: + archive_fd, archive_path_str = tempfile.mkstemp(prefix="sbc-engine-", suffix=".7z") + archive_path = Path(archive_path_str) + os.close(archive_fd) + + for attempt in range(1, max_attempts + 1): + try: + print(f"Downloading engine: {url}") + with urllib.request.urlopen(url, timeout=60) as response, archive_path.open("wb") as output_file: + shutil.copyfileobj(response, output_file) + return archive_path + except (urllib.error.URLError, TimeoutError) as error: + if attempt == max_attempts: + raise RuntimeError(f"Failed to download engine after {max_attempts} attempts") from error + print(f"Download attempt {attempt}/{max_attempts} failed: {error}") + time.sleep(attempt) + + raise RuntimeError("Unreachable code: retries exhausted") + + +def main() -> None: + app() diff --git a/build/sbc_packager/cli_make_package_json.py b/build/sbc_packager/cli_make_package_json.py new file mode 100644 index 00000000..5b549d0a --- /dev/null +++ b/build/sbc_packager/cli_make_package_json.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +import shutil +from pathlib import Path +from typing import Annotated, Any + +import typer + +from .json_io import read_json_dict, write_json_dict +from .models import DistConfig + +app = typer.Typer(add_completion=False, no_args_is_help=True) +JsonMap = dict[str, Any] +AFTER_PACK_HOOK_REL_PATH = "build/sbc_after_pack.cjs" +AFTER_PACK_HOOK_TEMPLATE = Path(__file__).resolve().parent / "assets" / "sbc_after_pack.cjs" + + +@app.command(name="make-package-json") +def make_package_json_command( + package_json: Annotated[Path, typer.Argument(...)], + config_json: Annotated[Path, typer.Argument(...)], + repo_full_name: Annotated[str, typer.Argument(...)], + version: Annotated[str, typer.Argument(...)], +) -> None: + make_package_json( + package_json=package_json, + config_json=config_json, + repo_full_name=repo_full_name, + version=version, + ) + + +def make_package_json(package_json: Path, config_json: Path, repo_full_name: str, version: str) -> None: + resolved_package_json = package_json.resolve() + config = DistConfig.model_validate(read_json_dict(config_json.resolve())) + package_template = read_json_dict(resolved_package_json) + + title = config.title + repo_dot_name = repo_full_name.replace("/", ".") + + package_template["name"] = title.replace(" ", "-") + build = ensure_dict(package_template, "build") + build["artifactName"] = f"{title}-${{version}}.${{ext}}" + package_template["version"] = version + package_template["repository"] = f"github:{repo_full_name}" + build["appId"] = f"com.springrts.launcher.{repo_dot_name}" + build["afterPack"] = AFTER_PACK_HOOK_REL_PATH + build.pop("publish", None) + + merge_dependencies(package_template, config.dependencies) + ensure_packaged_files(build) + + write_json_dict(resolved_package_json, package_template, pretty=False) + copy_after_pack_hook(resolved_package_json.parent / AFTER_PACK_HOOK_REL_PATH) + + +def merge_dependencies(package_template: JsonMap, dependencies: dict[str, str] | None) -> None: + package_dependencies = ensure_dict(package_template, "dependencies") + if dependencies is None: + return + for dependency_name, dependency_value in dependencies.items(): + package_dependencies[dependency_name] = dependency_value + + +def ensure_packaged_files(build: JsonMap) -> None: + append_if_missing(ensure_list(build, "extraFiles"), "files/**") + linux = ensure_dict(build, "linux") + append_if_missing(ensure_list(linux, "extraFiles"), "files/**") + win = ensure_dict(build, "win") + append_if_missing(ensure_list(win, "extraFiles"), "files/**") + + +def ensure_dict(parent: JsonMap, key: str) -> JsonMap: + value = parent.get(key) + if isinstance(value, dict): + return value + parent[key] = {} + return parent[key] + + +def ensure_list(parent: JsonMap, key: str) -> list[Any]: + value = parent.get(key) + if isinstance(value, list): + return value + parent[key] = [] + return parent[key] + + +def append_if_missing(values: list[Any], value: str) -> None: + if value not in values: + values.append(value) + + +def copy_after_pack_hook(path: Path) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(AFTER_PACK_HOOK_TEMPLATE, path) + + +def main() -> None: + app() diff --git a/build/sbc_packager/cli_package.py b/build/sbc_packager/cli_package.py new file mode 100644 index 00000000..2b91a471 --- /dev/null +++ b/build/sbc_packager/cli_package.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Annotated + +import typer + +from .cli_download_engine import download_engine +from .cli_make_package_json import make_package_json +from .cli_prepare import prepare_packaged_assets + +app = typer.Typer(add_completion=False, no_args_is_help=True) + + +@app.command(name="package") +def package_command( + repo_root: Annotated[Path, typer.Option(..., "--repo-root", exists=True, file_okay=False)], + config_in: Annotated[Path, typer.Option(..., "--config-in", exists=True, dir_okay=False)], + config_out: Annotated[Path, typer.Option(..., "--config-out")], + files_dir: Annotated[Path, typer.Option(..., "--files-dir")], + meta_out: Annotated[Path, typer.Option(..., "--meta-out")], + package_json: Annotated[Path, typer.Option(..., "--package-json")], + repo_full_name: Annotated[str, typer.Option(..., "--repo-full-name")], + platform: Annotated[str, typer.Option(..., "--platform")], + git_hash: Annotated[str, typer.Option(..., "--git-hash")], + package_version: Annotated[str, typer.Option(..., "--package-version")], + linux_setup_id: Annotated[str, typer.Option("--linux-setup-id")] = "latest-linux", + windows_setup_id: Annotated[str, typer.Option("--windows-setup-id")] = "latest-win", +) -> None: + run_packaging_pipeline( + repo_root=repo_root, + config_in=config_in, + config_out=config_out, + files_dir=files_dir, + meta_out=meta_out, + package_json=package_json, + repo_full_name=repo_full_name, + platform=platform, + git_hash=git_hash, + package_version=package_version, + linux_setup_id=linux_setup_id, + windows_setup_id=windows_setup_id, + ) + print(f"Pipeline finished with git hash: {git_hash}") + print(f"Pipeline finished with package version: {package_version}") + + +def run_packaging_pipeline( + repo_root: Path, + config_in: Path, + config_out: Path, + files_dir: Path, + meta_out: Path, + package_json: Path, + repo_full_name: str, + platform: str, + git_hash: str, + package_version: str, + linux_setup_id: str, + windows_setup_id: str, +) -> None: + resolved_repo_root = repo_root.resolve() + resolved_config_in = config_in.resolve() + resolved_config_out = config_out.resolve() + resolved_files_dir = files_dir.resolve() + resolved_meta_out = meta_out.resolve() + resolved_package_json = package_json.resolve() + + if not resolved_package_json.is_file(): + raise RuntimeError( + "Missing launcher package.json at " + f"{resolved_package_json}. " + "Initialize a launcher build tree first (copy spring-launcher files into the build directory), " + "then rerun sbc-packager-package." + ) + + prepare_packaged_assets( + repo_root=resolved_repo_root, + config_in=resolved_config_in, + config_out=resolved_config_out, + files_dir=resolved_files_dir, + meta_out=resolved_meta_out, + git_hash=git_hash, + platform=platform, + linux_setup_id=linux_setup_id, + windows_setup_id=windows_setup_id, + ) + + download_engine(meta_file=resolved_meta_out, files_dir=resolved_files_dir) + + make_package_json( + package_json=resolved_package_json, + config_json=resolved_config_out, + repo_full_name=repo_full_name, + version=package_version, + ) + + +def main() -> None: + app() diff --git a/build/sbc_packager/cli_prepare.py b/build/sbc_packager/cli_prepare.py new file mode 100644 index 00000000..a3ad4086 --- /dev/null +++ b/build/sbc_packager/cli_prepare.py @@ -0,0 +1,181 @@ +from __future__ import annotations + +import re +import shutil +from pathlib import Path +from typing import Annotated, Literal + +import typer + +from .copying import collect_generated_top_level_excludes, copy_repo_filtered +from .json_io import read_json_dict, write_json_dict +from .models import DistConfig, EngineResource, PackageAssetsMeta, SetupConfig +from .rapid_alias import write_rapid_alias_metadata + +app = typer.Typer(add_completion=False, no_args_is_help=True) + + +@app.command(name="prepare") +def prepare_command( + repo_root: Annotated[Path, typer.Option(..., "--repo-root", exists=True, file_okay=False)], + config_in: Annotated[Path, typer.Option(..., "--config-in", exists=True, dir_okay=False)], + config_out: Annotated[Path, typer.Option(..., "--config-out")], + files_dir: Annotated[Path, typer.Option(..., "--files-dir")], + meta_out: Annotated[Path, typer.Option(..., "--meta-out")], + git_hash: Annotated[str, typer.Option(..., "--git-hash")], + platform: Annotated[str, typer.Option(..., "--platform")], + linux_setup_id: Annotated[str, typer.Option("--linux-setup-id")] = "latest-linux", + windows_setup_id: Annotated[str, typer.Option("--windows-setup-id")] = "latest-win", +) -> None: + prepare_packaged_assets( + repo_root=repo_root, + config_in=config_in, + config_out=config_out, + files_dir=files_dir, + meta_out=meta_out, + git_hash=git_hash, + platform=platform, + linux_setup_id=linux_setup_id, + windows_setup_id=windows_setup_id, + ) + + +def prepare_packaged_assets( + repo_root: Path, + config_in: Path, + config_out: Path, + files_dir: Path, + meta_out: Path, + git_hash: str, + platform: str, + linux_setup_id: str, + windows_setup_id: str, +) -> None: + resolved_repo_root = repo_root.resolve() + resolved_config_in = config_in.resolve() + resolved_config_out = config_out.resolve() + resolved_files_dir = files_dir.resolve() + resolved_meta_out = meta_out.resolve() + + normalized_platform = normalize_platform(platform) + game_name = resolve_game_name_from_modinfo(resolved_repo_root, git_hash) + + config = DistConfig.model_validate(read_json_dict(resolved_config_in)) + selected_setup_id = linux_setup_id if normalized_platform == "linux" else windows_setup_id + selected_engine = select_engine_resource(config, selected_setup_id, normalized_platform) + + copy_config_without_modification(resolved_config_in, resolved_config_out) + + extra_top_level_excludes = collect_generated_top_level_excludes( + repo_root=resolved_repo_root, + generated_paths=[resolved_files_dir, resolved_config_out, resolved_meta_out], + ) + game_dir = prepare_game_directory( + repo_root=resolved_repo_root, + files_dir=resolved_files_dir, + game_name=game_name, + git_hash=git_hash, + extra_top_level_excludes=extra_top_level_excludes, + ) + + meta = PackageAssetsMeta( + gitHash=git_hash, + gameName=game_name, + gameDirectory=game_dir.relative_to(resolved_files_dir).as_posix(), + engineResource=selected_engine, + platform=normalized_platform, + ) + write_json_dict(resolved_meta_out, meta.model_dump(mode="json"), pretty=True) + + write_rapid_alias_metadata(files_dir=resolved_files_dir, git_hash=git_hash, game_name=game_name) + + print(f"Prepared packaged assets for {normalized_platform}") + print(f"Game: {game_name}") + print(f"Engine destination: {selected_engine.destination}") + print("Copied config.json without modification") + print("Generated rapid alias metadata") + + +def normalize_platform(platform: str) -> Literal["linux", "win32"]: + if platform not in {"linux", "win32"}: + raise RuntimeError(f"Unsupported platform: {platform}") + return "linux" if platform == "linux" else "win32" + + +def select_engine_resource(config: DistConfig, setup_id: str, platform: str) -> EngineResource: + setup = get_setup_by_id(config, setup_id) + if setup is None: + raise RuntimeError(f"Setup '{setup_id}' not found in config.json for platform: {platform}") + + resource = get_primary_engine_resource(setup) + if resource is None: + raise RuntimeError(f"Setup '{setup_id}' has no usable engine resource entry for platform: {platform}") + return resource + + +def get_primary_engine_resource(setup: SetupConfig) -> EngineResource | None: + if not setup.downloads.resources: + return None + resource = setup.downloads.resources[0] + if resource.url is None or resource.destination is None: + return None + return resource + + +def get_setup_by_id(config: DistConfig, setup_id: str) -> SetupConfig | None: + for setup in config.setups: + if setup.package.id == setup_id: + return setup + return None + + +def copy_config_without_modification(config_in: Path, config_out: Path) -> None: + config_out.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(config_in, config_out) + + +def resolve_game_name_from_modinfo(repo_root: Path, git_hash: str) -> str: + modinfo_path = repo_root / "modinfo.lua" + modinfo_text = modinfo_path.read_text(encoding="utf-8") + + name = parse_modinfo_string_field(modinfo_text, "name") + version = parse_modinfo_string_field(modinfo_text, "version") + if name is None: + raise RuntimeError(f"Expected string field 'name' in {modinfo_path}") + if version is None: + raise RuntimeError(f"Expected string field 'version' in {modinfo_path}") + + resolved_version = version.replace("$VERSION", git_hash) + return f"{name} {resolved_version}".strip() + + +def parse_modinfo_string_field(modinfo_text: str, field_name: str) -> str | None: + pattern = rf"^\s*{re.escape(field_name)}\s*=\s*[\"']([^\"']+)[\"']" + match = re.search(pattern, modinfo_text, flags=re.MULTILINE) + if match is None: + return None + return match.group(1) + + +def prepare_game_directory( + repo_root: Path, + files_dir: Path, + game_name: str, + git_hash: str, + extra_top_level_excludes: set[str], +) -> Path: + game_dir = files_dir / "games" / f"{game_name}.sdd" + shutil.rmtree(game_dir, ignore_errors=True) + game_dir.mkdir(parents=True, exist_ok=True) + copy_repo_filtered(repo_root, game_dir, extra_top_level_excludes) + + modinfo_path = game_dir / "modinfo.lua" + modinfo_text = modinfo_path.read_text(encoding="utf-8") + if "$VERSION" not in modinfo_text: + raise RuntimeError(f"Expected $VERSION placeholder in {modinfo_path}") + modinfo_path.write_text(modinfo_text.replace("$VERSION", git_hash), encoding="utf-8") + return game_dir + + +def main() -> None: + app() diff --git a/build/sbc_packager/copying.py b/build/sbc_packager/copying.py new file mode 100644 index 00000000..c486b102 --- /dev/null +++ b/build/sbc_packager/copying.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +import shutil +from pathlib import Path + +EXCLUDED_TOP_LEVEL: set[str] = { + ".git", + ".github", + ".vscode", + "build", + "dist_cfg", + "doc", + "issues", +} + +EXCLUDED_PATHS: set[str] = { + ".gitignore", + ".gitmodules", +} + + +def collect_generated_top_level_excludes(repo_root: Path, generated_paths: list[Path]) -> set[str]: + excludes: set[str] = set() + resolved_repo_root = repo_root.resolve() + + for generated_path in generated_paths: + resolved_generated = generated_path.resolve() + try: + relative = resolved_generated.relative_to(resolved_repo_root) + except ValueError: + continue + if relative.parts: + excludes.add(relative.parts[0]) + + return excludes + + +def copy_repo_filtered(source_root: Path, destination_root: Path, extra_top_level_excludes: set[str]) -> None: + for entry in source_root.iterdir(): + relative_path = entry.relative_to(source_root).as_posix() + if should_exclude(relative_path, extra_top_level_excludes): + continue + + destination_entry = destination_root / entry.name + if entry.is_dir(): + destination_entry.mkdir(parents=True, exist_ok=True) + copy_tree_filtered(source_root, entry, destination_entry, extra_top_level_excludes) + continue + + if entry.is_file(): + destination_entry.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(entry, destination_entry) + + +def copy_tree_filtered( + source_root: Path, + source_dir: Path, + destination_dir: Path, + extra_top_level_excludes: set[str], +) -> None: + for entry in source_dir.iterdir(): + relative_path = entry.relative_to(source_root).as_posix() + if should_exclude(relative_path, extra_top_level_excludes): + continue + + destination_entry = destination_dir / entry.name + if entry.is_dir(): + destination_entry.mkdir(parents=True, exist_ok=True) + copy_tree_filtered(source_root, entry, destination_entry, extra_top_level_excludes) + continue + + if entry.is_file(): + destination_entry.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(entry, destination_entry) + + +def should_exclude(relative_path: str, extra_top_level_excludes: set[str]) -> bool: + top_level = relative_path.split("/", 1)[0] + if top_level.startswith(".local-packaged-"): + return True + if top_level in EXCLUDED_TOP_LEVEL: + return True + if top_level in extra_top_level_excludes: + return True + return relative_path in EXCLUDED_PATHS diff --git a/build/sbc_packager/json_io.py b/build/sbc_packager/json_io.py new file mode 100644 index 00000000..1ed82cea --- /dev/null +++ b/build/sbc_packager/json_io.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + + +def read_json_dict(path: Path) -> dict[str, Any]: + with path.open("r", encoding="utf-8") as input_file: + data = json.load(input_file) + if not isinstance(data, dict): + raise RuntimeError(f"JSON root must be an object: {path}") + return data + + +def write_json_dict(path: Path, data: dict[str, Any], pretty: bool) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w", encoding="utf-8") as output_file: + if pretty: + json.dump(data, output_file, indent=2) + output_file.write("\n") + return + json.dump(data, output_file, separators=(",", ":")) diff --git a/build/sbc_packager/models.py b/build/sbc_packager/models.py new file mode 100644 index 00000000..f6235ba8 --- /dev/null +++ b/build/sbc_packager/models.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from typing import Any, Literal + +from pydantic import BaseModel, ConfigDict, Field + + +class EngineResource(BaseModel): + url: str | None = None + destination: str | None = None + extract: bool | None = None + + model_config = ConfigDict(extra="allow") + + +class DownloadsConfig(BaseModel): + games: list[str] | None = None + maps: list[str] | None = None + engines: list[str] | None = None + resources: list[EngineResource] | None = None + nextgen: list[Any] | None = None + + model_config = ConfigDict(extra="allow") + + +class LaunchConfig(BaseModel): + game: str | None = None + engine: str | None = None + + model_config = ConfigDict(extra="allow") + + +class PackageConfig(BaseModel): + id: str | None = None + platform: str | None = None + + model_config = ConfigDict(extra="allow") + + +class SetupConfig(BaseModel): + package: PackageConfig = Field(default_factory=PackageConfig) + downloads: DownloadsConfig = Field(default_factory=DownloadsConfig) + launch: LaunchConfig = Field(default_factory=LaunchConfig) + no_downloads: bool | None = None + + model_config = ConfigDict(extra="allow") + + +class DistConfig(BaseModel): + title: str + dependencies: dict[str, str] | None = None + setups: list[SetupConfig] = Field(default_factory=list) + + model_config = ConfigDict(extra="allow") + + +class PackageAssetsMeta(BaseModel): + gitHash: str + gameName: str + gameDirectory: str + engineResource: EngineResource + platform: Literal["linux", "win32"] diff --git a/build/sbc_packager/rapid_alias.py b/build/sbc_packager/rapid_alias.py new file mode 100644 index 00000000..19a0d384 --- /dev/null +++ b/build/sbc_packager/rapid_alias.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +import gzip +import hashlib +from pathlib import Path + + +def write_rapid_alias_metadata(files_dir: Path, git_hash: str, game_name: str) -> None: + rapid_root = files_dir / "rapid" / "repos.springrts.com" + sbc_root = rapid_root / "sbc" + package_hash = hashlib.md5(f"{game_name}|{git_hash}".encode()).hexdigest() + + write_gzip_text(rapid_root / "repos.gz", "sbc,https://repos.springrts.com/sbc,,\n") + write_gzip_text( + sbc_root / "versions.gz", + ( + f"sbc:git:{git_hash},{package_hash},,{game_name}\n" + f"sbc:test,{package_hash},,{game_name}\n" + ), + ) + + +def write_gzip_text(path: Path, text: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with gzip.open(path, "wb") as compressed_file: + compressed_file.write(text.encode("utf-8")) diff --git a/build/uv.lock b/build/uv.lock new file mode 100644 index 00000000..9ecc0a0c --- /dev/null +++ b/build/uv.lock @@ -0,0 +1,244 @@ +version = 1 +revision = 3 +requires-python = "==3.13.*" + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyright" +version = "1.1.408" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578, upload-time = "2026-01-08T08:07:38.795Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144, upload-time = "2026-01-08T08:07:37.082Z" }, +] + +[[package]] +name = "rich" +version = "14.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/99/a4cab2acbb884f80e558b0771e97e21e939c5dfb460f488d19df485e8298/rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8", size = 230143, upload-time = "2026-02-01T16:20:47.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" }, + { url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" }, + { url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" }, + { url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" }, + { url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" }, + { url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" }, + { url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" }, + { url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" }, + { url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" }, + { url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" }, + { url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" }, +] + +[[package]] +name = "sbc-packager" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "pydantic" }, + { name = "typer" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pyright" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "pydantic", specifier = ">=2.12.5" }, + { name = "typer", specifier = ">=0.23.1" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pyright", specifier = ">=1.1.408" }, + { name = "ruff", specifier = ">=0.15.1" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "typer" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/b6/3e681d3b6bb22647509bdbfdd18055d5adc0dce5c5585359fa46ff805fdc/typer-0.24.0.tar.gz", hash = "sha256:f9373dc4eff901350694f519f783c29b6d7a110fc0dcc11b1d7e353b85ca6504", size = 118380, upload-time = "2026-02-16T22:08:48.496Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/d0/4da85c2a45054bb661993c93524138ace4956cb075a7ae0c9d1deadc331b/typer-0.24.0-py3-none-any.whl", hash = "sha256:5fc435a9c8356f6160ed6e85a6301fdd6e3d8b2851da502050d1f92c5e9eddc8", size = 56441, upload-time = "2026-02-16T22:08:47.535Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] diff --git a/dist_cfg/config.json b/dist_cfg/config.json index c00af0cc..fbf111ad 100644 --- a/dist_cfg/config.json +++ b/dist_cfg/config.json @@ -1,9 +1,9 @@ { - "title" : "SpringBoard", - "dependencies" : { + "title": "SpringBoard", + "dependencies": { "pngjs3": "^5.1.3" }, - "setups" : [ + "setups": [ { "package": { "id": "latest-win", @@ -11,19 +11,18 @@ "platform": "win32" }, "downloads": { - "games": ["sbc:test"], "resources": [ { - "url": "https://github.com/Spring-SpringBoard/SpringBoard-Resources/releases/download/resources/spring_.maintenance.104.0.1-1553-gd3c0012_win32-minimal-portable.7z", - "destination": "engine/104.0.1-1553-gd3c0012 maintenance", - "extract": true + "url": "https://github.com/beyond-all-reason/spring/releases/download/spring_bar_%7BBAR105%7D105.1.1-2472-ga5aa45c/spring_bar_.BAR105.105.1.1-2472-ga5aa45c_windows-64-minimal-portable.7z", + "destination": "engine/105.1.1-2472-ga5aa45c-bar", + "extract": true } ] }, "launch": { "game": "rapid://sbc:test", "map": "sb_initial_blank_10x8", - "engine": "104.0.1-1553-gd3c0012 maintenance", + "engine": "105.1.1-2472-ga5aa45c-bar", "map_options": { "new_map_x": 10, "new_map_y": 8 @@ -33,7 +32,6 @@ } } }, - { "package": { "id": "latest-linux", @@ -41,42 +39,11 @@ "platform": "linux" }, "downloads": { - "games": ["sbc:test"], "resources": [ { - "url": "https://github.com/Spring-SpringBoard/SpringBoard-Resources/releases/download/resources/spring_.maintenance.104.0.1-1553-gd3c0012_minimal-portable-linux64-static.7z", - "destination": "engine/104.0.1-1553-gd3c0012 maintenance", - "extract": true - } - ] - }, - "launch": { - "game": "rapid://sbc:test", - "map": "sb_initial_blank_10x8", - "engine": "104.0.1-1553-gd3c0012 maintenance", - "map_options": { - "new_map_x": 10, - "new_map_y": 8 - }, - "game_options": { - "MapSeed": 42 - } - } - }, - - { - "package": { - "id": "latest-win-bar", - "display": "Latest (BAR Engine)", - "platform": "win32" - }, - "downloads": { - "games": ["sbc:test"], - "resources": [ - { - "url": "https://github.com/beyond-all-reason/spring/releases/download/spring_bar_%7BBAR105%7D105.1.1-2472-ga5aa45c/spring_bar_.BAR105.105.1.1-2472-ga5aa45c_windows-64-minimal-portable.7z", - "destination": "engine/105.1.1-2472-ga5aa45c-bar", - "extract": true + "url": "https://github.com/beyond-all-reason/spring/releases/download/spring_bar_%7BBAR105%7D105.1.1-2472-ga5aa45c/spring_bar_.BAR105.105.1.1-2472-ga5aa45c_linux-64-minimal-portable.7z", + "destination": "engine/105.1.1-2472-ga5aa45c-bar", + "extract": true } ] }, @@ -93,87 +60,18 @@ } } }, - - { - "package": { - "id": "latest-linux-bar", - "display": "Latest (BAR Engine)", - "platform": "linux" - }, - "downloads": { - "games": ["sbc:test"], - "resources": [ - { - "url": "https://github.com/beyond-all-reason/spring/releases/download/spring_bar_%7BBAR105%7D105.1.1-2472-ga5aa45c/spring_bar_.BAR105.105.1.1-2472-ga5aa45c_linux-64-minimal-portable.7z", - "destination": "engine/105.1.1-2472-ga5aa45c-bar", - "extract": true - } - ] - }, - "launch": { - "game": "rapid://sbc:test", - "map": "sb_initial_blank_10x8", - "engine": "105.1.1-2472-ga5aa45c-bar", - "map_options": { - "new_map_x": 10, - "new_map_y": 8 - }, - "game_options": { - "MapSeed": 42 - } - } - }, - - { - "package": { - "id": "stable", - "display": "Stable" - }, - "downloads": { - "games" : ["sbc:stable"], - "maps" : ["TitanDuel 2.2"], - "engines" : [ "104.0.1-1553-gd3c0012 maintenance" ] - }, - "launch": { - "game": "rapid://sbc:stable", - "map": "TitanDuel 2.2" - } - }, - { "package": { "id": "dev", "display": "Development" }, "downloads": { - "engines" : [ "104.0.1-1553-gd3c0012 maintenance" ] - }, - "auto_start" : false, - "no_downloads" : true, - "load_dev_exts": false, - "launch": { - "game": "SpringBoard Core $VERSION", - "map": "sb_initial_blank_10x8", - "map_options": { - "new_map_x": 10, - "new_map_y": 8 - }, - "game_options": { - "MapSeed": 42 - } - } - }, - - { - "package": { - "id": "dev-bar-engine", - "display": "Development (BAR Engine)" - }, - "downloads": { - "engines" : [ "105.1.1-2472-ga5aa45c-bar" ] + "engines": [ + "105.1.1-2472-ga5aa45c-bar" + ] }, - "auto_start" : false, - "no_downloads" : true, + "auto_start": false, + "no_downloads": true, "load_dev_exts": false, "launch": { "game": "SpringBoard Core $VERSION", @@ -187,7 +85,6 @@ } } }, - { "package": { "id": "asset-download", @@ -204,6 +101,4 @@ } } ] - - -} +} \ No newline at end of file diff --git a/dist_cfg/spring_platform.js b/dist_cfg/spring_platform.js new file mode 100644 index 00000000..7b8e1a8a --- /dev/null +++ b/dist_cfg/spring_platform.js @@ -0,0 +1,106 @@ +'use strict'; + +const log = require('electron-log'); +const path = require('path'); +const fs = require('fs'); +const { app } = require('electron'); +const { existsSync, mkdirSync } = fs; +const assert = require('assert'); + +const platformName = process.platform; + +if (platformName === 'linux') { + // AppImage mounts do not preserve setuid sandbox requirements for chrome-sandbox. + // Force Chromium to use the non-setuid sandbox mode. + app.commandLine.appendSwitch('no-sandbox'); + app.commandLine.appendSwitch('disable-setuid-sandbox'); +} + +const { config } = require('./launcher_config'); +const { resolveWritePath } = require('./write_path'); + +var FILES_DIR = 'files'; +FILES_DIR = path.resolve(`${__dirname}/../files`); +if (!existsSync(FILES_DIR)) { + FILES_DIR = path.resolve(`${process.resourcesPath}/../files`); +} + +function copyBundledPath(srcPath, dstPath) { + const stat = fs.lstatSync(srcPath); + if (stat.isDirectory()) { + fs.cpSync(srcPath, dstPath, { recursive: true, force: true }); + return; + } + if (stat.isSymbolicLink()) { + const resolved = fs.realpathSync(srcPath); + const resolvedStat = fs.lstatSync(resolved); + if (resolvedStat.isDirectory()) { + fs.cpSync(resolved, dstPath, { recursive: true, force: true }); + } else { + fs.copyFileSync(resolved, dstPath); + } + return; + } + fs.copyFileSync(srcPath, dstPath); +} + +// The following order is necessary: +// 1. Set write dir +// 2. Set logfile based on the writedir +// 3. Start logging + +assert(config.title != undefined); +const writePath = resolveWritePath(config.title); + +assert(writePath != undefined); +if (!existsSync(writePath)) { + try { + mkdirSync(writePath); + } catch (err) { + log.error(`Cannot create writePath at: ${writePath}`); + log.error(err); + } +} + +if (existsSync(FILES_DIR) && existsSync(writePath)) { + fs.readdirSync(FILES_DIR).forEach(function (entry) { + const srcPath = path.join(FILES_DIR, entry); + const dstPath = path.join(writePath, entry); + try { + copyBundledPath(srcPath, dstPath); + } catch (err) { + log.error(`Failed to copy bundled path from ${srcPath} to ${dstPath}`); + log.error(err); + } + }); +} + +let prDownloaderBin; +let butlerBin; +if (platformName === 'win32') { + prDownloaderBin = 'pr-downloader.exe'; + butlerBin = 'butler/windows/butler.exe'; + exports.springBin = 'spring.exe'; +} else if (platformName === 'linux') { + prDownloaderBin = 'pr-downloader'; + butlerBin = 'butler/linux/butler'; + exports.springBin = 'spring'; + // } else if (platformName === 'darwin') { + // prDownloaderBin = 'pr-downloader-mac'; + // butlerBin = 'butler'; // TODO: Support Mac? + // exports.springBin = 'Contents/MacOS/spring'; +} else { + log.error(`Unsupported platform: ${platformName}`); + process.exit(-1); +} + +exports.prDownloaderPath = path.resolve(`${__dirname}/../bin/${prDownloaderBin}`); +if (!existsSync(exports.prDownloaderPath)) { + exports.prDownloaderPath = path.resolve(`${process.resourcesPath}/../bin/${prDownloaderBin}`); +} +exports.butlerPath = path.resolve(`${__dirname}/../bin/${butlerBin}`); +if (!existsSync(exports.butlerPath)) { + exports.butlerPath = path.resolve(`${process.resourcesPath}/../bin/${butlerBin}`); +} + +exports.writePath = writePath;