diff --git a/.gitea/workflows/build-fedora-rpms.yml b/.gitea/workflows/build-fedora-rpms.yml deleted file mode 100644 index bc4b686..0000000 --- a/.gitea/workflows/build-fedora-rpms.yml +++ /dev/null @@ -1,343 +0,0 @@ -name: Build Fedora RPMs - -on: - workflow_dispatch: - inputs: - package_kind: - description: "Interpret package_input as source or binary package names" - required: true - default: "source" - type: choice - options: - - source - - binary - package_input: - description: "Comma, space, or newline separated package list. Leave blank to use the full manifest." - required: false - default: "" - type: string - source_manifest: - description: "Checked-in source manifest path" - required: true - default: "manifests/fedora-43/source-packages.txt" - type: string - source_map: - description: "Checked-in binary to source map path" - required: true - default: "manifests/fedora-43/source-map.tsv" - type: string - fedora_branch: - description: "Fedora dist-git branch" - required: true - default: "f43" - type: string - fedora_release: - description: "Fedora container release" - required: true - default: "43" - type: string - builder_image: - description: "Prebuilt Fedora RPM builder image reference" - required: true - default: "ghcr.io/dawsonc/kde-x86_64-v4-fedora-builder:f43" - type: string - optimization_level: - description: "Compiler optimization level" - required: true - default: "-O3" - type: choice - options: - - -O2 - - -O3 - target_march: - description: "Target march override" - required: true - default: "x86-64-v4" - type: string -permissions: - contents: read - -jobs: - prepare: - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.select.outputs.matrix }} - package_count: ${{ steps.select.outputs.package_count }} - shard_count: ${{ steps.select.outputs.shard_count }} - steps: - - uses: actions/checkout@v4 - - - id: select - env: - PACKAGE_KIND: ${{ inputs.package_kind }} - PACKAGE_INPUT: ${{ inputs.package_input }} - SOURCE_MANIFEST: ${{ inputs.source_manifest }} - SOURCE_MAP: ${{ inputs.source_map }} - run: | - python3 <<'PY' - import json - import os - import re - from pathlib import Path - - source_manifest = Path(os.environ["SOURCE_MANIFEST"]) - source_map_path = Path(os.environ["SOURCE_MAP"]) - requested_source_map_path = source_map_path - package_kind = os.environ["PACKAGE_KIND"].strip() - package_input = os.environ["PACKAGE_INPUT"] - if not source_manifest.exists(): - raise SystemExit(f"Source manifest not found: {source_manifest}") - if not source_map_path.exists() and source_map_path.name == "binary-map.tsv": - compatibility_path = source_map_path.with_name("source-map.tsv") - if compatibility_path.exists(): - source_map_path = compatibility_path - if not source_map_path.exists(): - raise SystemExit( - f"Source map not found: {requested_source_map_path}. " - f"Expected an existing TSV such as {requested_source_map_path.with_name('source-map.tsv')}" - ) - source_packages = [ - line.strip() - for line in source_manifest.read_text(encoding="ascii").splitlines() - if line.strip() - ] - - binary_to_source = {} - for line in source_map_path.read_text(encoding="ascii").splitlines(): - if not line.strip(): - continue - binary_name, source_name = line.split("\t", 1) - binary_to_source[binary_name] = source_name - - def tokenize(raw: str): - parts = re.split(r"[\s,]+", raw.strip()) - return [part for part in parts if part] - - if package_input.strip(): - selected = tokenize(package_input) - else: - selected = source_packages - - if package_kind == "binary": - mapped = [] - missing = [] - for binary_name in selected: - source_name = binary_to_source.get(binary_name) - if source_name is None: - missing.append(binary_name) - else: - mapped.append(source_name) - if missing: - raise SystemExit( - "Missing binary-to-source mappings for: " + ", ".join(sorted(missing)) - ) - selected = mapped - - deduped = [] - seen = set() - for package_name in selected: - if package_name not in seen: - seen.add(package_name) - deduped.append(package_name) - - max_matrix = 256 - if not deduped: - matrix = [] - else: - shard_size = max(1, -(-len(deduped) // max_matrix)) - shards = [ - deduped[index:index + shard_size] - for index in range(0, len(deduped), shard_size) - ] - matrix = [ - { - "shard": shard_index + 1, - "packages": shard, - "package_count": len(shard), - } - for shard_index, shard in enumerate(shards) - ] - - github_output = Path(os.environ["GITHUB_OUTPUT"]) - with github_output.open("a", encoding="utf-8") as fh: - fh.write(f"matrix={json.dumps(matrix)}\n") - fh.write(f"package_count={len(deduped)}\n") - fh.write(f"shard_count={len(matrix)}\n") - PY - - - name: Summarize selection - run: | - echo "Selected packages: ${{ steps.select.outputs.package_count }}" >> "$GITHUB_STEP_SUMMARY" - echo "Shards: ${{ steps.select.outputs.shard_count }}" >> "$GITHUB_STEP_SUMMARY" - echo '${{ steps.select.outputs.matrix }}' >> "$GITHUB_STEP_SUMMARY" - - build: - needs: prepare - if: ${{ needs.prepare.outputs.package_count != '0' }} - runs-on: ubuntu-latest - container: - image: ${{ inputs.builder_image }} - strategy: - fail-fast: false - max-parallel: 128 - matrix: - include: ${{ fromJSON(needs.prepare.outputs.matrix) }} - steps: - - uses: actions/checkout@v4 - - - name: Cache Fedora DNF data - uses: actions/cache@v4 - with: - path: | - /var/cache/dnf - /var/lib/dnf - key: dnf-fedora-${{ inputs.fedora_release }} - restore-keys: | - dnf-fedora-${{ inputs.fedora_release }}- - - - name: Verify builder toolchain - run: | - command -v dnf - command -v fedpkg - command -v rpmbuild - command -v git - command -v node - command -v gcc - command -v g-ir-scanner - command -v gi-docgen - rpm -q rpm-build rpmdevtools dnf-plugins-core - - - name: Build shard packages - env: - SHARD_INDEX: ${{ matrix.shard }} - PACKAGES_JSON: ${{ toJSON(matrix.packages) }} - FEDORA_BRANCH: ${{ inputs.fedora_branch }} - OPT_LEVEL: ${{ inputs.optimization_level }} - TARGET_MARCH: ${{ inputs.target_march }} - run: | - python3 <<'PY' - import json - import os - import shutil - import subprocess - from pathlib import Path - - workspace = Path(os.environ["GITHUB_WORKSPACE"]) - shard_index = int(os.environ["SHARD_INDEX"]) - packages = json.loads(os.environ["PACKAGES_JSON"]) - fedora_branch = os.environ["FEDORA_BRANCH"] - opt_level = os.environ["OPT_LEVEL"] - target_march = os.environ["TARGET_MARCH"] - optflags_override = ( - f"{opt_level} -flto=auto -ffat-lto-objects -fexceptions -g " - "-grecord-gcc-switches -pipe -Wall -Wno-complain-wrong-lang " - "-Werror=format-security -Wp,-U_FORTIFY_SOURCE,-D_FORTIFY_SOURCE=3 " - "-Wp,-D_GLIBCXX_ASSERTIONS -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 " - "-fstack-protector-strong -specs=/usr/lib/rpm/redhat/redhat-annobin-cc1 " - f"-m64 -march={target_march} -mtune=generic -fasynchronous-unwind-tables " - "-fstack-clash-protection -fcf-protection -mtls-dialect=gnu2 " - "-fno-omit-frame-pointer -mno-omit-leaf-frame-pointer" - ) - shard_root = workspace / "artifacts" / f"shard-{shard_index:03d}" - shard_root.mkdir(parents=True, exist_ok=True) - rpmbuild_root = workspace / ".rpmbuild" - rpmbuild_root.mkdir(parents=True, exist_ok=True) - - for package_name in packages: - package_dir = workspace / package_name - if package_dir.exists(): - shutil.rmtree(package_dir) - subprocess.run( - [ - "git", - "clone", - "--depth", - "1", - "--branch", - fedora_branch, - f"https://src.fedoraproject.org/rpms/{package_name}.git", - str(package_dir), - ], - check=True, - ) - subprocess.run(["fedpkg", "sources"], cwd=package_dir, check=True) - spec_file = next(package_dir.glob("*.spec"), None) - if spec_file is None: - raise SystemExit(f"No spec file found for {package_name}") - subprocess.run( - [ - "dnf", - "-y", - "builddep", - "--setopt=keepcache=1", - "--setopt=skip_if_unavailable=True", - str(spec_file), - ], - cwd=package_dir, - check=True, - ) - topdir = rpmbuild_root / package_name - for subdir in ("BUILD", "BUILDROOT", "RPMS", "SOURCES", "SPECS", "SRPMS"): - (topdir / subdir).mkdir(parents=True, exist_ok=True) - subprocess.run( - [ - "rpmbuild", - "-ba", - str(spec_file), - "--define", - f"optflags {optflags_override}", - "--define", - f"_topdir {topdir}", - "--define", - f"_builddir {topdir / 'BUILD'}", - "--define", - f"_buildrootdir {topdir / 'BUILDROOT'}", - "--define", - f"_rpmdir {topdir / 'RPMS'}", - "--define", - f"_srcrpmdir {topdir / 'SRPMS'}", - "--define", - f"_sourcedir {package_dir}", - "--define", - f"_specdir {package_dir}", - ], - cwd=package_dir, - check=True, - ) - - outdir = shard_root / package_name - outdir.mkdir(parents=True, exist_ok=True) - for rpm_path in (topdir / "RPMS").rglob("*.rpm"): - shutil.copy2(rpm_path, outdir / rpm_path.name) - for src_path in (topdir / "SRPMS").rglob("*.src.rpm"): - shutil.copy2(src_path, outdir / src_path.name) - print(f"Built {package_name}") - PY - - - name: Collect artifacts - env: - SHARD_INDEX: ${{ matrix.shard }} - run: | - SHARD_LABEL=$(printf "%03d" "${SHARD_INDEX}") - OUTDIR="${GITHUB_WORKSPACE}/artifacts/shard-${SHARD_LABEL}" - ls -la "${OUTDIR}" - - - name: Upload RPM artifacts - uses: actions/upload-artifact@v4 - with: - name: rpm-shard-${{ format('{0:03}', matrix.shard) }} - path: artifacts/shard-${{ format('{0:03}', matrix.shard) }}/ - if-no-files-found: error - - - name: Append build summary - if: always() - env: - SHARD_INDEX: ${{ matrix.shard }} - run: | - SHARD_LABEL=$(printf "%03d" "${SHARD_INDEX}") - echo "### shard-${SHARD_LABEL}" >> "$GITHUB_STEP_SUMMARY" - if [ -d "artifacts/shard-${SHARD_LABEL}" ]; then - find "artifacts/shard-${SHARD_LABEL}" -maxdepth 2 -type f | sort >> "$GITHUB_STEP_SUMMARY" - else - echo "No artifacts collected." >> "$GITHUB_STEP_SUMMARY" - fi diff --git a/.gitea/workflows/build-v3-rpms.yml b/.gitea/workflows/build-v3-rpms.yml new file mode 100644 index 0000000..21b8060 --- /dev/null +++ b/.gitea/workflows/build-v3-rpms.yml @@ -0,0 +1,183 @@ +name: Build Fedora x86_64-v3 RPMs (Gitea) + +on: + workflow_dispatch: + push: + paths: + - 'packages.txt' + - '.gitea/workflows/build-v3-rpms.yml' + +jobs: + prepare: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.select.outputs.matrix }} + package_count: ${{ steps.select.outputs.package_count }} + steps: + - uses: actions/checkout@v4 + + - id: select + run: | + python3 <<'PY' + import json + import os + from pathlib import Path + + packages_file = Path("packages.txt") + if not packages_file.exists(): + print("::error::packages.txt not found") + exit(1) + + packages = [ + line.strip() + for line in packages_file.read_text(encoding="utf-8").splitlines() + if line.strip() and not line.startswith("#") + ] + + # Gitea Actions might have different limits, but 256 is generally safe for matrix + max_matrix = 256 + matrix = [] + if packages: + shard_size = max(1, -(-len(packages) // max_matrix)) + shards = [ + packages[index:index + shard_size] + for index in range(0, len(packages), shard_size) + ] + matrix = [ + { + "shard": shard_index + 1, + "shard_label": f"{shard_index + 1:03d}", + "packages": shard, + } + for shard_index, shard in enumerate(shards) + ] + + # Gitea Actions supports GITHUB_OUTPUT + output_file = os.environ.get("GITHUB_OUTPUT") + if output_file: + with open(output_file, "a") as fh: + fh.write(f"matrix={json.dumps(matrix)}\n") + fh.write(f"package_count={len(packages)}\n") + else: + print(f"::set-output name=matrix::{json.dumps(matrix)}") + print(f"::set-output name=package_count::{len(packages)}") + PY + + build: + needs: prepare + if: ${{ needs.prepare.outputs.package_count != '0' }} + runs-on: ubuntu-latest + container: + image: fedora:43 + strategy: + fail-fast: false + matrix: + include: ${{ fromJSON(needs.prepare.outputs.matrix) }} + steps: + - uses: actions/checkout@v4 + - name: Install build tools + run: | + dnf -y install 'dnf-command(builddep)' fedpkg rpm-build rpmdevtools git python3 + + - name: Build shard packages + env: + SHARD_INDEX: ${{ matrix.shard }} + PACKAGES_JSON: ${{ toJSON(matrix.packages) }} + FEDORA_BRANCH: f43 + TARGET_MARCH: x86-64-v3 + run: | + python3 <<'PY' + import json + import os + import shutil + import subprocess + import time + from pathlib import Path + + workspace = Path(os.environ.get("GITHUB_WORKSPACE", ".")) + shard_index = int(os.environ["SHARD_INDEX"]) + packages = json.loads(os.environ["PACKAGES_JSON"]) + fedora_branch = os.environ["FEDORA_BRANCH"] + target_march = os.environ["TARGET_MARCH"] + + # Standard Fedora optflags with march override + optflags_override = ( + f"-O2 -flto=auto -ffat-lto-objects -fexceptions -g " + "-grecord-gcc-switches -pipe -Wall -Werror=format-security " + "-Wp,-U_FORTIFY_SOURCE,-D_FORTIFY_SOURCE=3 " + "-Wp,-D_GLIBCXX_ASSERTIONS -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 " + "-fstack-protector-strong -specs=/usr/lib/rpm/redhat/redhat-annobin-cc1 " + f"-m64 -march={target_march} -mtune=generic -fasynchronous-unwind-tables " + "-fstack-clash-protection -fcf-protection -mtls-dialect=gnu2 " + "-fno-omit-frame-pointer -mno-omit-leaf-frame-pointer" + ) + + shard_root = workspace / "artifacts" / f"shard-{shard_index:03d}" + shard_root.mkdir(parents=True, exist_ok=True) + rpmbuild_root = workspace / ".rpmbuild" + rpmbuild_root.mkdir(parents=True, exist_ok=True) + + def run_with_retry(command, *, cwd=None, cleanup_path=None, attempts=3): + for attempt in range(1, attempts + 1): + if cleanup_path is not None and cleanup_path.exists(): + shutil.rmtree(cleanup_path) + try: + subprocess.run(command, cwd=cwd, check=True) + return + except subprocess.CalledProcessError: + if attempt == attempts: + raise + time.sleep(5 * attempt) + + for package_name in packages: + package_dir = workspace / package_name + run_with_retry( + [ + "git", "clone", "--depth", "1", "--branch", fedora_branch, + f"https://src.fedoraproject.org/rpms/{package_name}.git", + str(package_dir), + ], + cleanup_path=package_dir, + ) + + run_with_retry(["fedpkg", "sources"], cwd=package_dir) + + spec_file = next(package_dir.glob("*.spec"), None) + if not spec_file: + print(f"No spec file for {package_name}") + continue + + subprocess.run(["dnf", "-y", "builddep", str(spec_file)], check=True) + + topdir = rpmbuild_root / package_name + for subdir in ("BUILD", "BUILDROOT", "RPMS", "SOURCES", "SPECS", "SRPMS"): + (topdir / subdir).mkdir(parents=True, exist_ok=True) + + subprocess.run( + [ + "rpmbuild", "-ba", str(spec_file), + "--define", f"optflags {optflags_override}", + "--define", f"_topdir {topdir}", + "--define", f"_builddir {topdir / 'BUILD'}", + "--define", f"_buildrootdir {topdir / 'BUILDROOT'}", + "--define", f"_rpmdir {topdir / 'RPMS'}", + "--define", f"_srcrpmdir {topdir / 'SRPMS'}", + "--define", f"_sourcedir {package_dir}", + "--define", f"_specdir {package_dir}", + ], + check=True, + ) + + for rpm_path in (topdir / "RPMS").rglob("*.rpm"): + shutil.copy2(rpm_path, shard_root / rpm_path.name) + for src_path in (topdir / "SRPMS").rglob("*.src.rpm"): + shutil.copy2(src_path, shard_root / src_path.name) + print(f"Built {package_name}") + PY + + - name: Upload RPM artifacts + uses: actions/upload-artifact@v4 + with: + name: rpm-shard-${{ matrix.shard_label }} + path: artifacts/shard-${{ matrix.shard_label }}/ + if-no-files-found: error diff --git a/README.md b/README.md index e83f30b..387a7d9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ -# Fedora 43 KDE GitHub Actions RPM Builder +# Fedora 43 KDE Actions RPM Builder -This repository is a static GitHub Actions input repo for rebuilding Fedora 43 +This repository provides CI workflows for both GitHub Actions and Gitea Actions to rebuild Fedora 43 KDE packages from Fedora dist-git with `x86-64-v3` code generation and `-O3` -optimization. \ No newline at end of file +optimization. + +## Workflows +- **GitHub Actions**: `.github/workflows/build-v3-rpms.yml` +- **Gitea Actions**: `.gitea/workflows/build-v3-rpms.yml` \ No newline at end of file