diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..a3205da --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,84 @@ +name: Quality Checks + +on: + push: + branches: [main] + pull_request: + merge_group: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + # Keep every multi-workspace check in sync. + QUALITY_WORKSPACES: | + . + tests/testlib + tests/rust-consumer + +jobs: + cargo-fmt: + name: Code Format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - run: | + for workspace in $QUALITY_WORKSPACES; do + cargo fmt --manifest-path "$workspace/Cargo.toml" --all -- --check + done + + taplo-fmt: + name: TOML Format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: uncenter/setup-taplo@v1 + - run: taplo fmt --check + + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - run: | + for workspace in $QUALITY_WORKSPACES; do + cargo clippy --manifest-path "$workspace/Cargo.toml" --workspace --all-targets --all-features -- -D warnings + done + + doc: + name: Documentation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@stable + - env: + RUSTDOCFLAGS: -D warnings + run: | + for workspace in $QUALITY_WORKSPACES; do + cargo doc --manifest-path "$workspace/Cargo.toml" --workspace --all-features --no-deps + done + + unused-dependencies: + name: Unused Dependencies + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: taiki-e/install-action@e67fa11c4b9316fa714ddf0abed07a0c3143b95b # v2.87.4 + with: + tool: cargo-shear + - run: | + for workspace in $QUALITY_WORKSPACES; do + (cd "$workspace" && cargo shear) + done diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8cd1594 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,114 @@ +name: Release + +on: + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+' + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + validate-source: + name: Validate release source + runs-on: ubuntu-latest + outputs: + release-notes: ${{ steps.changelog.outputs.notes }} + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + persist-credentials: false + - name: Require tagged commit to be on main + run: | + git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main + if ! git merge-base --is-ancestor HEAD refs/remotes/origin/main; then + echo "::error::Release tags must point to a commit on main." + exit 1 + fi + - name: Extract release notes + id: changelog + env: + RELEASE_TAG: ${{ github.ref_name }} + run: | + python3 - <<'PY' + import os + import re + import uuid + from pathlib import Path + + tag = os.environ["RELEASE_TAG"] + changelog = Path("CHANGELOG.md").read_text() + sections = re.split(r"^## +([^\n]+)\n", changelog, flags=re.MULTILINE) + matches = [ + sections[index + 1].strip() + for index in range(1, len(sections), 2) + if re.fullmatch(rf"{re.escape(tag)} - [0-9]{{4}}-[0-9]{{2}}-[0-9]{{2}}", sections[index].strip()) + ] + if len(matches) != 1 or not matches[0]: + raise SystemExit(f"CHANGELOG.md must contain exactly one nonempty '## {tag} - YYYY-MM-DD' section") + delimiter = uuid.uuid4().hex + with open(os.environ["GITHUB_OUTPUT"], "a") as output: + output.write(f"notes<<{delimiter}\n{matches[0]}\n{delimiter}\n") + PY + + publish: + name: Publish to crates.io + needs: validate-source + runs-on: ubuntu-latest + environment: crates-io + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@stable + - name: Validate release tag + env: + RELEASE_TAG: ${{ github.ref_name }} + run: | + python3 - <<'PY' + import os + import re + import tomllib + from pathlib import Path + + tag = os.environ["RELEASE_TAG"] + if not re.fullmatch(r"(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)", tag): + raise SystemExit(f"Expected a bare MAJOR.MINOR.PATCH tag, got {tag!r}") + package = tomllib.loads(Path("Cargo.toml").read_text())["package"] + if tag != package["version"]: + raise SystemExit(f"Tag {tag!r} does not match Cargo.toml version {package['version']!r}") + PY + - name: Verify package + run: cargo publish --locked --dry-run + - name: Authenticate with crates.io + id: auth + uses: rust-lang/crates-io-auth-action@v1 + - name: Publish crate + env: + CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} + run: cargo publish --locked + + github-release: + name: Create GitHub release + needs: [validate-source, publish] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + # GitHub automatically attaches source archives (.zip and .tar.gz). + - name: Create release with changelog notes + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + RELEASE_TAG: ${{ github.ref_name }} + RELEASE_NOTES: ${{ needs.validate-source.outputs.release-notes }} + run: | + if ! gh release view "$RELEASE_TAG" > /dev/null 2>&1; then + gh release create "$RELEASE_TAG" --verify-tag --title "$RELEASE_TAG" --notes "$RELEASE_NOTES" + fi diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5cb87c1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 0.2.0 - 2026-09-09 + +- Initial release of lib-patcher, a tool for filtering symbols in Rust static libraries to prevent linking conflicts. diff --git a/Cargo.toml b/Cargo.toml index 8cb0251..5d3c049 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,19 @@ [package] name = "lib-patcher" version = "0.2.0" -publish = false edition = "2024" authors = ["Stephan Eckes "] description = "Cross-platform symbol filtering for Rust static libraries to prevent linking conflicts" readme = "README.md" repository = "https://github.com/ai-coustics/lib-patcher" license = "MIT OR Apache-2.0" -keywords = ["static-library", "linking", "symbol-filtering", "build-tools", "ffi"] +keywords = [ + "static-library", + "linking", + "symbol-filtering", + "build-tools", + "ffi", +] categories = ["development-tools::build-utils", "development-tools::ffi"] [dependencies] diff --git a/README.md b/README.md index 5b98a5a..1b9f78b 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ error: duplicate symbol: serde_json::ser::indent #### Install ```sh -cargo install --git ssh://git@github.com/ai-coustics/lib-patcher.git lib-patcher +cargo install lib-patcher ``` #### Basic Usage @@ -76,7 +76,20 @@ lib-patcher --input libmylib.a --list ### As a Rust Library (build.rs) -Use it programmatically in your `build.rs`: +Add `lib-patcher` as a build dependency to your project: + +```sh +cargo add --build lib-patcher +``` + +Use it in your `build.rs` to patch an **already compiled external static library** +before linking it into your crate. In the example below, `vendor/libthirdparty.a` +must already exist. + +This does **not** patch the library produced by the current crate: Cargo runs +`build.rs` before compiling that crate, so its output library is not available yet. +To patch your own crate's static library, first build it, then use the +[CLI tool](#as-a-cli-tool-post-build) as a separate post-build step. ```rust use lib_patcher::patch_lib; diff --git a/src/android.rs b/src/android.rs index 401415a..b197d1f 100644 --- a/src/android.rs +++ b/src/android.rs @@ -1,7 +1,7 @@ use std::env; use std::path::{Path, PathBuf}; -use crate::elf::{patch_elf, ElfToolchain}; +use crate::elf::{ElfToolchain, patch_elf}; /// Android implementation: ELF symbol filtering using the Android NDK LLVM tools. /// @@ -33,7 +33,14 @@ pub(crate) fn patch_android( label: "Android", }; - patch_elf(static_lib, out_dir, lib_name, keep_prefix, final_lib, &tools); + patch_elf( + static_lib, + out_dir, + lib_name, + keep_prefix, + final_lib, + &tools, + ); } struct AndroidToolchain { diff --git a/src/linux.rs b/src/linux.rs index 3e9244b..8f97dbe 100644 --- a/src/linux.rs +++ b/src/linux.rs @@ -1,7 +1,7 @@ use std::path::{Path, PathBuf}; use std::process::Command; -use crate::elf::{patch_elf, ElfToolchain}; +use crate::elf::{ElfToolchain, patch_elf}; /// Linux implementation: uses readelf + objcopy (the shared ELF routine) to /// localize symbols, resolving native or GNU cross tools for the target arch. @@ -27,7 +27,14 @@ pub(crate) fn patch_linux( label: "Linux", }; - patch_elf(static_lib, out_dir, lib_name, keep_prefix, final_lib, &tools); + patch_elf( + static_lib, + out_dir, + lib_name, + keep_prefix, + final_lib, + &tools, + ); } /// Maps a target architecture to its GNU cross-toolchain triplet prefix