diff --git a/.git_components.yaml b/.git_components.yaml index b6e584d4..cfc5dbc2 100644 --- a/.git_components.yaml +++ b/.git_components.yaml @@ -37,3 +37,6 @@ config: tests: owners: - artiepoole +snapcraft: + owners: + - artiepoole diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index c0441731..ce17837d 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -144,6 +144,29 @@ jobs: echo "::group::yamlfmt output" yamlfmt -lint . echo "::endgroup::" + version-bump-check: + name: Check Version Bump for Rust Changes + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Ensure version is bumped if there are any rust changes + id: check-rust-changes + run: | + set -e + # Check if any .rs files in /src/ directories have been modified + changed_rust_files=$(git diff --name-only --diff-filter=ACMRTUXB origin/${{ github.base_ref }} | grep -E "^(daemon|cli|fpgad_macros)/src/.*\.rs$" || true) + + if [ -z "$changed_rust_files" ]; then + echo "✅ No Rust source files modified - version bump not required." + exit 0 + else + echo "::group::Modified Rust source files" + echo "${changed_rust_files}" + echo "::endgroup::" + python scripts/ci/check_version_bump.py + fi rust-check: name: Check Rust Code runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index f05a8de0..fdc8a022 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -165,17 +165,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" -[[package]] -name = "cli" -version = "0.1.0" -dependencies = [ - "clap", - "env_logger", - "log", - "tokio", - "zbus", -] - [[package]] name = "colorchoice" version = "1.0.4" @@ -197,20 +186,6 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "daemon" -version = "0.1.0" -dependencies = [ - "env_logger", - "fpgad_macros", - "googletest", - "log", - "rstest", - "thiserror", - "tokio", - "zbus", -] - [[package]] name = "endi" version = "1.1.0" @@ -304,6 +279,31 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fpgad" +version = "0.1.0" +dependencies = [ + "env_logger", + "fpgad_macros", + "googletest", + "log", + "rstest", + "thiserror", + "tokio", + "zbus", +] + +[[package]] +name = "fpgad_cli" +version = "0.1.0" +dependencies = [ + "clap", + "env_logger", + "log", + "tokio", + "zbus", +] + [[package]] name = "fpgad_macros" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index f7fb30d1..5c811f26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,15 @@ [workspace] resolver = "3" members = ["daemon", "cli", "fpgad_macros"] + +[workspace.package] +version = "0.1.0" +edition = "2024" +license = "GPL-3.0" +homepage = "https://github.com/canonical/fpgad" +repository = "https://github.com/canonical/fpgad" +authors = ["Talha Can Havadar ", "Artie Poole "] + +[workspace.dependencies] +fpgad_macros = { path = "fpgad_macros" } + diff --git a/PUBLISHING.md b/PUBLISHING.md new file mode 100644 index 00000000..c1a879c5 --- /dev/null +++ b/PUBLISHING.md @@ -0,0 +1,75 @@ +# Publishing Guide for FPGAd + +This document explains how to publish the FPGAd packages to crates.io. + +## Package Structure + +The workspace contains 3 packages: + +1. **`fpgad_macros`** - Procedural macros (must be published first) +2. **`fpgad`** - The daemon (depends on fpgad_macros) +3. **`fpgad_cli`** - Command-line interface + +## Publication Order + +Packages must be published in this order due to dependencies: + +1. `fpgad_macros` (no dependencies on other workspace crates) +2. `fpgad` (depends on fpgad_macros) +3. `fpgad_cli` (no dependencies on other workspace crates, but logically depends on daemon) + +## Pre-publication Checklist + +- [ ] All packages have proper metadata (version, license, description, etc.) +- [ ] All packages have README.md files +- [ ] Workspace-level metadata is shared across packages +- [ ] Dependencies have version requirements specified +- [ ] All packages compile successfully +- [ ] All tests pass (CI auto runs on PR) +- [ ] Documentation is complete +- [ ] CHANGELOG is up to date +- [ ] Git repository is clean + +## Publishing Commands + +### 1. Publish fpgad_macros + +```bash +cd fpgad_macros +cargo publish +``` + +### 2. Publish fpgad + +```bash +cd daemon +cargo publish +``` + +### 3. Publish fpgad_cli + +```bash +cd cli +cargo publish +``` + +## Dry Run Testing + +Before publishing, test with dry-run: + +```bash +cargo publish --dry-run +``` + +## Version Updates + +When releasing new versions, update the version in the workspace `Cargo.toml`: + +```toml +[workspace.package] +version = "X.Y.Z" # Update this +``` + +All packages will inherit this version. + +Note that the CI pipeline will enforce an increase in the version whenever contents of a .rs file inside /src is changed, before the commits can be merged. diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 0477d28d..89b2b151 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,7 +1,19 @@ [package] -name = "cli" -version = "0.1.0" -edition = "2024" +name = "fpgad_cli" +version.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +description = "Command-line interface for interacting with the FPGAd daemon" +readme = "README.md" +keywords = ["fpga", "cli", "embedded", "hardware", "xilinx"] +categories = ["command-line-utilities", "hardware-support"] + +[[bin]] +name = "fpgad_cli" +path = "src/main.rs" [dependencies] clap = { version = "4.5.41", features = ["derive"] } diff --git a/daemon/Cargo.toml b/daemon/Cargo.toml index d9132b3e..6f243f3d 100644 --- a/daemon/Cargo.toml +++ b/daemon/Cargo.toml @@ -1,12 +1,15 @@ [package] -name = "daemon" -version = "0.1.0" -edition = "2024" -license = "GPL-3.0" +name = "fpgad" +version.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true description = "An FPGA manager daemon that handles the dirty work for you." -homepage = "https://github.com/talhaHavadar/fpgad" -repository = "https://github.com/talhaHavadar/fpgad" readme = "README.md" +keywords = ["fpga", "daemon", "embedded", "hardware", "xilinx"] +categories = ["hardware-support", "embedded"] [features] default = ["softeners-all"] @@ -15,7 +18,7 @@ softeners = [] xilinx-dfx-mgr = ["softeners"] [dependencies] -fpgad_macros = { path = "../fpgad_macros" } +fpgad_macros = { workspace = true } log = "0.4.27" env_logger = "0.11.8" tokio = { version = "1.47.0", features = ["full"] } diff --git a/fpgad_macros/Cargo.toml b/fpgad_macros/Cargo.toml index 79a94b83..a08635c5 100644 --- a/fpgad_macros/Cargo.toml +++ b/fpgad_macros/Cargo.toml @@ -1,7 +1,15 @@ [package] name = "fpgad_macros" -version = "0.1.0" -edition = "2024" +version.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +description = "Procedural macros for the FPGAd project" +readme = "README.md" +keywords = ["fpga", "macros", "proc-macro"] +categories = ["development-tools::procedural-macro-helpers"] [lib] proc-macro = true diff --git a/scripts/ci/check_version_bump.py b/scripts/ci/check_version_bump.py new file mode 100644 index 00000000..c224531b --- /dev/null +++ b/scripts/ci/check_version_bump.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +# This file is part of fpgad, an application to manage FPGA subsystem together with device-tree and kernel modules. +# +# Copyright 2025 Canonical Ltd. +# +# SPDX-License-Identifier: GPL-3.0-only +# +# fpgad is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3, as published by the Free Software Foundation. +# +# fpgad is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/. + +""" +Check if version was bumped when Rust source files are modified. + +This script compares the version in Cargo.toml between the base branch +and the current branch, ensuring proper semver versioning when Rust +source files have been modified. +""" + +import os +import subprocess +import sys +import re +from pathlib import Path +import tomllib + + +def run_git_command(cmd: list[str]) -> str: + """Run a git command and return the output.""" + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + return result.stdout.strip() + + +def get_cargo_toml_content(ref: str | None = None) -> str: + """Get Cargo.toml content from a specific ref or current working tree.""" + if ref: + try: + return run_git_command(["git", "show", f"{ref}:Cargo.toml"]) + except subprocess.CalledProcessError as e: + print(f"::error::Failed to get Cargo.toml from {ref}: {e}") + sys.exit(1) + else: + cargo_path = Path("Cargo.toml") + if not cargo_path.exists(): + print("::error::Cargo.toml not found in current directory") + sys.exit(1) + return cargo_path.read_text() + + +def parse_version_from_toml(toml_content: str) -> str: + """Parse version from Cargo.toml content.""" + try: + data = tomllib.loads(toml_content) + version = data.get("workspace", {}).get("package", {}).get("version") + + if not version: + print("::error::Could not find version in [workspace.package] section") + sys.exit(1) + + return version + except Exception as e: + print(f"::error::Failed to parse TOML: {e}") + sys.exit(1) + + +def parse_semver(version: str) -> tuple[int, int, int]: + """ + Parse a semantic version string. + + Returns (major, minor, patch) as integers. + Ignores pre-release and build metadata. + """ + # Extract major.minor.patch (ignore pre-release and build metadata) + match = re.match(r"^(\d+)\.(\d+)\.(\d+)", version) + if not match: + print(f"::error::Invalid semver format: {version}") + sys.exit(1) + + return int(match.group(1)), int(match.group(2)), int(match.group(3)) + + +def compare_versions(old_version: str, new_version: str) -> bool: + """ + Compare two semantic versions. + + Returns True if new_version > old_version, False otherwise. + """ + old_major, old_minor, old_patch = parse_semver(old_version) + new_major, new_minor, new_patch = parse_semver(new_version) + + if new_major > old_major: + return True + elif new_major == old_major and new_minor > old_minor: + return True + elif new_major == old_major and new_minor == old_minor and new_patch > old_patch: + return True + + return False + + +def main(): + # Get base ref from environment or use 'main' as default + base_ref = os.environ.get("GITHUB_BASE_REF", "main") + + print("::group::extract version numbers") + # Get versions + old_toml = get_cargo_toml_content(f"origin/{base_ref}") + old_version = parse_version_from_toml(old_toml) + + new_toml = get_cargo_toml_content() + new_version = parse_version_from_toml(new_toml) + + print(f"Old version: {old_version}") + print(f"New version: {new_version}") + print("::endgroup::") + + print("::group::compare versions using semver") + version_increased = compare_versions(old_version, new_version) + + if not version_increased: + error_msg = ( + f"::error::Version was not increased!%0A" + f"Old version: {old_version}%0A" + f"New version: {new_version}%0A" + f"%0A" + f"Rust source files were modified but the version was not bumped in Cargo.toml.%0A" + f"Please update the version in the [workspace.package] section of the root Cargo.toml.%0A" + f"Version must follow semver and be greater than {old_version}.%0A" + f"%0A" + ) + print(error_msg) + print("::endgroup::") # necessary to avoid never-ending group + sys.exit(1) + else: + print(f"✅ Version was properly increased: {old_version} → {new_version}") + print("::endgroup::") + + +if __name__ == "__main__": + main() diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index cb179d06..e0ac5c24 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -38,6 +38,7 @@ plugs: name: com.canonical.fpgad provider-content: interface: content + content: provider-content target: $SNAP_DATA/provider-content device-tree-overlays: interface: system-files @@ -47,11 +48,11 @@ plugs: - /sys/kernel/config/device-tree/overlays apps: fpgad: - command: bin/cli + command: bin/fpgad_cli plugs: - cli-dbus daemon: - command: bin/daemon + command: bin/fpgad daemon: dbus restart-condition: always start-timeout: 30s diff --git a/tests/coverage_test.sh b/tests/coverage_test.sh index f1412a8b..f28576dd 100755 --- a/tests/coverage_test.sh +++ b/tests/coverage_test.sh @@ -20,20 +20,22 @@ eval "$(cargo llvm-cov show-env --sh)" export RUSTFLAGS="${RUSTFLAGS:-} -C llvm-args=-runtime-counter-relocation" # build the daemon only, to avoid getting coverage for cli (no tests written) -cargo build --bin daemon -# build the test binaries avoiding cli as well. Also extract the name of the integration test binary +cargo build --bin fpgad + +# build the test binaries avoiding cli as well. Also extract the names of the integration test binaries universal_test="$(\ -cargo test --no-run --bin daemon --test universal 2>&1 |\ +cargo test --no-run --bin fpgad --test universal 2>&1 |\ grep 'tests/universal.rs' |\ awk '{gsub(/[()]/,""); print $3}'\ )" echo "universal test binary: $universal_test" + # only run daemon unit tests -cargo test --bin daemon +cargo test --bin fpgad -daemon_bin=${CARGO_LLVM_COV_TARGET_DIR}/debug/daemon +daemon_bin=${CARGO_LLVM_COV_TARGET_DIR}/debug/fpgad # Kill any leftover processes or other daemon instances (this will not stop the snap version from spawning due to 'activates-on:` @@ -54,7 +56,8 @@ timeout 5 bash -c ' done ' -# run the test binary +# run the universal test binary +echo "Running universal tests..." sudo env LLVM_PROFILE_FILE="${CARGO_LLVM_COV_TARGET_DIR}/test_universal-%p.profraw" "$universal_test" --test-threads=1 --no-capture