diff --git a/.github/workflows/android-flutter-build.yml b/.github/workflows/android-flutter-build.yml new file mode 100644 index 00000000..506cd33e --- /dev/null +++ b/.github/workflows/android-flutter-build.yml @@ -0,0 +1,129 @@ +name: Android Flutter Build + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + build-android: + runs-on: windows-2025 + timeout-minutes: 90 + env: + RUSTFLAGS: -Awarnings + RUST_TOOLCHAIN_VERSION: 1.95.0 + FVM_VERSION: 4.1.2 + TYPESCRIPT_VERSION: 5.9.3 + WASM_BINDGEN_VERSION: 0.2.122 + WASI_SDK_VERSION: "20.0" + QUICKJS_WASM_SYS_WASI_SDK_MAJOR_VERSION: 20 + QUICKJS_WASM_SYS_WASI_SDK_MINOR_VERSION: 0 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install bootstrap Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + cache: true + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + + - name: Install FVM + shell: pwsh + run: | + dart pub global activate fvm $env:FVM_VERSION + "$env:APPDATA\Pub\Cache\bin" | Out-File -FilePath $env:GITHUB_PATH -Append -Encoding utf8 + + - name: Install project Flutter SDK + working-directory: apps/flutter/app + shell: pwsh + run: fvm install --skip-pub-get + + - name: Install Rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_TOOLCHAIN_VERSION }} + targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android,wasm32-unknown-unknown + + - name: Prepare Android bindgen libclang + shell: pwsh + run: | + $version = "21.1.8" + $packageName = "libclang.runtime.win-x64.$version" + $toolsDir = "target/operit-build-tools" + $packageDir = Join-Path $toolsDir $packageName + $archive = Join-Path $toolsDir "$packageName.nupkg" + New-Item -ItemType Directory -Force -Path $toolsDir | Out-Null + Invoke-WebRequest -Uri "https://www.nuget.org/api/v2/package/libclang.runtime.win-x64/$version" -OutFile $archive + Expand-Archive -Path $archive -DestinationPath $packageDir -Force + $libclang = Join-Path $packageDir "runtimes/win-x64/native/libclang.dll" + if (-not (Test-Path -LiteralPath $libclang)) { + throw "Android bindgen libclang was not prepared at $libclang" + } + + - name: Restore Android signing material + shell: pwsh + env: + ANDROID_RELEASE_KEYSTORE_BASE64: ${{ secrets.ANDROID_RELEASE_KEYSTORE_BASE64 }} + ANDROID_RELEASE_STORE_PASSWORD: ${{ secrets.ANDROID_RELEASE_STORE_PASSWORD }} + ANDROID_RELEASE_KEY_ALIAS: ${{ secrets.ANDROID_RELEASE_KEY_ALIAS }} + ANDROID_RELEASE_KEY_PASSWORD: ${{ secrets.ANDROID_RELEASE_KEY_PASSWORD }} + run: | + $required = @( + "ANDROID_RELEASE_KEYSTORE_BASE64", + "ANDROID_RELEASE_STORE_PASSWORD", + "ANDROID_RELEASE_KEY_ALIAS", + "ANDROID_RELEASE_KEY_PASSWORD" + ) + $missing = $required | Where-Object { [string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($_)) } + if ($missing.Count -gt 0) { + throw "Missing required GitHub Actions secrets: $($missing -join ', ')" + } + $secretsDir = "tools/release/secrets" + New-Item -ItemType Directory -Force -Path $secretsDir | Out-Null + $keystore = Join-Path $secretsDir "operit2-release.keystore" + [System.IO.File]::WriteAllBytes($keystore, [System.Convert]::FromBase64String($env:ANDROID_RELEASE_KEYSTORE_BASE64)) + @" + RELEASE_STORE_FILE=operit2-release.keystore + RELEASE_STORE_PASSWORD=$env:ANDROID_RELEASE_STORE_PASSWORD + RELEASE_KEY_ALIAS=$env:ANDROID_RELEASE_KEY_ALIAS + RELEASE_KEY_PASSWORD=$env:ANDROID_RELEASE_KEY_PASSWORD + "@ | Set-Content -Path (Join-Path $secretsDir "android-signing.properties") -Encoding utf8 + + - name: Check build environment + shell: pwsh + run: python tools/build_scripts/check_environment.py --products app --github-runner + + - name: Build Web Access assets + shell: pwsh + run: python tools/build_scripts/build_flutter_web_access.py + + - name: Build and package Android app + shell: pwsh + run: | + $version = (Select-String -Path apps/flutter/app/pubspec.yaml -Pattern '^version:\s*(\d+\.\d+\.\d+)\+(\d+)\s*$').Matches + if ($version.Count -ne 1) { + throw "apps/flutter/app/pubspec.yaml must contain exactly one release version line" + } + python tools/build_scripts/build_flutter_android.py --build-name $version[0].Groups[1].Value --build-number $version[0].Groups[2].Value --enforce-lockfile + + - name: Upload Android artifacts + uses: actions/upload-artifact@v4 + with: + name: operit2-android-${{ github.sha }} + path: tools/release/dist/operit2-app-android-*.apk + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/linux-release-build.yml b/.github/workflows/linux-release-build.yml new file mode 100644 index 00000000..514a653d --- /dev/null +++ b/.github/workflows/linux-release-build.yml @@ -0,0 +1,120 @@ +name: Linux Release Build + +on: + workflow_dispatch: + inputs: + products: + description: "Release products to build" + required: false + default: "all" + type: choice + options: + - "app" + - "cli" + - "all" + cli_arches: + description: "Linux CLI architectures" + required: false + default: "all" + type: choice + options: + - "host" + - "all" + - "x86_64" + - "aarch64" + +permissions: + contents: read + +jobs: + build-linux: + runs-on: ubuntu-24.04 + timeout-minutes: 90 + env: + RUSTFLAGS: -Awarnings + RUST_TOOLCHAIN_VERSION: 1.95.0 + FVM_VERSION: 4.1.2 + TYPESCRIPT_VERSION: 5.9.3 + WASM_BINDGEN_VERSION: 0.2.122 + WASI_SDK_VERSION: "20.0" + QUICKJS_WASM_SYS_WASI_SDK_MAJOR_VERSION: 20 + QUICKJS_WASM_SYS_WASI_SDK_MINOR_VERSION: 0 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Linux build dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + clang \ + cmake \ + gcc-aarch64-linux-gnu \ + libgstreamer-plugins-base1.0-dev \ + libgstreamer1.0-dev \ + libgtk-3-dev \ + liblzma-dev \ + libwebkit2gtk-4.1-dev \ + libc6-dev-arm64-cross \ + musl-tools \ + ninja-build \ + pkg-config + + - name: Install bootstrap Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + cache: true + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install FVM + run: | + dart pub global activate fvm "$FVM_VERSION" + echo "$HOME/.pub-cache/bin" >> "$GITHUB_PATH" + + - name: Install project Flutter SDK + working-directory: apps/flutter/app + run: fvm install --skip-pub-get + + - name: Install Rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_TOOLCHAIN_VERSION }} + targets: x86_64-unknown-linux-musl,aarch64-unknown-linux-gnu,wasm32-unknown-unknown + + - name: Check build environment + run: python3 tools/build_scripts/check_environment.py --products "${{ inputs.products }}" --cli-arches "${{ inputs.cli_arches }}" --github-runner + + - name: Build Web Access assets + run: python3 tools/build_scripts/build_flutter_web_access.py + + - name: Build and package Linux app + if: inputs.products != 'cli' + run: python3 tools/build_scripts/build_flutter_linux.py --archive-path tools/release/dist/operit2-app-linux-x86_64.tar.gz --enforce-lockfile + + - name: Build and package Linux CLI + if: inputs.products != 'app' + run: python3 tools/build_scripts/build_cli_linux.py --arches "${{ inputs.cli_arches }}" + + - name: Upload Linux app artifact + if: inputs.products != 'cli' + uses: actions/upload-artifact@v4 + with: + name: operit2-linux-app-${{ github.sha }} + path: tools/release/dist/operit2-app-linux-x86_64.tar.gz + if-no-files-found: error + retention-days: 14 + + - name: Upload Linux CLI artifacts + if: inputs.products != 'app' + uses: actions/upload-artifact@v4 + with: + name: operit2-linux-cli-${{ github.sha }} + path: tools/release/dist/operit2-cli-linux-*.tar.gz + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/windows-release-build.yml b/.github/workflows/windows-release-build.yml new file mode 100644 index 00000000..56085a50 --- /dev/null +++ b/.github/workflows/windows-release-build.yml @@ -0,0 +1,113 @@ +name: Windows Release Build + +on: + workflow_dispatch: + inputs: + products: + description: "Release products to build" + required: false + default: "all" + type: choice + options: + - "app" + - "cli" + - "all" + cli_arches: + description: "Windows CLI architectures" + required: false + default: "all" + type: choice + options: + - "host" + - "all" + - "x86_64" + - "aarch64" + +permissions: + contents: read + +jobs: + build-windows: + runs-on: windows-2025 + timeout-minutes: 90 + env: + RUSTFLAGS: -Awarnings + RUST_TOOLCHAIN_VERSION: 1.95.0 + FVM_VERSION: 4.1.2 + TYPESCRIPT_VERSION: 5.9.3 + WASM_BINDGEN_VERSION: 0.2.122 + WASI_SDK_VERSION: "20.0" + QUICKJS_WASM_SYS_WASI_SDK_MAJOR_VERSION: 20 + QUICKJS_WASM_SYS_WASI_SDK_MINOR_VERSION: 0 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install bootstrap Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + cache: true + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install FVM + shell: pwsh + run: | + dart pub global activate fvm $env:FVM_VERSION + "$env:APPDATA\Pub\Cache\bin" | Out-File -FilePath $env:GITHUB_PATH -Append -Encoding utf8 + + - name: Install project Flutter SDK + working-directory: apps/flutter/app + shell: pwsh + run: fvm install --skip-pub-get + + - name: Install Rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_TOOLCHAIN_VERSION }} + targets: x86_64-pc-windows-msvc,aarch64-pc-windows-msvc,wasm32-unknown-unknown + + - name: Install LLVM + shell: pwsh + run: choco install llvm --no-progress --limit-output --yes + + - name: Check build environment + shell: pwsh + run: python tools/build_scripts/check_environment.py --products "${{ inputs.products }}" --cli-arches "${{ inputs.cli_arches }}" --github-runner + + - name: Build Web Access assets + shell: pwsh + run: python tools/build_scripts/build_flutter_web_access.py + + - name: Build and package Windows app + if: inputs.products != 'cli' + shell: pwsh + run: python tools/build_scripts/build_flutter_windows.py --archive-path tools/release/dist/operit2-app-windows-x86_64.zip --enforce-lockfile + + - name: Build and package Windows CLI + if: inputs.products != 'app' + shell: pwsh + run: python tools/build_scripts/build_cli_windows.py --arches "${{ inputs.cli_arches }}" + + - name: Upload Windows app artifact + if: inputs.products != 'cli' + uses: actions/upload-artifact@v4 + with: + name: operit2-windows-app-${{ github.sha }} + path: tools/release/dist/operit2-app-windows-x86_64.zip + if-no-files-found: error + retention-days: 14 + + - name: Upload Windows CLI artifacts + if: inputs.products != 'app' + uses: actions/upload-artifact@v4 + with: + name: operit2-windows-cli-${{ github.sha }} + path: tools/release/dist/operit2-cli-windows-*.zip + if-no-files-found: error + retention-days: 14 diff --git a/BUILDING.md b/BUILDING.md index 2699e3f2..ea26735b 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -33,6 +33,15 @@ MSVC x64 and ARM64 components LLVM clang at C:\Program Files\LLVM\bin\clang.exe ``` +Linux desktop App builds: + +```text +CMake, Ninja, pkg-config, clang, and GTK development headers +GStreamer development headers exposing gstreamer-1.0, gstreamer-app-1.0, and +gstreamer-audio-1.0 through pkg-config +WebKitGTK 4.1 development headers exposing pkg-config package webkit2gtk-4.1 +``` + Flutter app builds: ```text @@ -135,6 +144,14 @@ Build only: .\.venv\Scripts\python.exe tools\release\build_release.py --scope cli ``` +`tools/release/dist` is the shared staging directory for local, SSH-collected, +and GitHub Actions assets. `build_release.py` keeps existing staged files so +platform builds can be composed. Start a new staged set explicitly with: + +```powershell +.\.venv\Scripts\python.exe tools\release\build_release.py --scope cli --clean-dist +``` + Release environment assumptions: ```text @@ -148,8 +165,9 @@ Local build_local.py: requirements. macOS can add unsigned iOS with --include-ios. GitHub Actions: - Apple Release Build, macOS Flutter Build, and iOS Flutter Build are manual - cloud entrypoints. Artifacts downloaded from cloud runs must be collected into + Apple Release Build, macOS Flutter Build, iOS Flutter Build, Windows Release + Build, Linux Release Build, and Android Flutter Build are manual cloud + entrypoints. Artifacts downloaded from cloud runs are collected into tools/release/dist with download_action_artifacts.py. ``` @@ -182,19 +200,50 @@ operit2-cli-linux-x86_64.tar.gz operit2-cli-linux-aarch64.tar.gz ``` -Apple release assets are built in GitHub Actions for normal release work. Use -the Apple aggregate workflow for macOS App, macOS CLI, and unsigned iOS archives, -or trigger the individual macOS and iOS workflows while diagnosing one product. -These workflows are manual `workflow_dispatch` entrypoints, and the macOS/iOS -workflows are also reusable through `workflow_call`. +Cloud workflows are independent manual `workflow_dispatch` entrypoints. The +Apple aggregate workflow invokes the reusable macOS and iOS workflows; the +other workflows build exactly one platform family: + +```text +Apple Release Build macOS App, macOS CLI, and unsigned iOS App +macOS Flutter Build macOS App and/or CLI +iOS Flutter Build unsigned iOS App +Windows Release Build Windows App and/or CLI +Linux Release Build Linux App and/or CLI +Android Flutter Build signed Android APKs +``` + +Android Flutter Build requires these repository secrets before it can run: + +```text +ANDROID_RELEASE_KEYSTORE_BASE64 +ANDROID_RELEASE_STORE_PASSWORD +ANDROID_RELEASE_KEY_ALIAS +ANDROID_RELEASE_KEY_PASSWORD +``` + +`ANDROID_RELEASE_KEYSTORE_BASE64` is the Base64 encoding of the Android release +keystore. The workflow writes the signing material only inside its runner. + +OpenHarmony has no GitHub Actions workflow yet. Its SDK and command-line tools +are not currently distributed from a reproducible URL available to this +repository, so a cloud workflow cannot provision a valid toolchain. The local +OpenHarmony procedure below remains the supported build path until that SDK +distribution source and the signing secrets are supplied. ```powershell gh workflow run "Apple Release Build" -f products=all -f include_ios=true -f build_web_assets=false gh workflow run "macOS Flutter Build" -f products=all -f build_web_assets=false gh workflow run "iOS Flutter Build" -f build_web_assets=false -.\.venv\Scripts\python.exe tools\release\download_action_artifacts.py --run-id --run-id +gh workflow run "Windows Release Build" -f products=all -f cli_arches=all +gh workflow run "Linux Release Build" -f products=all -f cli_arches=all +gh workflow run "Android Flutter Build" +.\.venv\Scripts\python.exe tools\release\download_action_artifacts.py --run-id --run-id --run-id --run-id ``` +`download_action_artifacts.py` accepts only completed successful runs and copies +their release archives into the existing `tools/release/dist` staging directory. + Collaborators can still build the current host locally with one Python command. Run it from the repository root. On Windows, use the project virtual environment. On macOS and Linux, use the active Python 3 environment. diff --git a/README.md b/README.md index 6f0a4f62..9ee263f8 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,12 @@ operit2 cli mcp local-set [--disabled true|false] [--env KEY=VALUE] [--appr .\.venv\Scripts\python.exe tools\release\publish_dist.py ``` +`build_release.py` stages local assets in `tools/release/dist`; it does not +publish them. GitHub Actions platform builds can be downloaded into the same +directory with `tools/release/download_action_artifacts.py`, then published with +`publish_dist.py`. See [BUILDING.md](BUILDING.md) for workflow inputs and Android +signing secrets. + ```powershell operit2 cli market stats operit2 cli market rank [updated|downloads|likes] [page] diff --git a/apps/flutter/app/windows/CMakeLists.txt b/apps/flutter/app/windows/CMakeLists.txt index 03bc6e2c..7e814962 100644 --- a/apps/flutter/app/windows/CMakeLists.txt +++ b/apps/flutter/app/windows/CMakeLists.txt @@ -42,6 +42,8 @@ function(APPLY_STANDARD_SETTINGS TARGET) target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") target_compile_options(${TARGET} PRIVATE /EHsc) target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + # Third-party Windows plugins still use the MSVC experimental coroutine header. + target_compile_definitions(${TARGET} PRIVATE "_SILENCE_EXPERIMENTAL_COROUTINE_DEPRECATION_WARNINGS") target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") endfunction() diff --git a/docs/release-versioning.md b/docs/release-versioning.md index e1ae85ec..96c6a785 100644 --- a/docs/release-versioning.md +++ b/docs/release-versioning.md @@ -170,6 +170,14 @@ Release scope: `build_release.py` only builds release assets. `publish_dist.py` uploads files already staged in `tools/release/dist`. +`tools/release/dist` accumulates assets from local builds, SSH collection, and +GitHub Actions downloads. Use `--clean-dist` with `build_release.py` only when +starting a new staged release set: + +```powershell +.\.venv\Scripts\python.exe tools\release\build_release.py --scope cli --clean-dist +``` + CLI architecture selection: ```powershell @@ -197,20 +205,47 @@ sudo cp -a lib64/libgcc_s*.so* /usr/aarch64-redhat-linux/sys-root/fc43/usr/lib64 sudo ln -sf libgcc_s.so.1 /usr/aarch64-redhat-linux/sys-root/fc43/usr/lib64/libgcc_s.so ``` -macOS App, macOS CLI, and unsigned iOS assets are produced by GitHub Actions for -normal release work. Apple workflows are manual `workflow_dispatch` entrypoints, -and the macOS/iOS workflows are also reusable through `workflow_call`: +Cloud release builds are composable. All available platform workflows are manual +`workflow_dispatch` entrypoints; macOS and iOS are additionally reusable through +`workflow_call` from Apple Release Build: + +```text +Apple Release Build macOS App, macOS CLI, unsigned iOS App +macOS Flutter Build macOS App and/or CLI +iOS Flutter Build unsigned iOS App +Windows Release Build Windows App and/or CLI +Linux Release Build Linux App and/or CLI +Android Flutter Build signed Android APKs +``` + +Android Flutter Build requires these repository secrets: + +```text +ANDROID_RELEASE_KEYSTORE_BASE64 +ANDROID_RELEASE_STORE_PASSWORD +ANDROID_RELEASE_KEY_ALIAS +ANDROID_RELEASE_KEY_PASSWORD +``` + +The Android keystore secret must be Base64 encoded. OpenHarmony remains a local +build because this repository has no reproducible distribution URL for its SDK +and command-line tools. Do not treat it as an available cloud build until that +external toolchain source and the signing secrets are configured. ```powershell gh workflow run "Apple Release Build" -f products=all -f include_ios=true -f build_web_assets=false gh workflow run "macOS Flutter Build" -f products=all -f build_web_assets=false gh workflow run "iOS Flutter Build" -f build_web_assets=false -.\.venv\Scripts\python.exe tools\release\download_action_artifacts.py --run-id --run-id +gh workflow run "Windows Release Build" -f products=all -f cli_arches=all +gh workflow run "Linux Release Build" -f products=all -f cli_arches=all +gh workflow run "Android Flutter Build" +.\.venv\Scripts\python.exe tools\release\download_action_artifacts.py --run-id --run-id --run-id --run-id ``` -Cloud release builds are composable: trigger the selected platform workflows, -collect each successful run into `tools/release/dist`, then publish the collected -files through `publish_dist.py` when the release set is complete. +`download_action_artifacts.py` verifies that every requested run completed with +the `success` conclusion before copying release assets into `tools/release/dist`. +Publish the collected files through `publish_dist.py` when the release set is +complete. Collaborators can build the current host locally with one Python entrypoint. On macOS, `--include-ios` also builds the unsigned iOS archive when the iOS Xcode diff --git a/tools/build_scripts/build_flutter_android.py b/tools/build_scripts/build_flutter_android.py index c169e73a..a0892819 100644 --- a/tools/build_scripts/build_flutter_android.py +++ b/tools/build_scripts/build_flutter_android.py @@ -10,7 +10,7 @@ RELEASE_DIR, copy_required_file, flutter_command, - dart_pub_get, + flutter_pub_get, read_properties, prepare_web_access_embedded_assets, run, @@ -76,7 +76,7 @@ def main() -> int: ensure_android_signing() flutter = flutter_command() configure_android_flutter_sdk(flutter) - dart_pub_get(enforce_lockfile=args.enforce_lockfile) + flutter_pub_get(enforce_lockfile=args.enforce_lockfile) run( [ flutter, diff --git a/tools/build_scripts/build_flutter_linux.py b/tools/build_scripts/build_flutter_linux.py index 60bc2b41..43c1cf78 100644 --- a/tools/build_scripts/build_flutter_linux.py +++ b/tools/build_scripts/build_flutter_linux.py @@ -8,7 +8,7 @@ FLUTTER_APP_DIR, compress_tar_gz, flutter_command, - dart_pub_get, + flutter_pub_get, host_arch, prepare_web_access_embedded_assets, run, @@ -29,7 +29,7 @@ def parse_args() -> argparse.Namespace: def main() -> int: args = parse_args() prepare_web_access_embedded_assets() - dart_pub_get(enforce_lockfile=args.enforce_lockfile) + flutter_pub_get(enforce_lockfile=args.enforce_lockfile) command = [flutter_command(), "build", "linux", "--release", "--no-pub"] if args.build_name: command.extend(["--build-name", args.build_name]) diff --git a/tools/build_scripts/build_flutter_windows.py b/tools/build_scripts/build_flutter_windows.py index e532b3fb..77e6fe75 100644 --- a/tools/build_scripts/build_flutter_windows.py +++ b/tools/build_scripts/build_flutter_windows.py @@ -8,7 +8,7 @@ FLUTTER_APP_DIR, compress_zip, flutter_command, - dart_pub_get, + flutter_pub_get, host_arch, prepare_web_access_embedded_assets, run, @@ -29,7 +29,7 @@ def parse_args() -> argparse.Namespace: def main() -> int: args = parse_args() prepare_web_access_embedded_assets() - dart_pub_get(enforce_lockfile=args.enforce_lockfile) + flutter_pub_get(enforce_lockfile=args.enforce_lockfile) command = [flutter_command(), "build", "windows", "--release", "--no-pub"] if args.build_name: command.extend(["--build-name", args.build_name]) diff --git a/tools/build_scripts/check_environment.py b/tools/build_scripts/check_environment.py index e5eb6eed..abf40ad2 100644 --- a/tools/build_scripts/check_environment.py +++ b/tools/build_scripts/check_environment.py @@ -92,6 +92,20 @@ def check_directory(name: str, path: Path) -> CheckResult: return missing(name, f"required directory not found: {path}") +# Checks one pkg-config package required by a native build. +def check_pkg_config_package(package: str) -> CheckResult: + pkg_config = shutil.which("pkg-config") + if pkg_config is None: + return missing("pkg-config", "command not found on PATH: pkg-config") + result = command_output([pkg_config, "--exists", package]) + if result.returncode == 0: + return ok(f"pkg-config package {package}", "available") + return missing( + f"pkg-config package {package}", + f"install the development package that provides {package}", + ) + + # Returns every Rust target installed through rustup. def installed_rust_targets() -> set[str]: result = command_output(["rustup", "target", "list", "--installed"]) @@ -156,6 +170,11 @@ def check_native_app_tools(include_ios: bool) -> list[CheckResult]: check_command("ninja", ["--version"]), check_command("pkg-config", ["--version"]), check_command("clang", ["--version"]), + check_pkg_config_package("gtk+-3.0"), + check_pkg_config_package("gstreamer-1.0"), + check_pkg_config_package("gstreamer-app-1.0"), + check_pkg_config_package("gstreamer-audio-1.0"), + check_pkg_config_package("webkit2gtk-4.1"), ] ) elif platform_name == "macos": @@ -178,6 +197,19 @@ def check_windows_aarch64_cli(cli_arches: str) -> list[CheckResult]: return [check_file("Visual Studio locator", vswhere), check_file("LLVM clang", llvm_clang)] +# Checks Linux cross compilers required by explicitly selected CLI targets. +def check_linux_cross_cli(cli_arches: str) -> list[CheckResult]: + if host_platform() != "linux" or cli_arches == "host": + return [] + results = [] + for architecture in selected_cli_arches(cli_arches): + if architecture == "x86_64": + results.append(check_command("musl-gcc", ["--version"])) + elif architecture == "aarch64": + results.append(check_command("aarch64-linux-gnu-gcc", ["--version"])) + return results + + # Checks signing files needed by the Windows release script's App scope. def check_release_signing_files() -> list[CheckResult]: return [ @@ -236,6 +268,7 @@ def build_checks(args: argparse.Namespace) -> list[CheckResult]: if args.products in ("cli", "all"): checks.extend(check_cli_rust_targets(host_platform(), args.cli_arches)) checks.extend(check_windows_aarch64_cli(args.cli_arches)) + checks.extend(check_linux_cross_cli(args.cli_arches)) if args.release_script and args.products in ("app", "all"): checks.extend(check_release_signing_files()) if args.wsl: diff --git a/tools/build_scripts/cli_common.py b/tools/build_scripts/cli_common.py index ee0e0014..4687f968 100644 --- a/tools/build_scripts/cli_common.py +++ b/tools/build_scripts/cli_common.py @@ -295,6 +295,22 @@ def windows_aarch64_env() -> dict[str, str]: return build_env +# Configures the linker required for one non-native Linux CLI target. +def linux_cross_target_env(target: CliBuildTarget) -> dict[str, str]: + build_env = {**os.environ} + if target.rust_target == "x86_64-unknown-linux-musl": + linker = require_command("musl-gcc") + build_env["CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER"] = linker + build_env["CC_x86_64_unknown_linux_musl"] = linker + return build_env + if target.rust_target == "aarch64-unknown-linux-gnu": + linker = require_command("aarch64-linux-gnu-gcc") + build_env["CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER"] = linker + build_env["CC_aarch64_unknown_linux_gnu"] = linker + return build_env + raise RuntimeError(f"Unsupported Linux cross target: {target.rust_target}") + + def build_cli_target( target: CliBuildTarget, use_default_target: bool = False, @@ -315,6 +331,8 @@ def build_cli_target( build_env = {**os.environ} if target.platform == "windows" and target.arch == "aarch64": build_env = windows_aarch64_env() + elif target.platform == "linux": + build_env = linux_cross_target_env(target) run( [ "cargo", diff --git a/tools/release/build_release.py b/tools/release/build_release.py index 8ca2da1a..cb1480c8 100644 --- a/tools/release/build_release.py +++ b/tools/release/build_release.py @@ -1360,6 +1360,11 @@ def main(): parser.add_argument("--products", nargs="+", choices=[item.value for item in ReleaseProduct]) parser.add_argument("--wsl-distro", default="FedoraLinux-43") parser.add_argument("--no-wsl", action="store_true") + parser.add_argument( + "--clean-dist", + action="store_true", + help="Remove existing staged release assets before building.", + ) parser.add_argument("--check-environment", action="store_true", help="Check release build requirements and exit.") parser.add_argument("--cli-arches", default=CliArchMode.HOST.value, choices=[item.value for item in CliArchMode], help="CLI target architectures: host (current only) or all (all desktop arches)") @@ -1374,7 +1379,10 @@ def main(): check_release_environment(products, args.cli_arches, args.no_wsl, args.wsl_distro) return - reset_dir(DIST_DIR) + if args.clean_dist: + reset_dir(DIST_DIR) + else: + DIST_DIR.mkdir(parents=True, exist_ok=True) reset_dir(WORK_DIR) if ReleaseProduct.APP in products or ReleaseProduct.CLI in products: diff --git a/tools/release/download_action_artifacts.py b/tools/release/download_action_artifacts.py index 762c85c2..84605a97 100644 --- a/tools/release/download_action_artifacts.py +++ b/tools/release/download_action_artifacts.py @@ -2,6 +2,7 @@ from __future__ import annotations import argparse +import json import re import shutil import subprocess @@ -46,6 +47,22 @@ def run(command: list[str | Path]) -> None: subprocess.run([str(part) for part in command], check=True) +# Verifies that a selected GitHub Actions run completed successfully. +def require_successful_run(run_id: str) -> None: + completed = subprocess.run( + ["gh", "run", "view", run_id, "--json", "status,conclusion"], + check=True, + text=True, + stdout=subprocess.PIPE, + ) + status = json.loads(completed.stdout) + if status.get("status") != "completed" or status.get("conclusion") != "success": + raise RuntimeError( + f"GitHub Actions run {run_id} is not a successful completed run: " + f"status={status.get('status')} conclusion={status.get('conclusion')}" + ) + + # Returns every release asset file found under one downloaded run directory. def release_assets(download_dir: Path) -> list[Path]: return sorted( @@ -57,6 +74,7 @@ def release_assets(download_dir: Path) -> list[Path]: # Downloads one run's artifacts into the work directory. def download_run(run_id: str, work_dir: Path) -> Path: + require_successful_run(run_id) download_dir = work_dir / run_id if download_dir.exists(): shutil.rmtree(download_dir)