From 7407d1be726907f54f1e8afe2c8204731df6b962 Mon Sep 17 00:00:00 2001 From: steckes Date: Wed, 9 Sep 2026 11:27:08 +0200 Subject: [PATCH 1/9] Allow crate publishing --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 8cb0251..7ec206f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,6 @@ [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" From ef49ac455004958fbc842880efad31e111dc6973 Mon Sep 17 00:00:00 2001 From: steckes Date: Wed, 9 Sep 2026 11:27:18 +0200 Subject: [PATCH 2/9] Update installation docs --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5b98a5a..4867d25 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,13 @@ lib-patcher --input libmylib.a --list ### As a Rust Library (build.rs) -Use it programmatically in your `build.rs`: +Add `lib-patchet` as a build dependency to your project: + +```sh +cargo add --build lib-patcher +``` + +Then use it programmatically in your `build.rs`: ```rust use lib_patcher::patch_lib; From a8603ac8a06e7ba06dd2d35746441dacd08ee012 Mon Sep 17 00:00:00 2001 From: steckes Date: Wed, 9 Sep 2026 11:27:23 +0200 Subject: [PATCH 3/9] taplo fmt --- Cargo.toml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 7ec206f..5d3c049 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,13 @@ description = "Cross-platform symbol filtering for Rust static libraries to prev 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] From 27eac565b2d78bf0f5312bc7e960cd0040708c5e Mon Sep 17 00:00:00 2001 From: steckes Date: Wed, 9 Sep 2026 11:27:39 +0200 Subject: [PATCH 4/9] cargo fmt --- src/android.rs | 11 +++++++++-- src/linux.rs | 11 +++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) 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 From 047ba88018c257e57e3cd41c7a50564a7a57a92e Mon Sep 17 00:00:00 2001 From: steckes Date: Wed, 9 Sep 2026 11:31:42 +0200 Subject: [PATCH 5/9] add quality worflow --- .github/workflows/quality.yml | 96 +++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 .github/workflows/quality.yml diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..845b575 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,96 @@ +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 and the shared cache 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 + # Formatting does not compile anything, so no cache is needed. + - 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 + - uses: Swatinem/rust-cache@v2 + with: + workspaces: ${{ env.QUALITY_WORKSPACES }} + shared-key: quality + # Only Clippy on main writes the shared cache. + save-if: ${{ github.ref == 'refs/heads/main' }} + - 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 + - uses: Swatinem/rust-cache@v2 + with: + workspaces: ${{ env.QUALITY_WORKSPACES }} + shared-key: quality + save-if: false + - 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 From d2a6832ed7b26349da5b30709d71232a1bdcfe1f Mon Sep 17 00:00:00 2001 From: steckes Date: Wed, 9 Sep 2026 11:43:44 +0200 Subject: [PATCH 6/9] add release action --- .github/workflows/release.yml | 114 ++++++++++++++++++++++++++++++++++ CHANGELOG.md | 5 ++ 2 files changed, 119 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100644 CHANGELOG.md 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. From 33b2b808a4c4dba94bc05af387130a883753e28d Mon Sep 17 00:00:00 2001 From: Stephan Eckes Date: Wed, 9 Sep 2026 12:56:58 +0200 Subject: [PATCH 7/9] Update README.md Co-authored-by: Matthias Geier --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4867d25..a3b6f07 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ lib-patcher --input libmylib.a --list ### As a Rust Library (build.rs) -Add `lib-patchet` as a build dependency to your project: +Add `lib-patcher` as a build dependency to your project: ```sh cargo add --build lib-patcher From 39798cffd0dd9fd69f8b0eb3bc5bf47aaef8b336 Mon Sep 17 00:00:00 2001 From: steckes Date: Wed, 9 Sep 2026 13:15:17 +0200 Subject: [PATCH 8/9] remove cache from quality workflow --- .github/workflows/quality.yml | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 845b575..a3205da 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -16,7 +16,7 @@ concurrency: env: CARGO_TERM_COLOR: always - # Keep every multi-workspace check and the shared cache in sync. + # Keep every multi-workspace check in sync. QUALITY_WORKSPACES: | . tests/testlib @@ -31,7 +31,6 @@ jobs: - uses: dtolnay/rust-toolchain@stable with: components: rustfmt - # Formatting does not compile anything, so no cache is needed. - run: | for workspace in $QUALITY_WORKSPACES; do cargo fmt --manifest-path "$workspace/Cargo.toml" --all -- --check @@ -53,12 +52,6 @@ jobs: - uses: dtolnay/rust-toolchain@stable with: components: clippy - - uses: Swatinem/rust-cache@v2 - with: - workspaces: ${{ env.QUALITY_WORKSPACES }} - shared-key: quality - # Only Clippy on main writes the shared cache. - save-if: ${{ github.ref == 'refs/heads/main' }} - run: | for workspace in $QUALITY_WORKSPACES; do cargo clippy --manifest-path "$workspace/Cargo.toml" --workspace --all-targets --all-features -- -D warnings @@ -70,11 +63,6 @@ jobs: steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - with: - workspaces: ${{ env.QUALITY_WORKSPACES }} - shared-key: quality - save-if: false - env: RUSTDOCFLAGS: -D warnings run: | From 3c3c813242cbae8b07e2d6f7d23107823d0927a4 Mon Sep 17 00:00:00 2001 From: steckes Date: Wed, 9 Sep 2026 13:15:51 +0200 Subject: [PATCH 9/9] explain when lib-patcher can be used as build dependecy --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a3b6f07..1b9f78b 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,14 @@ Add `lib-patcher` as a build dependency to your project: cargo add --build lib-patcher ``` -Then use it programmatically in your `build.rs`: +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;