diff --git a/.github/workflows/copr-crates.yml b/.github/workflows/copr-crates.yml new file mode 100644 index 0000000..3005d1a --- /dev/null +++ b/.github/workflows/copr-crates.yml @@ -0,0 +1,261 @@ +name: COPR Crates + +on: + push: + branches: [main] + paths: + - "copr/**" + pull_request: + paths: + - "copr/**" + workflow_dispatch: + +concurrency: + group: copr-crates-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + detect-packages: + runs-on: ubuntu-24.04 + outputs: + count: ${{ steps.detect.outputs.count }} + plan: ${{ steps.detect.outputs.plan }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Resolve diff base + id: base + env: + EVENT_NAME: ${{ github.event_name }} + PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} + PUSH_BEFORE_SHA: ${{ github.event.before }} + run: | + set -euo pipefail + + if [[ "$EVENT_NAME" == "pull_request" ]]; then + base_sha="$PR_BASE_SHA" + elif [[ "$EVENT_NAME" == "push" ]]; then + base_sha="$PUSH_BEFORE_SHA" + else + base_sha="$(git rev-list --max-parents=0 HEAD)" + fi + + if [[ -z "$base_sha" || "$base_sha" == "0000000000000000000000000000000000000000" ]]; then + base_sha="$(git rev-list --max-parents=0 HEAD)" + fi + + echo "sha=$base_sha" >> "$GITHUB_OUTPUT" + + - name: Detect changed COPR packages + id: detect + env: + BASE_SHA: ${{ steps.base.outputs.sha }} + HEAD_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + run: | + set -euo pipefail + + python3 <<'PY' + import json + import os + import pathlib + import re + import subprocess + import sys + + base_sha = os.environ["BASE_SHA"] + head_sha = os.environ["HEAD_SHA"] + + diff_files = subprocess.check_output( + ["git", "diff", "--name-only", base_sha, head_sha, "--", "copr"], + text=True, + ).splitlines() + + changed_packages = { + path.split("/")[1] + for path in diff_files + if path.startswith("copr/") and len(path.split("/")) >= 3 + } + + graph = {} + readme_order = [] + readme_path = pathlib.Path("copr/README.md") + if readme_path.exists(): + bullet = re.compile(r"^(?P\s*)\*\s+(?P[A-Za-z0-9._+-]+)\s*$") + stack = [] + for line in readme_path.read_text(encoding="utf-8").splitlines(): + match = bullet.match(line) + if match: + name = match.group("name") + indent = len(match.group("indent").replace("\t", " ")) + + graph.setdefault(name, set()) + readme_order.append(name) + + while stack and stack[-1][0] >= indent: + stack.pop() + + if stack: + parent = stack[-1][1] + graph.setdefault(parent, set()).add(name) + + stack.append((indent, name)) + + spec_names = {} + for pkg in sorted(changed_packages): + spec_files = sorted(pathlib.Path("copr", pkg).glob("*.spec")) + if not spec_files: + continue + + if len(spec_files) != 1: + print( + f"::error file=copr/{pkg}::Expected exactly one .spec file, found {len(spec_files)}" + ) + sys.exit(1) + + spec_names[pkg] = spec_files[0].name + + build_set = set(spec_names) + ordered_packages = [] + visited = set() + visiting = set() + + def visit(pkg): + if pkg in visited: + return + if pkg in visiting: + print(f"::error file=copr/README.md::Dependency cycle detected involving {pkg}") + sys.exit(1) + + visiting.add(pkg) + for dep in graph.get(pkg, set()): + if dep in build_set: + visit(dep) + visiting.remove(pkg) + visited.add(pkg) + ordered_packages.append(pkg) + + seeds = [] + for pkg in readme_order: + if pkg in build_set and pkg not in seeds: + seeds.append(pkg) + for pkg in sorted(build_set): + if pkg not in seeds: + seeds.append(pkg) + + for pkg in seeds: + visit(pkg) + + plan = [ + { + "package": pkg, + "spec_name": spec_names[pkg], + } + for pkg in ordered_packages + ] + + plan_json = json.dumps(plan, separators=(",", ":")) + + summary_lines = [f"Detected {len(plan)} COPR package(s) to build."] + summary_lines.extend(f"- {entry['package']} ({entry['spec_name']})" for entry in plan) + pathlib.Path(os.environ["GITHUB_STEP_SUMMARY"]).write_text( + "\n".join(summary_lines) + "\n", + encoding="utf-8", + ) + + with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output: + output.write(f"count={len(plan)}\n") + output.write("plan< + needs.detect-packages.outputs.count != '0' && + (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) + runs-on: ubuntu-24.04 + timeout-minutes: 240 + steps: + - uses: actions/checkout@v4 + + - name: Require COPR secrets + env: + COPR_LOGIN: ${{ secrets.COPR_LOGIN }} + COPR_TOKEN: ${{ secrets.COPR_TOKEN }} + run: | + set -euo pipefail + if [[ -z "$COPR_LOGIN" || -z "$COPR_TOKEN" ]]; then + echo "COPR_LOGIN and COPR_TOKEN secrets are required." + exit 1 + fi + + - name: Install copr-cli + run: | + set -euo pipefail + python3 -m pip install --user --upgrade copr-cli + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + + - name: Configure copr-cli auth + env: + COPR_LOGIN: ${{ secrets.COPR_LOGIN }} + COPR_TOKEN: ${{ secrets.COPR_TOKEN }} + run: | + set -euo pipefail + mkdir -p "$HOME/.config" + cat > "$HOME/.config/copr" <"] diff --git a/copr/rust-dma-heap/rust-dma-heap.spec b/copr/rust-dma-heap/rust-dma-heap.spec new file mode 100644 index 0000000..939e751 --- /dev/null +++ b/copr/rust-dma-heap/rust-dma-heap.spec @@ -0,0 +1,82 @@ +# Generated by rust2rpm 28 +%bcond check 1 +%global debug_package %{nil} + +%global crate dma-heap + +Name: rust-dma-heap +Version: 0.4.1 +Release: %autorelease +Summary: Linux DMA-Buf Heap Abstraction Library + +License: MIT +URL: https://crates.io/crates/dma-heap +Source: %{crates_source} +# Manually created patch for downstream crate metadata changes +Patch: dma-heap-fix-metadata.diff + +BuildRequires: cargo-rpm-macros >= 24 + +%global _description %{expand: +Linux DMA-Buf Heap Abstraction Library.} + +%description %{_description} + +%package devel +Summary: %{summary} +BuildArch: noarch + +%description devel %{_description} + +This package contains library source intended for building other packages which +use the "%{crate}" crate. + +%files devel +%license %{crate_instdir}/LICENSE +%doc %{crate_instdir}/README.md +%{crate_instdir}/ + +%package -n %{name}+default-devel +Summary: %{summary} +BuildArch: noarch + +%description -n %{name}+default-devel %{_description} + +This package contains library source intended for building other packages which +use the "default" feature of the "%{crate}" crate. + +%files -n %{name}+default-devel +%ghost %{crate_instdir}/Cargo.toml + +%package -n %{name}+nightly-devel +Summary: %{summary} +BuildArch: noarch + +%description -n %{name}+nightly-devel %{_description} + +This package contains library source intended for building other packages which +use the "nightly" feature of the "%{crate}" crate. + +%files -n %{name}+nightly-devel +%ghost %{crate_instdir}/Cargo.toml + +%prep +%autosetup -n %{crate}-%{version} -p1 +%cargo_prep + +%generate_buildrequires +%cargo_generate_buildrequires + +%build +%cargo_build + +%install +%cargo_install + +%if %{with check} +%check +%cargo_test +%endif + +%changelog +%autochangelog diff --git a/copr/rust-dma-heap/rust2rpm.toml b/copr/rust-dma-heap/rust2rpm.toml new file mode 100644 index 0000000..b1f9b6e --- /dev/null +++ b/copr/rust-dma-heap/rust2rpm.toml @@ -0,0 +1,4 @@ +[package] +cargo-toml-patch-comments = [ + "add SPDX license metadata; upstream crate only ships license-file", +] diff --git a/copr/rust-is_terminal_polyfill/rust-is_terminal_polyfill.spec b/copr/rust-is_terminal_polyfill/rust-is_terminal_polyfill.spec new file mode 100644 index 0000000..85821fc --- /dev/null +++ b/copr/rust-is_terminal_polyfill/rust-is_terminal_polyfill.spec @@ -0,0 +1,69 @@ +# Generated by rust2rpm 28 +%bcond check 1 +%global debug_package %{nil} + +%global crate is_terminal_polyfill + +Name: rust-is_terminal_polyfill +Version: 1.70.2 +Release: %autorelease +Summary: Polyfill for is_terminal stdlib feature for use with older MSRVs + +License: MIT OR Apache-2.0 +URL: https://crates.io/crates/is_terminal_polyfill +Source: %{crates_source} + +BuildRequires: cargo-rpm-macros >= 24 + +%global _description %{expand: +Polyfill for `is_terminal` stdlib feature for use with older MSRVs.} + +%description %{_description} + +%package devel +Summary: %{summary} +BuildArch: noarch + +%description devel %{_description} + +This package contains library source intended for building other packages which +use the "%{crate}" crate. + +%files devel +%license %{crate_instdir}/LICENSE-APACHE +%license %{crate_instdir}/LICENSE-MIT +%doc %{crate_instdir}/README.md +%{crate_instdir}/ + +%package -n %{name}+default-devel +Summary: %{summary} +BuildArch: noarch + +%description -n %{name}+default-devel %{_description} + +This package contains library source intended for building other packages which +use the "default" feature of the "%{crate}" crate. + +%files -n %{name}+default-devel +%ghost %{crate_instdir}/Cargo.toml + +%prep +%autosetup -n %{crate}-%{version} -p1 +%cargo_prep + +%generate_buildrequires +%cargo_generate_buildrequires + +%build +%cargo_build + +%install +%cargo_install + +%if %{with check} +%check +%cargo_test +%endif + +%changelog +%autochangelog diff --git a/docs/plans/fedora-packaging.md b/docs/plans/fedora-packaging.md new file mode 100644 index 0000000..75d6ba3 --- /dev/null +++ b/docs/plans/fedora-packaging.md @@ -0,0 +1,139 @@ +# Fedora Packaging Plan (smoo + supplemental Rust crates) + +## Goal + +Make non-vendored Fedora builds of `smoo` reliable by temporarily carrying missing Rust crate packages in-repo (under `copr/`), then upstreaming those crates to Fedora steadily until the local carry set is empty. + + +## Current State Survey (2026-02-26) + +### What phrog does today + +- `~/src/phrog/copr/` contains one directory per carried crate package. +- Typical crate directory contents are `rust2rpm.toml`, `*.spec`, and optional patch/changelog files. +- `~/src/phrog/copr/README.md` tracks crate build order for COPR chain builds. + +### What smoo does today + +- `smoo.spec` already has vendor/non-vendor toggles (`%bcond_without vendor`) but defaults to vendoring. +- `.packit.yaml` currently runs vendored builds (PR jobs explicitly pass `with_opts: [vendor]`; other jobs inherit vendored default from spec). +- There is no `copr/` crate-carry directory in `smoo` yet. + +### Dependency gap snapshot (Fedora 43 + updates repos) + +Scope used: external normal/build deps in the closure of `smoo-gadget-cli` + `smoo-host-cli`. + +- 196 external crate names in scope. +- Crates missing entirely from Fedora repos: + - `dma-heap` + - `is_terminal_polyfill` + - `macaddr` + - `mmap` + - `tempdir` + - `usb-gadget` +- Missing needed compatibility stream: + - `io-uring` `0.7.x` (Fedora currently has `0.6.x` and `0.5.x`) +- Practical impact: + - `smoo-host-cli` is blocked only by `is_terminal_polyfill`. + - `smoo-gadget-cli` is blocked by `dma-heap`, `io-uring 0.7`, `macaddr`, `mmap`, `tempdir`, `usb-gadget`, and also `is_terminal_polyfill`. + +## Plan + +## Phase 0: Make non-vendor path testable first + +Before adding crate carry, ensure `smoo.spec` can be exercised in non-vendor mode in current Fedora macro environments. + +- Verify and fix any macro incompatibilities in `%generate_buildrequires`, `%build`, and `%check`. +- Add a local validation command set for packaging work: + - `rpmspec -q --buildrequires smoo.spec` + - `mock --rebuild --without vendor ` + +Exit criteria: + +- Non-vendor buildrequires generation runs successfully. +- A non-vendor mock build fails only on missing crate packages (not spec/macro syntax). + +## Phase 1: Add `copr/` carry tree to smoo + +Create an in-repo carry area modeled on phrog: + +- `copr/.gitignore` (at minimum ignore `**/*.crate` and `**/*.src.rpm`). +- `copr/README.md` with: + - crate inventory + - build order + - generation/build commands + - upstreaming status per crate + +Initial crate carry set: + +- `rust-is_terminal_polyfill` +- `rust-tempdir` +- `rust-mmap` +- `rust-macaddr` +- `rust-usb-gadget` +- `rust-dma-heap` +- `rust-io-uring0.7` (compat package stream) + +Dependency-driven build order: + +1. `rust-is_terminal_polyfill` +2. `rust-tempdir` +3. `rust-mmap` (depends on `tempdir`) +4. `rust-macaddr` +5. `rust-usb-gadget` (depends on `macaddr`) +6. `rust-dma-heap` +7. `rust-io-uring0.7` + +## Phase 2: Wire COPR + Packit for iterative builds + +- Stand up a dedicated dependency COPR project (recommended: `samcday/smoo-rust-deps`). +- Chain-build the carried crates in README order. +- Update smoo Packit jobs to consume the deps COPR via `additional_repos`. +- Add/enable at least one non-vendor PR build target (keep vendored build as fallback until stable). + +Exit criteria: + +- Packit PR build passes in non-vendor mode with only Fedora repos + `smoo-rust-deps` COPR. +- Vendored mode remains available as emergency fallback during migration. + +## Phase 3: Upstream crates to Fedora steadily + +For each carried crate: + +- Open Fedora review request. +- Address review feedback and land in dist-git. +- Wait for build to appear in target Fedora releases used by smoo. +- Remove the crate from `smoo` `copr/` carry list and deps COPR once Fedora provides it. + +Recommended upstream order (to unblock fastest): + +1. `is_terminal_polyfill` (unblocks both host and gadget) +2. `tempdir` -> `mmap` +3. `macaddr` -> `usb-gadget` +4. `dma-heap` +5. `io-uring 0.7` compat + +## Phase 4: De-vendor by default + +Once dependency carry is stable and mostly upstreamed: + +- Flip spec/Packit defaults to non-vendor builds. +- Keep vendored mode as explicit opt-in escape hatch only. +- Document policy in `README.md` (Fedora section) and `copr/README.md`. + +Exit criteria: + +- `smoo` builds in Fedora/COPR without vendoring by default. +- Carry set trends to zero as crates land upstream. + +## Risks and Mitigations + +- Macro behavior drift across Fedora versions: validate with `mock` for each target release before changing defaults. +- Feature-specific crate subpackages missing (not just base crate): validate non-vendor `buildrequires` output after every dependency bump. +- Stale carry set: require every carried crate to have an upstream tracking link/status in `copr/README.md`. + +## Definition of Done + +- Non-vendored `smoo` COPR/Packit builds pass on target Fedora arches. +- All carried crates have upstream review tickets filed. +- `copr/README.md` is the single source of truth for carry status and retirement. diff --git a/smoo.spec b/smoo.spec index 4eb9f7a..6c9ef48 100644 --- a/smoo.spec +++ b/smoo.spec @@ -44,11 +44,11 @@ Host-side CLI that speaks the smoo USB protocol over rusb. %else %cargo_prep %generate_buildrequires -%cargo_generate_buildrequires +%cargo_generate_buildrequires -p smoo-gadget-cli -p smoo-host-cli %endif %build -%cargo_build +%cargo_build -p smoo-gadget-cli -p smoo-host-cli %cargo_vendor_manifest %{cargo_license_summary} %{cargo_license} > LICENSE.dependencies @@ -61,7 +61,7 @@ install -Dpm0755 target/rpm/smoo-host \ %if %{with check} %check -%cargo_test +%cargo_test -p smoo-gadget-cli -p smoo-host-cli %endif %files