pax_global_header00006660000000000000000000000064152717772030014525gustar00rootroot0000000000000052 comment=14b3b52bdc1beaabcf91410b2f3b49eddc23bcec rizsotto-Bear-0c30b27/000077500000000000000000000000001527177720300146135ustar00rootroot00000000000000rizsotto-Bear-0c30b27/.claude/000077500000000000000000000000001527177720300161265ustar00rootroot00000000000000rizsotto-Bear-0c30b27/.claude/rules/000077500000000000000000000000001527177720300172605ustar00rootroot00000000000000rizsotto-Bear-0c30b27/.claude/rules/rust.md000066400000000000000000000045521527177720300206050ustar00rootroot00000000000000--- paths: - "**/*.rs" --- # Rust house style Conventions for Rust code across every workspace crate. The per-crate `CLAUDE.md` files add only crate-specific constraints on top of these. - **Edition**: Rust 2024 idioms (`edition = "2024"` in `Cargo.toml`). - **File header**: every `.rs` file starts with `// SPDX-License-Identifier: GPL-3.0-or-later` as its first line, before any `//!` module doc. Bear is GPL-3.0-or-later (see `COPYING`). - **Error handling**: domain errors are `thiserror` enums in the library code; the binaries (`crates/bear-driver/src/`, `crates/bear-wrapper/src/`) use `anyhow::Result` with `.context(...)` at the boundary and turn any `Err` into a non-zero exit. - **Panicking macros**: `unwrap()` is for test code only. In production use `.expect("short reason")` only when a prior-stage invariant makes the `None`/`Err` structurally impossible, and name that invariant in the string; propagate everything else with `?`. `panic!` / `unreachable!` are for unambiguous programmer bugs (violated API contract, malformed generated data), with a one-line comment stating the invariant. - **Module structure**: modules are organised into directories by responsibility (`output/`, `config/`, `modes/`, `interpreters/`), plus a few top-level modules. Extend an existing module before adding one; keep each module's public surface as small as the crate needs. - **Abstraction**: introduce a trait only for a real polymorphism seam with a second implementation in sight. No speculative abstractions. - **Logging**: use the `log` crate via qualified `log::level!(...)` calls, initialised once with `env_logger` at the binary entry. Levels: `info` for startup/config summaries; `debug` for per-event trace; `warn` for recoverable anomalies; `error` for non-fatal failures that are logged and swallowed. No `trace!`. - **Platform gating**: split OSes with `#[cfg(target_os = "macos")]` / `#[cfg(not(target_os = "macos"))]` / `#[cfg(unix)]`; gate optional capabilities with the build-probed `#[cfg(has_symbol_X)]` (target) / `#[cfg(has_executable_X)]` (host tool) keys, never on `target_os = "linux"`. - **Lint suppressions**: only `#[allow(...)]` when there is no better fix, always with `reason = "..."` inside the attribute. - **Comments** explain *why*, not *what*; default to none unless a subtle invariant would otherwise need re-deriving. rizsotto-Bear-0c30b27/.claude/rules/testing.md000066400000000000000000000037451527177720300212700ustar00rootroot00000000000000--- paths: - "**/*.rs" --- # Test conventions Where tests live and how to write them. - **Placement**: unit tests live next to the code they exercise (`#[cfg(test)] mod tests` at the bottom of the `.rs` file); behavioural contracts get integration tests under `tests/integration/`, each tagged `// Requirements: ` (see `tests/integration/CLAUDE.md` for the linkage rules and the test harness). A unit test that protects a requirement carries the same tag. ## Style - **Name the subject under test `sut`.** Whatever the test exercises - the function result, the struct instance, the parsed value - binds to a variable named `sut`, so every test reads the same way. - **Separate prepare from execute and verify.** Structure each test as arrange / act / assert: set up inputs first, then produce `sut`, then assert on it. Keep the three phases visually distinct (blank lines, or `// arrange` / `// act` / `// assert` comments when it aids reading). - **Drive input-only variations from a table.** When several tests differ only in their inputs and expected outputs, do not copy-paste the test body. Collect `(input, expected)` tuples in a list and loop over them, asserting once per case. Include the case input in the assertion message so a failure says which row failed. - **Prefer value factories.** Build test inputs and expected values with small named helper functions rather than inline literals, so each case reads as intent ("a relative source file in a subdir") rather than as raw data. - **Fixtures.** When three or more tests in a file share on-disk scaffolding (temp dirs, fake binaries, wired-up structs), extract a `mod fixture` builder struct inside the `#[cfg(test)]` block that owns every `TempDir` so paths stay valid for the test's lifetime. - **Mock doubles.** Annotate a trait that needs a test double with `#[cfg_attr(test, mockall::automock)]` on the trait definition; hand-write a mock only for a composed trait that `automock` cannot express. rizsotto-Bear-0c30b27/.claude/skills/000077500000000000000000000000001527177720300174275ustar00rootroot00000000000000rizsotto-Bear-0c30b27/.claude/skills/release/000077500000000000000000000000001527177720300210475ustar00rootroot00000000000000rizsotto-Bear-0c30b27/.claude/skills/release/SKILL.md000066400000000000000000000172041527177720300222530ustar00rootroot00000000000000--- name: release description: Cut a release of Bear. Verify metadata, run the full check set, fast-forward to master, tag, and publish a GitHub release plus discussion announcement. Use when the user asks to release, cut a version, tag a release, or ship a new Bear version. --- # Release Bear This skill captures the release process. The goal is that running through it mechanically produces a tagged, published release with consistent metadata. Replace `X.Y.Z` with the version being released (e.g. `4.1.2`). The release branch is `next`; the destination branch is `master`. Pushing the tag and publishing the GitHub release are outward-facing and hard to undo. Drive the steps below and confirm with the user before any outward-facing action (the fast-forward to master, pushing the tag, creating the release, posting the announcement). ## 1. Preconditions - All work for the release is on `next`. - Local `next` matches `origin/next`, so the CI result covers what gets merged: ```sh git checkout next git fetch origin git status # must be up to date with origin/next ``` If you have local commits, push them first and wait for CI. - `master` has no commits `next` is missing: ```sh git log --oneline next..origin/master # must be empty ``` `master` only moves by fast-forward from `next`, so a non-empty result means someone pushed to `master` directly. Stop and ask the user. Do not rebase or force-push a shared branch on your own. If the user agrees, merging `master` into `next` fixes it without rewriting history. - Latest CI run on `origin/next` is green: ```sh gh run list --branch next --limit 1 ``` ## 2. Pre-flight checklist Run the checks in root `CLAUDE.md` (Workflow step 6) from the repo root on `next`. All must pass before tagging. Then verify the release metadata: | Item | Where | Check | |---|---|---| | Workspace version | `Cargo.toml` (`[workspace.package]`) | matches `X.Y.Z` | | Lockfile in sync | `Cargo.lock` | `bear`, `compilers-codegen`, etc. show `X.Y.Z`; clean `cargo build` does not modify it | | Man page date | `man/bear.1.md` line 3 | set to today (e.g. `April 25, 2026`) | | Man page generated | `man/bear.1` | regenerated from `.md` (`man/CLAUDE.md`) | If the workspace version does not match, commit `chore(release): bump version to X.Y.Z` on `next` with the refreshed `Cargo.lock`, push, and let CI run. If the man page date or content changed, regenerate `bear.1` as `man/CLAUDE.md` describes. Commit any pre-flight fixups to `next` and let CI run again. ## 3. Merge to master Use a fast-forward merge to keep `master`'s first-parent history linear, which is the convention the 4.x series follows. ```sh git checkout master git pull --ff-only origin master git merge --ff-only next ``` Do not create a merge commit. If `--ff-only` fails, `master` has commits `next` lacks. Stop and ask the user, as in the preconditions. Do not rebase or force-push `next` on your own. ## 4. Tag Tags are unprefixed (`4.1.2`, not `v4.1.2`), annotated, and SSH-signed. The message body uses the `v`-prefixed form. Verify your signing config once: ```sh git config --get gpg.format # ssh git config --get user.signingkey # path or key starting with "key::" git config --get tag.gpgsign # true (recommended) ``` Then tag the merge tip: ```sh git tag -s X.Y.Z -m "vX.Y.Z" git tag --verify X.Y.Z # confirm signature is good ``` ## 5. Push ```sh git push origin master git push origin X.Y.Z ``` ## 6. Draft release notes Use prior releases as the template (`gh release view 4.1.1`). Sections, in order, when applicable: - `### Features` - `### Bug Fixes` - `### Performance` - `### Internal Refactoring` - `### Documentation` - `### Closed Issues` - bullet list of `#NNN - one-line description` - `### Thanks` - issue reporters and external PR authors, by `@handle`; plus the downstream packagers currently shipping Bear, by name or `@handle` where known (see the repology command below). Packagers are chronically invisible volunteers - name them every release. - `### New Contributors` - first-time contributors only Commit subjects are not enough on their own. A `refactor:` or `test:` commit can still change something a user sees, typically the wording of a diagnostic or the shape of a log line, and its subject will not say so. Read the bodies, and read any rationale entry added since the previous tag: those record accepted user-visible costs in their Consequences section. Anything a user could have scripted against belongs in the notes even when no feature changed. Trailer: ``` **Full Changelog**: https://github.com/rizsotto/Bear/compare/PREV...X.Y.Z ``` Useful inputs while drafting: ```sh # commit subjects since the previous tag git log --pretty="%s" PREV..X.Y.Z # rationale entries added since the previous tag; their Consequences # sections name the user-visible deltas the subjects above will not git log --diff-filter=A --name-only --pretty=format: PREV..X.Y.Z -- docs/rationale/ \ ':!docs/rationale/CLAUDE.md' ':!docs/rationale/TEMPLATE.md' # external contributors since the previous tag git log --pretty="%an <%ae>" PREV..X.Y.Z | sort -u # issues closed since the previous tag gh issue list --state closed --search "closed:>=YYYY-MM-DD" --limit 50 # PRs merged since the previous tag gh pr list --state merged --search "merged:>=YYYY-MM-DD" --limit 50 # downstream packagers shipping the latest Bear (repology needs a User-Agent; # "newest" entries are the repos that kept up with the previous release) curl -s -A 'Bear-release-notes' 'https://repology.org/api/v1/project/bear-clang' | python3 -c "import json,sys; [print(p['repo'], p['version'], ', '.join(p.get('maintainers',[]))) for p in json.load(sys.stdin) if p.get('status')=='newest']" ``` Save the notes to a temporary file (e.g. `/tmp/release-notes.md`) - they are reused for the GitHub release and the discussion announcement. ## 7. Publish the GitHub release ```sh gh release create X.Y.Z \ --title X.Y.Z \ --notes-file /tmp/release-notes.md \ --verify-tag ``` Use `--draft` first if you want to review the rendered output before it goes live, then `gh release edit X.Y.Z --draft=false`. ## 8. Announce on the discussions thread The pinned thread for release announcements is . Keep the announcement short: - One-line summary linking to the release page. - Three to five highlight bullets. - Thanks to issue reporters and external contributors by `@handle`. - Invite users to open new issues for problems; the discussion thread is for general feedback. Post with: ```sh gh api graphql -f query=' mutation($id: ID!, $body: String!) { addDiscussionComment(input: {discussionId: $id, body: $body}) { comment { url } } }' -f id=DISCUSSION_NODE_ID -f body="$(cat /tmp/announcement.md)" ``` (Get `DISCUSSION_NODE_ID` once via `gh api graphql -f query='{ repository(owner:"rizsotto",name:"Bear") { discussion(number:399) { id } } }'`.) Alternatively, paste through the web UI. ## 9. Post-release - Pin the announcement comment in discussion #399 if you typically do. - Verify the release page renders correctly and the tag signature is shown. - Notify downstream packagers if the release contains packaging-relevant changes (install layout, prerequisites, breaking flags). Past channels: Homebrew, Arch, Debian/Fedora maintainers. ## Conventions reference | Item | Convention | |---|---| | Release branch name | `next` | | Tag name | `X.Y.Z` (no `v` prefix) | | Tag type | annotated, SSH-signed | | Tag message | `vX.Y.Z` | | GitHub release title | `X.Y.Z` | | Merge style | fast-forward only | | Version source of truth | `Cargo.toml` `[workspace.package].version` | rizsotto-Bear-0c30b27/.claude/skills/requirements/000077500000000000000000000000001527177720300221525ustar00rootroot00000000000000rizsotto-Bear-0c30b27/.claude/skills/requirements/SKILL.md000066400000000000000000000105341527177720300233550ustar00rootroot00000000000000--- name: requirements description: Write, modify, or review a requirement file under docs/requirements. Pick the single owning file, keep the text contract-only, name IDs so they need no explanation, and verify cross-references and test coverage. Use when the user asks to add, change, audit, or review a requirement, or when the workflow calls for writing one. --- # Write and maintain requirement files The rules are in `docs/requirements/CLAUDE.md`; read it first. A new file fills `docs/requirements/TEMPLATE.md`. This skill is the method. Use it to write a new requirement, edit one, or audit the directory. ## 1. Find the owner before writing a word Every behavior has exactly one owning requirement file. Before adding any passage, find where it belongs: - `ls docs/requirements/` and grep the directory for the key nouns of the behavior. Read every file that hits. - An owner exists: extend it. No owner: create a new file. - The new content is one instance of a contract other files also instantiate (another recognized toolchain, another output filter): extend the shared owner, usually with a table row. Do not create a per-instance file. A new file that would restate an existing contract with the names changed is a signal to merge, not to add. - Two files each state half of one contract (one says "exits non-zero when X", another says "X never affects the exit code"): that is a latent contradiction. Pick or create one owner, state the full contract there organized around the principle that resolves the halves, and reduce the other files to one-line deferrals. When another file needs to mention the behavior, it gets a one-line pointer naming the owner ("owned by `output-duplicate-detection`"), never a re-narration. If a re-narration can drift, it will. ## 2. Name the ID so it needs no explanation - Take the prefix from the existing filenames. Do not invent a new prefix for one file. Pick the prefix by what the content is about, not by which crate implements it. - Place the new ID next to the most similar existing ID. If a reader needs prose to tell them apart, rename until the names alone carry the distinction. Disambiguation paragraphs are a naming bug. - Renaming or merging an ID is one atomic change: update every `Requirements:` tag in tests (the lookup grep is in `tests/integration/CLAUDE.md`) and every cross-reference in `docs/`. ## 3. Keep the body contract-only For every sentence ask: is this what the user can expect (keep), why we chose it (rationale entry), or how it is built (code comment)? Beyond the list in `docs/requirements/CLAUDE.md`, strike: - Rust API and type names standing in for behavior (`Path::canonicalize`, crate names, function names). State the observable behavior instead. - Internal file names as actors in acceptance criteria (a YAML interpreter definition, a module path). - Buffering or performance speculation dressed as a constraint. - History: what the code used to do, fixed-bug narratives, unimplemented proposals, config keys that never shipped. A regression scenario that cites an issue number is contract, not history, and stays. - Contracts phrased as a diff against the past ("same as before this feature"). State the behavior absolutely. An Intent that describes the pre-fix bug in present tense turns false once the fix ships. - Copies of external specifications. Link them. ## 4. Verify every cross-reference, gloss included A link is two claims: the target exists, and the gloss describes it. The script below checks the first. For the second, read the target and confirm the sentence around the link matches what that file owns. A gloss describing env-derived flags that links the wrapper mechanism is worse than no link. ## 5. Verify before finishing ```sh ./scripts/check-requirements-coverage.sh ./scripts/check-requirements-references.sh ``` Both must exit 0. The `REVIEW` lines are prompts, not verdicts. A hit in Intent or acceptance criteria almost always needs a fix. Then read the touched file once more against steps 1 to 4 and `TEMPLATE.md`. ## Scope of a maintenance edit When editing an existing file, apply the checks to the passages the change touches. Fix genre violations you are already rewriting; note violations elsewhere in the file and propose a separate cleanup rather than growing the change. A deliberate directory-wide sweep is its own task with its own commit series. rizsotto-Bear-0c30b27/.clippy.toml000066400000000000000000000001301527177720300170600ustar00rootroot00000000000000# https://doc.rust-lang.org/clippy/lint_configuration.html allow-unwrap-in-tests = true rizsotto-Bear-0c30b27/.deny.toml000066400000000000000000000046721527177720300165360ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Written for cargo-deny 0.20.2. cargo-deny rejects unknown keys, and keys # change between releases, so CI pins this version (CARGO_DENY_VERSION in # build_rust.yml and codeql.yml). Bump all three together. # Every dependency is statically linked into GPL-3.0-or-later binaries, # so each one has to carry a GPL-3.0-compatible license. CI gates on # this because the alternative is finding out from a distribution # packager, after release, that the linked result cannot be shipped. # # The list is deliberately minimal: each entry is load-bearing for the # current tree, and cargo-deny reports any allowance nothing matches, so # a stale entry does not sit here unnoticed. A dependency that needs a # new entry is a decision worth making explicitly rather than a default # worth inheriting. [licenses] allow = [ "MIT", "Apache-2.0", # Both of these appear only inside AND expressions, where the # permissive alternative cannot satisfy the requirement on its own: # encoding_rs is "(Apache-2.0 OR MIT) AND BSD-3-Clause", and # unicode-ident is "(MIT OR Apache-2.0) AND Unicode-3.0". "BSD-3-Clause", "Unicode-3.0", # Bear's own crates. They are part of the graph cargo-deny walks, # published or not. "GPL-3.0-or-later", ] # Stated rather than inherited. These three carry the whole weight of the # advisory gate, and two of cargo-deny's defaults are looser than what # this job enforced when it ran cargo-audit: unsound defaults to the # workspace only, and yanked to a warning. Pinning them keeps a # cargo-deny upgrade from quietly relaxing the gate. [advisories] unmaintained = "all" unsound = "all" yanked = "deny" # A second version of a crate costs build time and doubles what has to be # trusted, so a new one is a decision, not a side effect of a bump. Dev-only # crates count too: they are built on every test run. [bans] multiple-versions = "deny" multiple-versions-include-dev = true skip = [ { crate = "syn:^2", reason = "mockall_derive (dev-only) has not moved to syn 3" }, ] # A "*" requirement takes whatever version comes next. Path dependencies # between Bear's own crates have no version; they pass because no crate in # the workspace is published. wildcards = "deny" allow-wildcard-paths = true # crates.io only: a git or alternative-registry dependency bypasses the # review that a published, versioned crate gets. [sources] unknown-registry = "deny" unknown-git = "deny" rizsotto-Bear-0c30b27/.devcontainer/000077500000000000000000000000001527177720300173525ustar00rootroot00000000000000rizsotto-Bear-0c30b27/.devcontainer/Dockerfile000066400000000000000000000032261527177720300213470ustar00rootroot00000000000000FROM fedora:44 ENV RUSTUP_HOME=/root/.rustup \ CARGO_HOME=/root/.cargo \ PATH=/root/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin # time/bc/file/which: small diagnostics used by agent-driven measurement # and inspection sessions (GNU time and bc are not in the base image). # # mdbook is pinned (--locked) to build the docs site under site/. Keep this # version identical to the one the Pages CI workflow installs. # # typos is pinned the same way; keep it identical to the version the rust CI # workflow installs. It runs as a pre-commit check. # # beads (bd) is pinned to a release tarball, checked against its sha256. # It tracks local development work; GitHub issues stay for user reports. # BD_DISABLE_METRICS keeps its usage metrics (on by default) switched off. ENV BD_DISABLE_METRICS=1 ARG BEADS_VERSION=1.3.1 ARG BEADS_SHA256=3219443a9734b89b93fb16ee8d65844759fa1b3cd3cf139c606b7353cfb0715c RUN dnf -y install curl ca-certificates git gh pandoc time bc file which \ && dnf -y group install c-development \ && dnf -y clean all \ && rm -rf /var/cache/dnf \ && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile default \ && curl -fsSL https://claude.ai/install.sh | sh \ && cargo install mdbook --version 0.5.4 --locked \ && cargo install typos-cli --version 1.51.1 --locked \ && curl -fsSLo /tmp/beads.tar.gz https://github.com/gastownhall/beads/releases/download/v${BEADS_VERSION}/beads_${BEADS_VERSION}_linux_amd64.tar.gz \ && echo "${BEADS_SHA256} /tmp/beads.tar.gz" | sha256sum -c - \ && tar -xzf /tmp/beads.tar.gz -C /usr/local/bin bd \ && rm /tmp/beads.tar.gz rizsotto-Bear-0c30b27/.devcontainer/devcontainer.json000066400000000000000000000074461527177720300227410ustar00rootroot00000000000000// For format details, see https://aka.ms/devcontainer.json. For config options, see the // README at: https://github.com/devcontainers/templates/tree/main/src/rust { "name": "Rust Dev Container (${localWorkspaceFolderBasename})", "build": { "dockerfile": "Dockerfile", }, // Give the container a writable, delegated cgroup v2 subtree. By default // rootless Podman mounts /sys/fs/cgroup read-only, so a process cannot create // a child cgroup -- which the cgroup-based process-tree teardown path // (interception-signal-forwarding) needs in order to be developed and tested. // // --systemd=always makes Podman mount the container's own cgroup read-write // inside our private cgroup namespace (the cpu/io/memory/pids controllers are // already delegated by the kernel). Host prerequisite for rootless Podman: // controller delegation for the user session, e.g. // /etc/systemd/system/user@.service.d/delegate.conf: // [Service] // Delegate=cpu cpuset io memory pids // (usually already in place -- the controllers above being visible confirms // it). Podman-specific flag. // // After changing this, rebuild the container and verify with: // findmnt /sys/fs/cgroup # should report rw // mkdir "/sys/fs/cgroup$(awk -F: '/^0::/{print $3}' /proc/self/cgroup)/x" "runArgs": ["--systemd=always"], // Override the automatic workspace mount to add the :z SELinux relabel option. // This is required on Fedora/RHEL hosts where SELinux is enforcing, otherwise // the bind-mounted directory is inaccessible inside the container. // ${localWorkspaceFolderBasename} expands to the project directory name, so // this file stays generic across Rust projects. "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", "workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/${localWorkspaceFolderBasename},type=bind,z=true", "mounts": [ // Persist the cargo registry (downloaded crate cache) across container // rebuilds. Targets the registry subdir of CARGO_HOME (/root/.cargo, set in // the Dockerfile) rather than /root/.cargo itself, so the volume does not // shadow the rustc/cargo binaries installed into /root/.cargo/bin at build. "source=devcontainer-cargo-cache-${devcontainerId},target=/root/.cargo/registry,type=volume", // Forward the SSH agent socket from the host so SSH keys are available // inside the container for commit signing and remote operations. // Requires the SSH agent to be running on the host: eval $(ssh-agent) && ssh-add "source=${localEnv:SSH_AUTH_SOCK},target=/ssh-agent,type=bind", "source=${localEnv:HOME}/.ssh,target=/root/.ssh,type=bind,readonly=true,z=true", // Mount the host gitconfig (read-only) so git user identity, signing key, // and gpg/ssh signing settings are inherited inside the container. "source=${localEnv:HOME}/.gitconfig,target=/root/.gitconfig,type=bind,readonly=true,z=true", // Mount the host ~/.claude (read-write) so Claude credentials, settings, // rules, and skills are shared with the container. Writable so refreshed // auth tokens and session state persist back to the host, avoiding repeated // re-authentication across container rebuilds. "source=${localEnv:HOME}/.claude,target=/root/.claude,type=bind,z=true", ], // Point git at the forwarded SSH agent socket. "remoteEnv": { "SSH_AUTH_SOCK": "/ssh-agent", }, // In rootless Podman on Fedora/RHEL, container uid=0 (root) maps to the host // user's uid (laca, uid=1000). Running as root inside the container is therefore // safe and ensures the mounted project files are writable. "remoteUser": "root", "containerUser": "root", // Verify the Rust toolchain is available after the container is created. "postCreateCommand": "rustc --version && cargo --version", } rizsotto-Bear-0c30b27/.gitattributes000066400000000000000000000004761527177720300175150ustar00rootroot00000000000000# Rust code *.rs text eol=lf *.toml text eol=lf *.lock text eol=lf # Misc scripting files Makefile text eol=lf *.mk text eol=lf *.sh text eol=lf # Human-readable files *.md eol=lf README.* text eol=lf COPYING text eol=lf *.txt text eol=lf # Git configuration files .gitignore text eol=lf .gitattributes text eol=lf rizsotto-Bear-0c30b27/.github/000077500000000000000000000000001527177720300161535ustar00rootroot00000000000000rizsotto-Bear-0c30b27/.github/FUNDING.yml000066400000000000000000000000231527177720300177630ustar00rootroot00000000000000github: [rizsotto] rizsotto-Bear-0c30b27/.github/ISSUE_TEMPLATE/000077500000000000000000000000001527177720300203365ustar00rootroot00000000000000rizsotto-Bear-0c30b27/.github/ISSUE_TEMPLATE/bug_report.yml000066400000000000000000000126661527177720300232440ustar00rootroot00000000000000name: Bug report description: Report a problem with Bear's interception, output, or behavior. title: "[Bug]: " labels: [] body: - type: markdown attributes: value: | Thanks for taking the time to file a bug report. The more concrete detail you give, the faster the issue can be diagnosed. For general questions or feature ideas, please use [Discussions](https://github.com/rizsotto/Bear/discussions) instead. - type: checkboxes id: preflight attributes: label: Pre-flight checklist options: - label: I have read the `README.md` and `man bear`. required: true - label: I have searched existing issues (open and closed). required: true - label: I have confirmed the build works without Bear. required: true - type: textarea id: description attributes: label: Describe the bug description: A clear and concise description of what the bug is. placeholder: e.g. Bear produces an empty compile_commands.json when ... validations: required: true - type: textarea id: command attributes: label: Exact Bear command description: The full command you ran, including the build invocation after `--`. placeholder: bear -- make -j8 render: shell validations: required: true - type: textarea id: reproduce attributes: label: Steps to reproduce description: Minimal steps to reproduce, including any setup. If possible, link to a public repository that demonstrates the issue. placeholder: | 1. Clone https://github.com/... 2. Run `bear -- make` 3. ... validations: required: true - type: textarea id: expected attributes: label: Expected behavior description: What did you expect to happen? If the issue is about a missing or incorrect entry in `compile_commands.json`, paste the entry you expected. render: shell validations: required: true - type: textarea id: actual attributes: label: Actual behavior description: What actually happened? Paste the relevant output, the offending `compile_commands.json` entry (or note its absence), or the error you saw. render: shell validations: required: true - type: textarea id: debug-log attributes: label: Debug log description: Output of re-running with debug logs enabled, e.g. `RUST_LOG=debug bear -- make`. Paste inline or attach as a file. Strongly encouraged - most bugs cannot be diagnosed without it. render: shell - type: input id: bear-version attributes: label: Bear version description: Output of `bear --version`. placeholder: "4.1.3" validations: required: true - type: dropdown id: install-method attributes: label: Install method options: - Built from source - Distro package (apt, dnf, pacman, etc.) - Homebrew - MacPorts - Other validations: required: true - type: input id: install-detail attributes: label: Install method detail description: If a distro or third-party package, name it (e.g. `apt on Debian 12`, `Homebrew 4.x`, `aur/bear-git`). Leave blank if built from source. placeholder: e.g. apt on Debian 12 stable - type: dropdown id: os attributes: label: Operating system options: - Linux - macOS - FreeBSD - OpenBSD - NetBSD - Other validations: required: true - type: input id: os-version attributes: label: OS version description: e.g. `Ubuntu 24.04`, `macOS 14.5`, `FreeBSD 14.0`. Output of `uname -sr` is fine. placeholder: Ubuntu 24.04 validations: required: true - type: dropdown id: arch attributes: label: Architecture description: Output of `uname -m`. options: - x86_64 - aarch64 - arm64 - armv7 - i686 - riscv64 - ppc64le - Other validations: required: true - type: dropdown id: build-tool attributes: label: Build tool multiple: true options: - GNU make - CMake - Ninja - Autotools (configure) - Bazel - Meson - Plain shell script - Other validations: required: true - type: input id: compiler attributes: label: Compiler and version description: Which compiler the build invokes, and its version (e.g. `gcc 13.2.0`, `clang 18.1.0`). If multiple, list them. Include any wrapper in use (`ccache`, `distcc`, `mpicc`, `nvcc`, ...). placeholder: clang 18.1.0 validations: required: true - type: input id: cross-compile attributes: label: Cross-compilation target (optional) description: Leave blank if compiling for the host architecture. placeholder: e.g. aarch64-linux-gnu - type: input id: project-link attributes: label: Project reference (optional) description: A link to the source code being built helps reproduction. Leave blank if it is closed source. placeholder: https://github.com/... - type: textarea id: additional attributes: label: Additional context description: Anything else worth knowing - workarounds tried, related issues, environment variables set (e.g. `INTERCEPT_LIBDIR`), unusual toolchain setup, etc. rizsotto-Bear-0c30b27/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000003141527177720300223240ustar00rootroot00000000000000blank_issues_enabled: false contact_links: - name: Questions and discussion url: https://github.com/rizsotto/Bear/discussions about: Ask questions, share ideas, or discuss usage in Discussions. rizsotto-Bear-0c30b27/.github/labels.yml000066400000000000000000000037611527177720300201470ustar00rootroot00000000000000# Source of truth for GitHub issue and PR labels on this repository. # Format consumable by github.com/EndBug/label-sync (and similar actions). # To apply changes: run a label-sync action against this file, or re-create # manually with `gh label create / edit`. # --- Kind (issue type) --- - name: bug color: 'fc2929' - name: enhancement color: '84b6eb' - name: documentation color: '0075ca' description: 'Improvements or fixes to docs (README, man page, comments).' - name: question color: 'cc317c' - name: tracking color: '7057ff' description: 'Project work item tracked by the maintainer (not user-reported).' # --- Disposition --- - name: duplicate color: 'cccccc' - name: invalid color: 'e6e6e6' - name: wontfix color: 'ffffff' # --- Triage and workflow --- - name: needs-repro color: 'c2e0c6' description: 'Cannot be triaged without a fresh reproduction on current code.' - name: need help color: 'fbca04' description: "This is something that I can't do myself." - name: next color: 'c2e0c6' description: 'will work on this in the near future' # --- Area (subsystem) --- - name: 'area:interception' color: '0052cc' description: 'Compiler invocation capture (preload library, wrapper executable).' - name: 'area:semantic' color: '0e8a16' description: 'Command recognition and interpreter logic.' - name: 'area:output' color: '5319e7' description: 'compile_commands.json formatting and writing.' - name: 'area:config' color: '006b75' description: 'User configuration and CLI arguments.' - name: 'area:build' color: 'bf8700' description: 'Build pipeline, platform probes, packaging.' # --- Legacy area-ish labels (kept; revisit during retro) --- - name: static binary color: '684df2' - name: generic output color: '657500' - name: encode color: 'a6cdea' # --- Pull request tags --- - name: dependencies color: '0366d6' description: 'Pull requests that update a dependency file' - name: rust color: '000000' description: 'Pull requests that update rust code' rizsotto-Bear-0c30b27/.github/workflows/000077500000000000000000000000001527177720300202105ustar00rootroot00000000000000rizsotto-Bear-0c30b27/.github/workflows/build_rust.yml000066400000000000000000000104641527177720300231140ustar00rootroot00000000000000name: rust CI on: push: branches: [master, next] pull_request: env: CARGO_TERM_COLOR: always # Keep identical to the typos version the devcontainer installs. TYPOS_VERSION: 1.51.1 # Keep identical to the cargo-deny version in .deny.toml. CARGO_DENY_VERSION: 0.20.2 permissions: contents: read jobs: lint: name: Lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: dtolnay/rust-toolchain@stable with: toolchain: stable components: clippy,rustfmt - run: cargo fmt -- --check - run: cargo clippy --all-targets --locked -- -D warnings compile: name: Compile runs-on: ubuntu-latest needs: [lint] steps: - uses: actions/checkout@v7 - uses: dtolnay/rust-toolchain@stable with: toolchain: stable - run: cargo build --verbose --release --locked test: name: Test strategy: fail-fast: false matrix: os: - ubuntu-latest - windows-latest - macos-latest toolchain: - stable - nightly include: - os: ubuntu-26.04-arm toolchain: stable runs-on: ${{ matrix.os }} needs: [lint] steps: - uses: actions/checkout@v7 - uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ matrix.toolchain }} - name: Install ccache on Ubuntu if: runner.os == 'Linux' run: | sudo apt-get update sudo apt-get install -y ccache - name: Run integration tests run: | cargo build --verbose --locked cargo test --verbose --locked env: RUST_LOG: debug test-musl: name: Test (Alpine, musl) runs-on: ubuntu-latest needs: [lint] container: alpine:latest steps: - name: Install build dependencies run: apk add --no-cache git build-base lld rust cargo ccache - uses: actions/checkout@v7 - name: Run integration tests run: | cargo build --verbose --locked cargo test --verbose --locked env: RUST_LOG: debug compile-android: name: Compile (Android, ${{ matrix.target }}, API ${{ matrix.api }}) strategy: fail-fast: false matrix: include: - target: aarch64-linux-android api: 24 clang: aarch64-linux-android24-clang # posix_spawn and posix_spawnp exist from API 28. - target: aarch64-linux-android api: 28 clang: aarch64-linux-android28-clang - target: armv7-linux-androideabi api: 24 clang: armv7a-linux-androideabi24-clang runs-on: ubuntu-latest needs: [lint] steps: - uses: actions/checkout@v7 - uses: dtolnay/rust-toolchain@stable with: toolchain: stable targets: ${{ matrix.target }} - name: Cross-build with the Android NDK run: | ndk_bin="$ANDROID_NDK_LATEST_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin" target_lower=$(echo "${{ matrix.target }}" | tr '-' '_') target_upper=$(echo "$target_lower" | tr 'a-z' 'A-Z') export "CC_${target_lower}=$ndk_bin/${{ matrix.clang }}" export "AR_${target_lower}=$ndk_bin/llvm-ar" export "CARGO_TARGET_${target_upper}_LINKER=$ndk_bin/${{ matrix.clang }}" cargo build --verbose --release --target ${{ matrix.target }} --locked doc: name: Generate Documentation runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: dtolnay/rust-toolchain@stable with: toolchain: stable - run: cargo doc --no-deps --locked spell-check: name: Spell Check Documentation runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: taiki-e/install-action@v2 with: tool: typos@${{ env.TYPOS_VERSION }} - run: typos dependency-policy-check: name: Dependency policy runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: dtolnay/rust-toolchain@stable with: toolchain: stable - uses: taiki-e/install-action@v2 with: tool: cargo-deny@${{ env.CARGO_DENY_VERSION }} - name: Run Cargo Deny run: cargo deny check licenses bans sources rizsotto-Bear-0c30b27/.github/workflows/codeql.yml000066400000000000000000000031741527177720300222070ustar00rootroot00000000000000name: CodeQL on: push: branches: [master, next] pull_request: schedule: - cron: '23 4 * * 1' env: CARGO_TERM_COLOR: always # Keep identical to the cargo-deny version in .deny.toml. CARGO_DENY_VERSION: 0.20.2 permissions: contents: read jobs: analyze: name: Analyze (${{ matrix.language }}) runs-on: ubuntu-latest permissions: security-events: write strategy: fail-fast: false matrix: include: - language: actions build-mode: none - language: rust build-mode: none # The preload shim is compiled by the cc crate, so CodeQL has # to trace a real build to see it. - language: c-cpp build-mode: manual steps: - uses: actions/checkout@v7 - uses: dtolnay/rust-toolchain@stable if: matrix.build-mode == 'manual' with: toolchain: stable - uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} - if: matrix.build-mode == 'manual' run: cargo build -p intercept-preload --locked - uses: github/codeql-action/analyze@v4 with: category: /language:${{ matrix.language }} advisories: name: Advisories runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: dtolnay/rust-toolchain@stable with: toolchain: stable - uses: taiki-e/install-action@v2 with: tool: cargo-deny@${{ env.CARGO_DENY_VERSION }} - name: Run Cargo Deny run: cargo deny check advisories rizsotto-Bear-0c30b27/.github/workflows/pages.yml000066400000000000000000000031471527177720300220370ustar00rootroot00000000000000name: docs site on: push: branches: [master, next] pull_request: env: CARGO_TERM_COLOR: always # Single source of truth for the mdBook version: check-docs-site.sh # tells contributors to install the version pinned here. Never # install unpinned. MDBOOK_VERSION: 0.5.4 permissions: contents: read jobs: build: name: Build Documentation Site runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: dtolnay/rust-toolchain@stable with: toolchain: stable - name: Install mdBook run: cargo install mdbook --version ${{ env.MDBOOK_VERSION }} --locked - name: Build and link check run: ./scripts/check-docs-site.sh - name: Post-process for search and AI-assistant discovery run: ./scripts/postprocess-docs-site.sh - name: Upload site uses: actions/upload-pages-artifact@v5 with: path: site/book # mdBook emits site/book/.nojekyll, and v4 started dropping # hidden files from the artifact by default. include-hidden-files: true deploy: name: Deploy Documentation Site if: github.event_name == 'push' && github.ref == 'refs/heads/master' needs: build runs-on: ubuntu-latest # Publishes the artifact built above and checks nothing out, so it # needs no contents scope of its own. permissions: pages: write id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v5 rizsotto-Bear-0c30b27/.gitignore000066400000000000000000000003411527177720300166010ustar00rootroot00000000000000.idea/ .vscode/ .claude/* !.claude/skills/ !.claude/rules/ .zed/ cmake-build*/ cmake-build-debug/ Output/ target/ site/book/ .DS_Store *~ *.profraw # Python bytecode from the docs-site generator scripts __pycache__/ *.pyc rizsotto-Bear-0c30b27/.rustfmt.toml000066400000000000000000000002721527177720300172730ustar00rootroot00000000000000# https://rust-lang.github.io/rustfmt/ style_edition = "2024" newline_style = "Unix" max_width = 110 use_field_init_shorthand = true use_try_shorthand = true use_small_heuristics = "Max"rizsotto-Bear-0c30b27/.typos.toml000066400000000000000000000011601527177720300167420ustar00rootroot00000000000000[files] extend-exclude = ["*.lock", "man/*.1"] # abd -> source matcher for ADA files # exect -> the exect(2) libc call # fo -> MSVC output flag /Fo # ot -> MSVC optimization flag /Ot # gir -> GObject-Introspection .gir binding files (Vala) # hpe -> HPE, the Cray compiler vendor # iinclude -> the -Iinclude compiler flag # mak -> config.mak, the ffmpeg/nasm configure output # mis -> the "mis-" prefix (mis-handle, mis-recognize) # nto -> QNX ntoARCH-prefixed compilers [default.extend-words] abd = "abd" exect = "exect" fo = "fo" ot = "ot" gir = "gir" hpe = "hpe" iinclude = "iinclude" mak = "mak" mis = "mis" nto = "nto" rizsotto-Bear-0c30b27/CLAUDE.md000066400000000000000000000136141527177720300160770ustar00rootroot00000000000000## Project overview Bear generates JSON compilation databases (`compile_commands.json`) for Clang tooling. It intercepts compiler invocations during a build (preload library or wrapper), keeps the ones it recognizes as compiler calls, and writes them out. ### Workspace crates | Crate | Purpose | |---|---| | `crates/bear` | Application layer: CLI, configuration, modes, output | | `crates/semantic` | Compiler recognition and flag parsing; owns the `compilers/*.yaml` definitions | | `crates/bear-driver` | Driver/supervisor binary; orchestrates interception and output | | `crates/intercept` | Shared/agent-side interception runtime (Execution, reporter, wire, env helpers) | | `crates/intercept-supervisor` | Driver-side interception (supervise, TCP collector, build environment) | | `crates/intercept-preload` | `LD_PRELOAD` / `DYLD_INSERT_LIBRARIES` shared library | | `crates/bear-wrapper` | Compiler wrapper binary; reports intercepted executions | | `crates/bear-completions` | Shell-completion script generator | | `build-support/platform-checks` | Build-time platform capability detection | | `build-support/compilers-codegen` | Code generator for compiler flag tables | | `tests/tools` | Shared test tooling; library + `cdb-compare` binary (package `bear-test-tools`) | | `tests/integration` | End-to-end tests (package `integration-tests`) | Build prerequisites (C compiler, linker) are in `INSTALL.md`. ## Routing - read before modifying Before modifying code in a subdirectory, read the `CLAUDE.md` in that directory first. These files contain rules, context, and constraints specific to that area. | When you are about to... | Read first | |---|---| | Modify CLI arguments, configuration, or output format | `crates/bear/CLAUDE.md` | | Touch compiler recognition or flag parsing | `crates/semantic/CLAUDE.md` | | Edit or add compiler interpreter YAML | `crates/semantic/compilers/CLAUDE.md` | | Edit the YAML-to-Rust code generator | `build-support/compilers-codegen/CLAUDE.md` | | Add or change a host capability probe | `build-support/platform-checks/CLAUDE.md` | | Touch the preload interception library | `crates/intercept-preload/CLAUDE.md` | | Touch driver-side interception (supervise, collector, build env) | `crates/intercept-supervisor/CLAUDE.md` | | Touch the shell-completions binary | `crates/bear-completions/CLAUDE.md` | | Write or modify integration tests | `tests/integration/CLAUDE.md` | | Edit the test-tooling comparator or normalization | `tests/tools/CLAUDE.md` (behavior in `tests/tools/SPEC.md`) | | Edit or regenerate the man page | `man/CLAUDE.md` | | Write or modify a docs site page | `site/CLAUDE.md` | | Add, modify, or review a requirement (contract) | `docs/requirements/CLAUDE.md` | | Record or look up a design decision, or a rejected option | `docs/rationale/CLAUDE.md` | | Run an operational procedure (release, ...) | invocable skills in `.claude/skills/` | Do not skip these reads. They contain constraints that prevent regressions. Keep the roles separate. A requirement says *what*; a rationale entry says *why*; the code says *how*. The man page is the user-facing reference for CLI flags and configuration keys; the site is the user guide. Each rule lives in one file. Other files link to it and never restate it. ## Workflow 1. **Start the task.** Pick it up in the task tracker. 2. **Classify the change.** A new feature, an architectural change, or a change to an existing contract takes every step. A bug fix that does not change a contract skips steps 3 and 7, and cites the governing requirement if one exists. Any other change (refactor, performance, internal) skips step 3. Every change finds its governing requirement and keeps its acceptance criteria passing. 3. **Requirement and approval.** Write or edit the requirement at status `proposed` (`docs/requirements/CLAUDE.md`, `requirements` skill). Present a short decision log (approach, alternatives considered, trade-offs) and wait for approval. Record the why in `docs/rationale/` (`docs/rationale/CLAUDE.md`). 4. **Test first.** Write the failing test that cites the requirement (`.claude/rules/testing.md`, `tests/integration/CLAUDE.md`). 5. **Implement.** Read the routing table above first. Rust conventions are in `.claude/rules/rust.md`. 6. **Checks.** All four must pass before committing: ```sh cargo fmt --check cargo clippy --all-targets -- -D warnings cargo test typos ``` Run `cargo build` before `cargo test` (`tests/integration/CLAUDE.md`). 7. **Different-model review.** Have a review subagent on a different model check the change (the `Agent` tool with a different `model`). 8. **Commit.** If the change implements a requirement, set it to `implemented` first (`docs/requirements/CLAUDE.md`). See "Commit messages" below. 9. **Close the task** in the task tracker, citing the commit. ## Code guidelines Always-on, language-agnostic standards for every workspace crate: - Maintain the existing code structure; extend before adding. - Keep dependencies minimal; a new dependency needs a real justification, not convenience. - No speculative features or over-engineering. Rust file conventions live in `.claude/rules/rust.md`, and test conventions in `.claude/rules/testing.md`. Both are path-scoped to `.rs` files. The per-crate `CLAUDE.md` files add only crate-specific constraints. ## Commit messages - Imperative subject under ~70 characters, with a conventional-commit area prefix (`docs:`, `fix:`, `feat:`, `chore:`, `test:`, `refactor:`, ...), scoped where it sharpens the scan-line (`docs(requirements): ...`). - Blank line, then a body explaining the *why*; the diff shows the *what*. - Reference a requirement ID when the commit implements or changes that contract, and an issue or PR only when it adds context the body can't. ## Output format - ASCII only in all files (no em dashes, smart quotes, Unicode bullets) - Use hyphens, straight quotes, three dots, asterisks/hyphens for bullets rizsotto-Bear-0c30b27/CODE_OF_CONDUCT.md000066400000000000000000000121441527177720300174140ustar00rootroot00000000000000 # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at *rizsotto@gmail.com*. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. rizsotto-Bear-0c30b27/CONTRIBUTING.md000066400000000000000000000043101527177720300170420ustar00rootroot00000000000000# How to contribute Thank you for taking the time to contribute! ## Development setup Bear is a Cargo workspace with the following crates: - **bear** - the application layer: CLI, configuration, modes, output - **semantic** - compiler recognition and flag parsing; owns the compiler definition YAML - **intercept-preload** - the `LD_PRELOAD` / `DYLD_INSERT_LIBRARIES` shared library - **platform-checks** - compile-time platform feature detection - **integration-tests** - end-to-end tests that exercise the installed tool The root `CLAUDE.md` has the full crate table. Build, lint, and test with: ```sh cargo fmt --check cargo clippy --all-targets -- -D warnings cargo test typos ``` Integration tests require a debug build (`cargo build`) to be present before running `cargo test`. ## Reporting bugs - Please read the documentation. It may be a known limitation of the current release. This might also help to clear false expectations about the tool, or help you classify your request not as a bug but as an enhancement. - Ensure that the bug was not already reported by searching on GitHub under [Issues](https://github.com/rizsotto/Bear/issues). - If you have not found an open issue addressing the problem, open a new one. Be sure to include a title and clear description, with as much relevant information as possible. Attach the output of the tool. Try running in verbose mode. ## Suggesting enhancements - Enhancement suggestions are tracked as GitHub issues. - Use a clear and descriptive title for the issue to identify the suggestion. - Describe the current behavior and explain which behavior you expected to see instead and why. ## Pull Requests - Open a new GitHub pull request with the patch. - Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. - Think about testability. Please write test(s) for the problem you have fixed. Make sure that existing tests are not broken. **PR checklist:** - [ ] `cargo fmt --check` passes - [ ] `cargo clippy --all-targets -- -D warnings` passes - [ ] `cargo test` passes (including integration tests) - [ ] `typos` passes - [ ] Documentation updated if behavior changed rizsotto-Bear-0c30b27/COPYING000066400000000000000000001045131527177720300156520ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 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 . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . rizsotto-Bear-0c30b27/Cargo.lock000066400000000000000000000736151527177720300165340ustar00rootroot00000000000000# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "aho-corasick" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] [[package]] name = "annotate-snippets" version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f211a51805bc641f3ad5b7664c77d2547af685cc33b4cd8d31964027a46f13f1" dependencies = [ "anstyle", "memchr", "unicode-width", ] [[package]] name = "anstyle" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anyhow" version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "arraydeque" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" [[package]] name = "assert_cmd" version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2aa3a22042e45de04255c7bf3626e239f450200fd0493c1e382263544b20aea6" dependencies = [ "anstyle", "bstr", "libc", "predicates", "predicates-core", "predicates-tree", "wait-timeout", ] [[package]] name = "assert_fs" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ecf5c70ca07b7f80220bce936f0556a960ca6fb00fc2bd4125b5e581b218137" dependencies = [ "anstyle", "globwalk", "predicates", "predicates-core", "predicates-tree", "tempfile", ] [[package]] name = "autocfg" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "base64" version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" [[package]] name = "bear" version = "4.2.4" dependencies = [ "clap", "glob", "intercept", "intercept-supervisor", "libc", "log", "mockall", "semantic", "serde", "serde-saphyr", "serde_json", "shell-words", "tempfile", "thiserror", ] [[package]] name = "bear-completions" version = "4.2.4" dependencies = [ "bear", "clap_complete", ] [[package]] name = "bear-driver" version = "4.2.4" dependencies = [ "anyhow", "bear", "intercept", "intercept-supervisor", "log", ] [[package]] name = "bear-test-tools" version = "4.2.4" dependencies = [ "anyhow", "clap", "serde", "serde_json", "shell-words", "thiserror", ] [[package]] name = "bear-wrapper" version = "4.2.4" dependencies = [ "anyhow", "intercept", "intercept-supervisor", "log", ] [[package]] name = "bit-set" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56d87354e4229f54a44f7bf2435906a4656dba36026ab6eaca629a2c436a691c" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5727b15fa97d4f4fee0a3b7c3d550ed0269f54329207b86388de918604e31269" dependencies = [ "borsh", "serde", ] [[package]] name = "bitflags" version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" [[package]] name = "borsh" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a" dependencies = [ "borsh-derive", "bytes", "cfg_aliases", ] [[package]] name = "borsh-derive" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12cdfe656708a01f89b451a7d36466e6fe6c414de0aa18fc54f864f6f9ca9f56" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", "syn 3.0.9", ] [[package]] name = "bstr" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" dependencies = [ "memchr", "regex-automata", "serde_core", ] [[package]] name = "bytes" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cc" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0" dependencies = [ "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" [[package]] name = "cfg_aliases" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "chacha20" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" dependencies = [ "cfg-if", "cpufeatures", "rand_core", ] [[package]] name = "clap" version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap_builder" version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" dependencies = [ "anstyle", "clap_lex", "strsim", ] [[package]] name = "clap_complete" version = "4.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "037e2a1a92236d0aff7e845093f64661d6df4c02c9fcc61a60e9e1d736fa392f" dependencies = [ "clap", ] [[package]] name = "clap_derive" version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" dependencies = [ "heck", "proc-macro2", "quote", "syn 3.0.9", ] [[package]] name = "clap_lex" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" [[package]] name = "compilers-codegen" version = "4.2.4" dependencies = [ "anyhow", "insta", "proptest", "serde", "serde-saphyr", "tempfile", ] [[package]] name = "console" version = "0.16.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e96a4956774c13c126a8b5af4daa79384f4d826534c95a02d76afb39e2ab64e3" dependencies = [ "encode_unicode", "libc", "windows-sys", ] [[package]] name = "core_detect" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" [[package]] name = "cpufeatures" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" dependencies = [ "libc", ] [[package]] name = "crossbeam-deque" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" dependencies = [ "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" [[package]] name = "ctor" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d" dependencies = [ "link-section", "linktime-proc-macro", ] [[package]] name = "difflib" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] name = "downcast" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" [[package]] name = "encode_unicode" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "encoding_rs" version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" dependencies = [ "cfg-if", "core_detect", "multiversion_no_op", "rustversion", "scopeguard", "simdutf8", ] [[package]] name = "encoding_rs_io" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fba3fe847045ecff794b9c138293a80db914678c453ad63fbf0c6a9eb6e00b22" dependencies = [ "encoding_rs", ] [[package]] name = "env_filter" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" dependencies = [ "log", "regex", ] [[package]] name = "env_logger" version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" dependencies = [ "env_filter", "log", ] [[package]] name = "equivalent" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" [[package]] name = "errno" version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", "windows-sys", ] [[package]] name = "fastrand" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "find-msvc-tools" version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" [[package]] name = "float-cmp" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" dependencies = [ "num-traits", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "fragile" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8878864ba14bb86e818a412bfd6f18f9eabd4ec0f008a28e8f7eb61db532fcf9" dependencies = [ "futures-core", ] [[package]] name = "futures-core" version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "getrandom" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", "r-efi", "rand_core", ] [[package]] name = "glob" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "globset" version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988" dependencies = [ "aho-corasick", "bstr", "log", "regex-automata", "regex-syntax", ] [[package]] name = "globwalk" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ "bitflags", "ignore", "walkdir", ] [[package]] name = "granit-parser" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da55feca5d1625466bae068f9fd4b87892caeae70c3fe671ddb0f50d41aebf37" dependencies = [ "arraydeque", "smallvec", ] [[package]] name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "ignore" version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b69833ed729dc5aa7d19541d96d6cf8e9137194207a04916d658e43168402f" dependencies = [ "crossbeam-deque", "globset", "log", "memchr", "regex-automata", "same-file", "walkdir", "winapi-util", ] [[package]] name = "indexmap" version = "2.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "insta" version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b67d3d2e287e4b86c10b3f3b641033d1f89b74bdb39d05f34952e2b9a6fe21cd" dependencies = [ "console", "once_cell", "similar", "tempfile", ] [[package]] name = "integration-tests" version = "4.2.4" dependencies = [ "anyhow", "assert_cmd", "assert_fs", "bear-test-tools", "encoding_rs", "platform-checks", "predicates", "serde-saphyr", "serde_json", "tempfile", "which", ] [[package]] name = "intercept" version = "4.2.4" dependencies = [ "compilers-codegen", "env_logger", "libc", "log", "serde", "serde_json", "tempfile", "thiserror", ] [[package]] name = "intercept-preload" version = "4.2.4" dependencies = [ "cc", "ctor", "errno", "intercept", "libc", "log", "platform-checks", "serde_json", "thiserror", ] [[package]] name = "intercept-supervisor" version = "4.2.4" dependencies = [ "intercept", "libc", "log", "serde", "serde_json", "signal-hook", "tempfile", "thiserror", "which", ] [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "libc" version = "0.2.191" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" [[package]] name = "link-section" version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39c29a617ce3df32c08497bdc1ab6e2376e0b17948ac166a2fbe5977c5954cd9" [[package]] name = "linktime-proc-macro" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e57c38c1e860fd37c604281cdfb1dd2216977fd76a50f85ba2f388ef3219616" [[package]] name = "linux-raw-sys" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "log" version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "memchr" version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "mockall" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a6ceddfe3ce334925e96bf420fdb2dcee5bed6c632a168ece622676dadeaf8a" dependencies = [ "cfg-if", "downcast", "fragile", "mockall_derive", "predicates", "predicates-tree", ] [[package]] name = "mockall_derive" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cfe16fbe8a314aeec0b861ac24e60b1e123e97634bab045475b9d6a18416fd8" dependencies = [ "cfg-if", "proc-macro2", "quote", "syn 2.0.119", ] [[package]] name = "multiversion_no_op" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" [[package]] name = "nohash-hasher" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" [[package]] name = "normalize-line-endings" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "platform-checks" version = "4.2.4" dependencies = [ "cc", "tempfile", ] [[package]] name = "predicates" version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" dependencies = [ "anstyle", "difflib", "float-cmp", "normalize-line-endings", "predicates-core", "regex", ] [[package]] name = "predicates-core" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" [[package]] name = "predicates-tree" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" dependencies = [ "predicates-core", "termtree", ] [[package]] name = "proc-macro-crate" version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] [[package]] name = "proptest" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8530004ccb15eae51c7e40009fbe317f341f804db54dc033eec1c50be28cfa0" dependencies = [ "bit-set", "bit-vec", "bitflags", "chacha20", "core_detect", "num-traits", "rand", "rand_xorshift", "regex-syntax", "rusty-fork", "tempfile", "unarray", ] [[package]] name = "quick-error" version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" dependencies = [ "getrandom", "rand_core", ] [[package]] name = "rand_core" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "rand_xorshift" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60aa6af80be32871323012e02e6e65f8a7cc7890931ae421d217ad8fe0df2ccf" dependencies = [ "rand_core", ] [[package]] name = "regex" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-lite" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "rustix" version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5e76742e5294153d4dde1207dbec5bdd81eef6fabdc0d2118babadabfd858aa" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys", ] [[package]] name = "rustversion" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "rusty-fork" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" dependencies = [ "fnv", "quick-error", "tempfile", "wait-timeout", ] [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semantic" version = "4.2.4" dependencies = [ "compilers-codegen", "intercept", "libc", "log", "mockall", "regex-lite", "shell-words", "tempfile", "thiserror", ] [[package]] name = "serde" version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", ] [[package]] name = "serde-saphyr" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a253edfdea34032da21af84e16fcc94f4c7a6ba237f9f3bc2dbf43cea8535c44" dependencies = [ "annotate-snippets", "base64", "encoding_rs_io", "granit-parser", "nohash-hasher", "num-traits", "serde_core", "smallvec", "zmij", ] [[package]] name = "serde_core" version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", "syn 3.0.9", ] [[package]] name = "serde_json" version = "1.0.154" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" dependencies = [ "itoa", "memchr", "serde", "serde_core", "zmij", ] [[package]] name = "shell-words" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" [[package]] name = "shlex" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b29ffa27d3d59a049bb328258de100f8012848d1b08d444e08bbc02ac468435e" dependencies = [ "libc", "signal-hook-registry", ] [[package]] name = "signal-hook-registry" version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ "errno", "libc", ] [[package]] name = "simdutf8" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "similar" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "smallvec" version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "syn" version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom", "once_cell", "rustix", "windows-sys", ] [[package]] name = "termtree" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "thiserror" version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", "syn 3.0.9", ] [[package]] name = "toml_datetime" version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" version = "0.25.17+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" dependencies = [ "indexmap", "toml_datetime", "toml_parser", "winnow", ] [[package]] name = "toml_parser" version = "1.1.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" dependencies = [ "winnow", ] [[package]] name = "unarray" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicode-ident" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" [[package]] name = "unicode-width" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "wait-timeout" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" dependencies = [ "libc", ] [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "which" version = "8.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bae2f2b2b816647a1cab1acc91f5bd20812d53cb344382635ec2181940c8034f" dependencies = [ "libc", ] [[package]] name = "winapi-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ "windows-sys", ] [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] [[package]] name = "winnow" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" dependencies = [ "memchr", ] [[package]] name = "zmij" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" rizsotto-Bear-0c30b27/Cargo.toml000066400000000000000000000033141527177720300165440ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later [workspace] members = ["crates/*", "build-support/*", "tests/*"] # tests/dogfooding is a POSIX-sh release-time harness, not a Rust crate, so it # has no Cargo.toml; exclude it from the tests/* glob. exclude = ["tests/dogfooding"] resolver = "3" [workspace.package] version = "4.2.4" authors = ["László Nagy "] repository = "https://github.com/rizsotto/Bear" homepage = "https://github.com/rizsotto/Bear" license = "GPL-3.0-or-later" edition = "2024" rust-version = "1.89" [workspace.lints.clippy] allow_attributes_without_reason = "warn" unwrap_used = "warn" undocumented_unsafe_blocks = "warn" [workspace.dependencies] thiserror = "2.0" anyhow = "1.0" serde = { version = "1.0", default-features = false, features = ["derive", "std"] } serde_json = { version = "1.0", default-features = false, features = ["std"] } serde-saphyr = { version = "2.0", default-features = false, features = ["deserialize", "serialize"] } clap = { version = "4.6", default-features = false, features = ["std", "cargo", "help", "usage", "suggestions"] } clap_complete = { version = "4.6", default-features = false } log = { version = "0.4", default-features = false, features = ["std"] } env_logger = { version = "0.11", default-features = false } shell-words = "1.1" tempfile = { version = "3.27", default-features = false } signal-hook = { version = "0.4", default-features = false } regex-lite = "0.1" libc = "0.2" errno = "0.3" ctor = "1.0" which = "8.0" cc = "1.10" assert_cmd = "2.2" assert_fs = "1.1" predicates = "3.1" mockall = "0.15" insta = "1.49" proptest = "1.12" encoding_rs = "0.8" glob = "0.3" [profile.release] strip = true lto = true opt-level = 3 codegen-units = 1 rizsotto-Bear-0c30b27/INSTALL.md000066400000000000000000000172231527177720300162500ustar00rootroot00000000000000# Install from distribution package Bear has been around for a while, and packages are available in many distributions. For an easy installation, consult your distribution's package manager. These packages are well-tested and should be the first choice for installation. Common package manager commands: ```sh # Debian / Ubuntu sudo apt install bear # Fedora sudo dnf install bear # Arch Linux sudo pacman -S bear # macOS (Homebrew) brew install bear # FreeBSD pkg install bear ``` For a full list of available packages, see the [Repology page](https://repology.org/project/bear-clang/versions). # Install from source If the latest version is not available in your distribution, install Bear from source. Follow the steps below. ## Prerequisites Bear is now implemented in Rust, so the Rust toolchain is required. **Rust toolchain** (1.89 or later): Bear uses the Rust 2024 edition and language features stabilized up to Rust 1.89. Install Rust using [rustup](https://rustup.rs/): ```sh curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` Ensure that `cargo` and `rustc` are available in your `PATH`: ```sh rustc --version # must be >= 1.89 cargo --version ``` **C compiler**: A C compiler is required to build the preload library (`intercept-preload`). The `cc` crate will typically find one automatically; ensure `gcc` or `clang` is installed. On Linux/ELF platforms the preload library links with `lld` or `mold`; GNU `ld` cannot combine the Rust compiler's linker version script with Bear's. Toolchains installed via rustup bundle `rust-lld`, which the build picks up automatically; otherwise install `lld` or `mold`. ## Simple installation 1. Clone the repository: ```sh git clone https://github.com/rizsotto/Bear.git cd Bear ``` 2. Build: ```sh cargo build --release ``` 3. (Optional) Generate shell completions: ```sh target/release/generate-completions target/release/completions ``` 4. Install: ```sh ./scripts/install.sh ``` If the completions directory exists under the build artifacts, the install script will automatically install completions for bash, zsh, fish, and elvish to standard locations. 5. Verify the installation: ```sh bear --version bear -- true # quick smoke test - should produce an empty compile_commands.json ``` ## Uninstall The install script does not generate an uninstall script. To remove Bear, delete the installed files manually. The installation layout is listed at the end of this document. ## Custom installation By default, Bear is installed to `/usr/local` (when run as root) or `$HOME/.local` (otherwise). You can override the installation prefix with `PREFIX`: ```sh sudo PREFIX=/usr ./scripts/install.sh ``` `PREFIX` is the final install location (e.g., `/usr`, `/usr/local`, `$HOME/.local`). The `bear` entry script goes into `$PREFIX/bin/`, the helper binaries and the preload library into `$PREFIX/libexec/bear/`, and so on. On distributions without a `libexec` directory (Arch Linux uses `/usr/lib`), set `LIBEXECDIR` to the directory that should hold `bear/`: ```sh sudo PREFIX=/usr LIBEXECDIR=/usr/lib ./scripts/install.sh ``` Like `PREFIX`, `LIBEXECDIR` is an absolute, on-system path: `DESTDIR` is prepended to it, and the `bear` entry script embeds it. It needs no build-time setting, because `bear-driver` finds its siblings relative to itself. The preload library directory name defaults to `lib`. On systems where a different directory is needed, set `INTERCEPT_LIBDIR` at both build and install time: ```sh # Build with the correct library directory compiled in INTERCEPT_LIBDIR=lib64 cargo build --release # Install with the same value so the file is placed where bear-driver expects it INTERCEPT_LIBDIR=lib64 ./scripts/install.sh ``` On glibc-based Linux, the special value `$LIB` can be used - the dynamic linker expands it at runtime (see `man ld.so`). On other platforms (macOS, musl, FreeBSD), use a concrete directory name. ### Shell completions with a user-local install When Bear is installed to a system prefix like `/usr` or `/usr/local`, shells typically find completions automatically. For a user-local install (`$HOME/.local`), you need to tell your shell where to look: **Bash** - add to `~/.bashrc`: ```bash source "$HOME/.local/share/bash-completion/completions/bear" ``` **Zsh** - add to `~/.zshrc` (before `compinit`): ```zsh fpath=("$HOME/.local/share/zsh/site-functions" $fpath) ``` **Fish** - add to `~/.config/fish/config.fish`: ```fish set -p fish_complete_path $HOME/.local/share/fish/vendor_completions.d ``` # How to package If you are a package maintainer for a distribution: - Build, generate completions, and install with `DESTDIR`, `PREFIX`, and `INTERCEPT_LIBDIR`: ```sh INTERCEPT_LIBDIR=lib64 cargo build --release target/release/generate-completions target/release/completions DESTDIR=$pkgdir PREFIX=/usr INTERCEPT_LIBDIR=lib64 ./scripts/install.sh ``` `DESTDIR` is the staging root prepended to all install paths. `PREFIX` is the final on-system prefix (e.g. `/usr`). Do not combine them into `PREFIX` - the entry script embeds the driver path (`$LIBEXECDIR/bear/bin/bear-driver`, under `PREFIX` by default) as a literal runtime path, so it must match the installed system, not the build chroot. If the distribution has no `libexec` directory, add `LIBEXECDIR` (for example `LIBEXECDIR=/usr/lib`). It defaults to `$PREFIX/libexec`. The same rule applies: give the on-system path, never one under `DESTDIR`. - The preload library is only built on Unix: `libexec.so` on Linux and the BSDs, `libexec.dylib` on macOS. Windows builds only produce `bear-driver` and `bear-wrapper`. Consult `crates/intercept-preload/build.rs` for details. - `bear-driver` locates its siblings using relative paths: `./bear-wrapper` and `../$INTERCEPT_LIBDIR/libexec.so`. The `bear` entry script in `$PREFIX/bin/` is the only artifact that uses an absolute path. - The expected installation layout: ``` $PREFIX/ |-- bin/ | `-- bear (shell script) |-- libexec/ ($LIBEXECDIR) | `-- bear/ | |-- bin/ | | |-- bear-driver | | `-- bear-wrapper | `-- $INTERCEPT_LIBDIR/ | `-- libexec.so `-- share/ |-- bash-completion/ | `-- completions/ | `-- bear (optional) |-- zsh/ | `-- site-functions/ | `-- _bear (optional) |-- fish/ | `-- vendor_completions.d/ | `-- bear.fish (optional) |-- elvish/ | `-- lib/ | `-- bear.elv (optional) |-- doc/ | `-- bear/ | |-- README.md | `-- COPYING `-- man/ `-- man1/ `-- bear.1 ``` - Common pitfalls: - If `INTERCEPT_LIBDIR` differs between the build and the install step, `bear-driver` looks for the preload library in the wrong place and preload interception silently fails at runtime. Set the same value for both, as shown above. - Build sandboxes that block `LD_PRELOAD` injection (for example Gentoo's Portage sandbox) will not observe anything under the default preload method; configure `intercept.mode: wrapper` for packages built or tested under such a sandbox (see `bear.yml` in `man/bear.1.md`). - After packaging, run the smoke test from step 5 above (`bear --version` and `bear -- true`) against the installed `$PREFIX/bin/bear`, not just the build directory, to confirm the installed layout resolves correctly. rizsotto-Bear-0c30b27/README.md000066400000000000000000000110361527177720300160730ustar00rootroot00000000000000[![Packaging status](https://repology.org/badge/tiny-repos/bear-clang.svg)](https://repology.org/project/bear-clang/versions) [![GitHub release](https://img.shields.io/github/release/rizsotto/Bear)](https://github.com/rizsotto/Bear/releases) [![GitHub Release Date](https://img.shields.io/github/release-date/rizsotto/Bear)](https://github.com/rizsotto/Bear/releases) [![Continuous Integration](https://github.com/rizsotto/Bear/workflows/rust%20CI/badge.svg)](https://github.com/rizsotto/Bear/actions) [![Contributors](https://img.shields.io/github/contributors/rizsotto/Bear)](https://github.com/rizsotto/Bear/graphs/contributors) ʕ·ᴥ·ʔ Build EAR =============== Bear gives you working code navigation, autocomplete, and diagnostics on any C/C++ project, whatever the build system. Prefix your build command with `bear --` and it produces the `compile_commands.json` file that Clang tooling (clangd, clang-tidy, and friends) needs to understand your code. The [JSON compilation database][JSONCDB] describes how each translation unit is compiled. Bear captures the compiler invocations while your build runs and writes the database for you - no changes to your build system required. [JSONCDB]: http://clang.llvm.org/docs/JSONCompilationDatabase.html When to use Bear ----------------- Bear is the tool of choice when the build system cannot produce a compilation database for you: - Make, autotools, or custom script builds - most of the C world. - A build you cannot or do not want to modify: a third-party project, a codebase you are just exploring, a CI job. - Unusual toolchains: embedded (ARM, TI, Microchip, QNX), HPC (Cray, Intel, NVIDIA, MPI wrappers), CUDA, cross-compilers, and compiler launchers such as ccache, distcc, and icecc. - A build you cannot run at all: parse `make -n` dry-run output or a saved build log instead. Bear can intercept the build as it runs, recording what the compiler was actually invoked with, including flags injected by wrappers and entries for generated sources. Bear can also interpret the commands a build system plans to run, formatting those into a compilation database as if they had executed. If your project uses CMake, Meson, or Bazel, those tools can export a compilation database directly; use that when it is available. How to install -------------- Bear is [packaged][REPOLOGY] for many distributions. Check your distribution's package manager first. Alternatively, you can [build it](INSTALL.md) from source. How to use ---------- After installation, run: bear -- Bear writes `compile_commands.json` to the current working directory. Pass Bear's own options before `--`; everything after that is treated as part of the build command. If you cannot run the build - all you have is a CI log or the build system's dry-run output - Bear can reconstruct the database from that text instead: make -n | bear parse-sh For more, run `bear --help` or read the man page. The [documentation site][DOCS] covers limitations, known issues, and platform- and toolchain-specific usage. Limitations ----------- Bear works on Linux, Android, macOS, FreeBSD, OpenBSD, NetBSD, DragonFly BSD, and Windows. Each operating system and build system imposes constraints on how Bear can run and which interception method is available, and those affect the final output. The [documentation site][DOCS] describes the available options and each option's limitations. Acknowledgments --------------- Bear is kept going by people who chip in. Thank you. - [@maflcko](https://github.com/maflcko) for regular sponsorship. - [Anthropic](https://claude.ai/) for a free Claude subscription, which supported development work. Problem reports --------------- Before opening a new problem report, please check the documentation to see if your problem is a known issue with a documented workaround. It's also helpful to look at older (possibly closed) [issues][ISSUES] before opening a new one. If you decide to report a problem, please provide as much context as possible to help reproduce the error. If you just have a question about usage, please don't be shy; ask your question in an issue or in our [chat][CHAT]. If you've found a bug and have a fix for it, please share it by opening a pull request. Please follow the [contribution guide][GUIDE] when you do. [ISSUES]: https://github.com/rizsotto/Bear/issues [DOCS]: https://rizsotto.github.io/Bear/ [CHAT]: https://gitter.im/rizsotto/Bear/discussions [GUIDE]: https://github.com/rizsotto/Bear/blob/master/CONTRIBUTING.md [REPOLOGY]: https://repology.org/project/bear-clang/versionsrizsotto-Bear-0c30b27/SECURITY.md000066400000000000000000000021461527177720300164070ustar00rootroot00000000000000# Security policy ## Supported versions Only the latest release gets security fixes. Older 4.x releases do not get backports; upgrade to the latest release instead. ## Reporting a vulnerability Please do not open a public issue for a security problem. Report it privately through GitHub: https://github.com/rizsotto/Bear/security/advisories/new Include the Bear version, your platform, and the steps to reproduce. You should get a first answer within 14 days. Once a fix is released, the advisory is published and credits the reporter, unless you ask otherwise. ## Scope Bear runs your build and records the compiler calls it sees. It runs with your privileges and trusts the build it is given. Issues in that area that are in scope: - the preload library (`libexec.so`) or the wrapper changing what the build executes, - the driver and the reporting channel between it and the intercepted processes, - the written `compile_commands.json` leaking or corrupting data beyond what the build itself passed to the compiler. A build script that is already malicious is out of scope; Bear does not sandbox the build. rizsotto-Bear-0c30b27/build-support/000077500000000000000000000000001527177720300174245ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/compilers-codegen/000077500000000000000000000000001527177720300230235ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/compilers-codegen/CLAUDE.md000066400000000000000000000010641527177720300243030ustar00rootroot00000000000000## compilers-codegen ## Consumers Two build scripts call this library, and both read `crates/semantic/compilers/*.yaml`: - `crates/semantic/build.rs` via `generate` (the full tables). - `crates/intercept/build.rs` via `generate_env_keys_only` (compiler environment keys only). ## Generated outputs Snapshot tests in `tests/snapshots/` lock the generated output against accidental schema drift. ## Adding a compiler The steps are in `crates/semantic/compilers/CLAUDE.md`. The `recognize:` checks it lists live in `yaml_types.rs::RecognizeEntry::validate`. rizsotto-Bear-0c30b27/build-support/compilers-codegen/Cargo.toml000066400000000000000000000007471527177720300247630ustar00rootroot00000000000000[package] name = "compilers-codegen" version.workspace = true authors.workspace = true repository.workspace = true homepage.workspace = true license.workspace = true edition.workspace = true rust-version.workspace = true publish = false [dependencies] anyhow = { workspace = true } serde = { workspace = true } serde-saphyr = { workspace = true } [dev-dependencies] tempfile = { workspace = true } insta = { workspace = true } proptest = { workspace = true } [lints] workspace = true rizsotto-Bear-0c30b27/build-support/compilers-codegen/src/000077500000000000000000000000001527177720300236125ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/compilers-codegen/src/codegen.rs000066400000000000000000000050451527177720300255700ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later use anyhow::{Result, bail}; /// Parse a pattern string into a FlagPattern Rust expression. pub fn pattern_to_rust(pattern: &str, count: Option) -> String { if let Some(flag) = pattern.strip_suffix("{ }*") { format!("FlagPattern::ExactlyWithGluedOrSep(\"{}\")", flag) } else if let Some(flag) = pattern.strip_suffix("{=}*") { format!("FlagPattern::ExactlyWithEqOrSep(\"{}\")", flag) } else if let Some(flag) = pattern.strip_suffix("{:}*") { format!("FlagPattern::ExactlyWithColonOrSep(\"{}\")", flag) } else if let Some(flag) = pattern.strip_suffix(":*") { format!("FlagPattern::ExactlyWithColon(\"{}\")", flag) } else if let Some(flag) = pattern.strip_suffix("=*") { if let Some(n) = count { format!("FlagPattern::Prefix(\"{}=\", {})", flag, n) } else { format!("FlagPattern::ExactlyWithEq(\"{}\")", flag) } } else if let Some(flag) = pattern.strip_suffix('*') { format!("FlagPattern::Prefix(\"{}\", {})", flag, count.unwrap_or(0)) } else { format!("FlagPattern::Exactly(\"{}\", {})", pattern, count.unwrap_or(0)) } } /// Map a result string to its Rust ArgumentKind expression. pub fn result_to_rust(result: &str) -> Result<&'static str> { match result { "output" => Ok("ArgumentKind::Output"), "configures_preprocessing" => { Ok("ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))") } "configures_compiling" => Ok("ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))"), "configures_assembling" => { Ok("ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))") } "configures_linking" => Ok("ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))"), "stops_at_preprocessing" => { Ok("ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))") } "stops_at_compiling" => Ok("ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))"), "stops_at_assembling" => Ok("ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Assembling))"), "info_and_exit" => Ok("ArgumentKind::Other(PassEffect::InfoAndExit)"), "driver_option" => Ok("ArgumentKind::Other(PassEffect::DriverOption)"), "pass_through" => Ok("ArgumentKind::Other(PassEffect::PassThrough)"), "none" => Ok("ArgumentKind::Other(PassEffect::None)"), other => bail!("unknown result value '{}'", other), } } rizsotto-Bear-0c30b27/build-support/compilers-codegen/src/env_keys.rs000066400000000000000000000020751527177720300260070ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later use std::collections::{BTreeSet, HashMap}; use crate::resolve::resolve_environment; use crate::yaml_types::FlagTable; /// Generate a static array of all compiler environment variable names. /// /// Returns the generated Rust source as a string. The output is sorted (a /// `BTreeSet`), so it does not depend on the order the tables are visited. pub fn generate_env_keys(raw_tables: &HashMap) -> String { let mut all_vars = BTreeSet::new(); for id in raw_tables.keys() { let entries = resolve_environment(id, raw_tables); for entry in &entries { if entry.effect != "none" { all_vars.insert(entry.variable.clone()); } } } let mut out = String::new(); out.push_str("// Generated from compilers/*.yaml -- DO NOT EDIT\n"); out.push_str(&format!("static COMPILER_ENV_KEYS: [&str; {}] = [\n", all_vars.len())); for var in &all_vars { out.push_str(&format!(" \"{}\",\n", var)); } out.push_str("];\n"); out } rizsotto-Bear-0c30b27/build-support/compilers-codegen/src/families.rs000066400000000000000000000037471527177720300257640ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later use std::collections::HashMap; use crate::tables::CompilerFile; use crate::yaml_types::FlagTable; /// Generate `families.rs`: the registry the semantic layer loops over to /// register one flag-based interpreter per compiler family. /// /// It `include!`s each per-family flag-table file (bringing the generated /// statics into scope) and then emits `FAMILIES`, one `FamilyDef` per /// discovered compiler, in discovery order. Each row carries references to /// that family's generated statics plus its `source_mode` and /// `response_file_syntax` (taken from the family's own table, not resolved /// through `extends`). `FamilyDef` itself is hand-written in `flag_based.rs`, /// which `include!`s this file. pub fn generate_families(compiler_files: &[CompilerFile], raw_tables: &HashMap) -> String { let mut out = String::new(); out.push_str("// Generated from compilers/*.yaml -- DO NOT EDIT\n"); // Bring every family's generated flag-table statics into scope. for compiler_file in compiler_files { out.push_str(&format!( "include!(concat!(env!(\"OUT_DIR\"), \"/{}\"));\n", compiler_file.names().output_file )); } out.push_str("pub(super) static FAMILIES: &[FamilyDef] = &[\n"); for compiler_file in compiler_files { let table = &raw_tables[&compiler_file.id]; let names = compiler_file.names(); out.push_str(&format!( " FamilyDef {{ id: \"{}\", flags: &{}, ignore_executables: &{}, ignore_flags: &{}, slash_prefix: {}, env_rules: &{}, source_mode: {}, response_file_syntax: {} }},\n", compiler_file.id, names.static_name, names.ignore_executables_name, names.ignore_flags_name, names.slash_prefix_name, names.env_rules_name, table.source_mode.to_rust(), table.response_file_syntax.to_rust(), )); } out.push_str("];\n"); out } rizsotto-Bear-0c30b27/build-support/compilers-codegen/src/lib.rs000066400000000000000000001237431527177720300247400ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later pub mod codegen; pub mod env_keys; pub mod families; pub mod recognition; pub mod resolve; pub mod tables; pub mod wrappers; pub mod yaml_types; use std::collections::HashMap; use std::path::Path; use anyhow::{Context, Result, bail}; use codegen::{pattern_to_rust, result_to_rust}; use env_keys::generate_env_keys; use families::generate_families; use recognition::{generate_compiler_ids, generate_recognition_patterns}; use resolve::{resolve_environment, resolve_flags, resolve_ignore_when, resolve_slash_prefix}; use tables::{CompilerFile, TableNames}; use wrappers::generate_wrappers; use yaml_types::{EnvEntry, FlagEntry, FlagTable, IgnoreWhen, Kind, WrapperTable}; /// A compiler flag table with all inheritance resolved and ready for code generation. pub struct ResolvedTable { pub names: TableNames, pub flags: Vec, pub ignore_when: IgnoreWhen, pub slash_prefix: bool, pub env_entries: Vec, } impl ResolvedTable { /// Resolve a single compiler table by merging inherited flags, ignore_when, /// slash_prefix, and environment entries from the extends chain. Flags are /// resolved by the file's `compiler.id`; the generated names come from its /// stem. pub fn new(compiler_file: &CompilerFile, raw_tables: &HashMap) -> Result { let key = compiler_file.id.as_str(); if !raw_tables.contains_key(key) { bail!("no table found for '{}'", key); } let mut flags = resolve_flags(key, raw_tables) .with_context(|| format!("resolving flags for {}", compiler_file.yaml_file()))?; flags.sort_by_key(|b| std::cmp::Reverse(b.match_.name_len())); Ok(ResolvedTable { names: compiler_file.names(), flags, ignore_when: resolve_ignore_when(key, raw_tables), slash_prefix: resolve_slash_prefix(key, raw_tables), env_entries: resolve_environment(key, raw_tables), }) } /// Generate the complete Rust source file for this compiler's flag table. pub fn generate(&self) -> Result { let mut out = String::new(); out.push_str( &self .generate_flag_array() .with_context(|| format!("generating flags for {}", self.names.yaml_file))?, ); out.push_str(&self.generate_ignore_arrays()); out.push_str(&format!("static {}: bool = {};\n", self.names.slash_prefix_name, self.slash_prefix)); out.push_str( &self .generate_env_array() .with_context(|| format!("generating env rules for {}", self.names.yaml_file))?, ); Ok(out) } fn generate_flag_array(&self) -> Result { let mut out = String::new(); out.push_str(&format!("// Generated from compilers/{} -- DO NOT EDIT\n", self.names.yaml_file)); out.push_str(&format!("static {}: [FlagRule; {}] = [\n", self.names.static_name, self.flags.len())); for entry in &self.flags { let pattern_rust = pattern_to_rust(&entry.match_.pattern, entry.match_.count); let result_rust = result_to_rust(&entry.result).with_context(|| format!("flag '{}'", entry.match_.pattern))?; out.push_str(&format!(" FlagRule::new({}, {}),\n", pattern_rust, result_rust)); } out.push_str("];\n"); Ok(out) } fn generate_ignore_arrays(&self) -> String { let mut out = String::new(); out.push_str(&format!( "static {}: [&str; {}] = [", self.names.ignore_executables_name, self.ignore_when.executables.len() )); for exe in &self.ignore_when.executables { out.push_str(&format!("\"{}\", ", exe)); } out.push_str("];\n"); out.push_str(&format!( "static {}: [&str; {}] = [", self.names.ignore_flags_name, self.ignore_when.flags.len() )); for flag in &self.ignore_when.flags { out.push_str(&format!("\"{}\", ", flag)); } out.push_str("];\n"); out } fn generate_env_array(&self) -> Result { let active: Vec<&EnvEntry> = self.env_entries.iter().filter(|e| e.effect != "none").collect(); for entry in &active { entry.validate().with_context(|| format!("environment entry in {}", self.names.yaml_file))?; } let mut out = String::new(); out.push_str(&format!("static {}: [EnvRule; {}] = [\n", self.names.env_rules_name, active.len())); for entry in &active { let mapping_rust = entry .mapping .to_rust() .with_context(|| format!("variable '{}' in {}", entry.variable, self.names.yaml_file))?; let effect_rust = result_to_rust(&entry.effect) .with_context(|| format!("variable '{}' in {}", entry.variable, self.names.yaml_file))?; out.push_str(&format!( " EnvRule::new(\"{}\", {}, {}),\n", entry.variable, mapping_rust, effect_rust )); } out.push_str("];\n"); Ok(out) } } /// Generate all flag tables, recognition patterns, and env keys. /// /// - `flags_dir`: path to the directory containing *.yaml files /// - `out_dir`: path to write generated .rs files /// /// Prints `cargo:rerun-if-changed` lines to stdout (for build.rs integration). pub fn generate(flags_dir: &Path, out_dir: &Path) -> Result<()> { // A per-file rerun-if-changed (printed by load_and_index/load_wrapper_tables // below) only ever covers files that already existed at the previous build. // Wrapper files need no other Rust-side registration to be picked up, so // adding a brand-new one is exactly the case a per-file-only watch misses: // cargo has no reason to rerun this build script, and the new launcher goes // unrecognized until something else forces a rebuild. Watch the whole // directory so a new (or removed) file is never silently missed. println!("cargo:rerun-if-changed={}", flags_dir.display()); let (raw_tables, compiler_files) = load_tables_from(flags_dir)?; // Load compiler-launcher (wrapper) tables first: both the recognition // patterns and the wrapper tables output need the same loaded value. let wrapper_tables = load_wrapper_tables(flags_dir, true)?; // Generate recognition patterns (compiler rows in discovery order -- // specializations before their base -- then wrapper rows) let recognition = generate_recognition_patterns(&raw_tables, &compiler_files, &wrapper_tables)?; write_output(out_dir, "recognition.rs", recognition)?; // Generate the compiler-id data the config module's `as:` deserializer // validates against (KNOWN_IDS plus the wrapper launcher basenames). let compiler_ids = generate_compiler_ids(&compiler_files, &wrapper_tables)?; write_output(out_dir, "compiler_ids.rs", compiler_ids)?; // Generate each compiler's flag table for compiler_file in &compiler_files { let resolved = ResolvedTable::new(compiler_file, &raw_tables)?; write_output(out_dir, &compiler_file.names().output_file, resolved.generate()?)?; } // Generate the family registry the semantic layer loops over to register // one interpreter per family (includes the flag tables above). let families = generate_families(&compiler_files, &raw_tables); write_output(out_dir, "families.rs", families)?; // Generate combined environment variable keys let env_keys = generate_env_keys(&raw_tables); write_output(out_dir, "env_keys.rs", env_keys)?; // Generate compiler-launcher (wrapper) tables: ccache, distcc, ... let wrappers = generate_wrappers(&wrapper_tables); write_output(out_dir, "wrappers.rs", wrappers)?; Ok(()) } /// Generate only the combined environment variable keys. /// /// Used by the `intercept` crate, which needs `COMPILER_ENV_KEYS` for its /// agent-side environment filtering but none of the flag tables or recognition /// patterns. Reads `*.yaml` from `flags_dir` and writes `env_keys.rs` into /// `out_dir`. /// /// Prints `cargo:rerun-if-changed` lines to stdout (for build.rs integration). pub fn generate_env_keys_only(flags_dir: &Path, out_dir: &Path) -> Result<()> { let (raw_tables, _compiler_files) = load_tables_from(flags_dir)?; let env_keys = generate_env_keys(&raw_tables); write_output(out_dir, "env_keys.rs", env_keys)?; Ok(()) } /// Parse a single YAML table, attaching the source file name to any parse /// error so a schema violation names the offending file. pub fn parse_table(yaml_file: &str, content: &str) -> Result { serde_saphyr::from_str(content).with_context(|| format!("parsing {}", yaml_file)) } /// Parse a single wrapper table, attaching the source file name to any /// parse error so a schema violation names the offending file. pub fn parse_wrapper_table(yaml_file: &str, content: &str) -> Result { serde_saphyr::from_str(content).with_context(|| format!("parsing {}", yaml_file)) } /// Insert a parsed table into an id-keyed map, keyed by `table.compiler.id`. /// /// Bails with a clear error naming both the id and the file that redefines /// it if the id collides with a table already present. Returns the id on /// success, so callers that also need a `yaml_file -> id` side index don't /// have to clone it themselves. pub fn insert_by_id( raw_tables: &mut HashMap, yaml_file: &str, table: FlagTable, ) -> Result { let id = table.compiler.id.clone(); if raw_tables.contains_key(&id) { bail!("duplicate compiler id '{}': {} redefines an id already used by another table", id, yaml_file); } raw_tables.insert(id.clone(), table); Ok(id) } /// Tables keyed by `compiler.id`, plus the discovered compiler files in /// generated-output order (`(extends depth desc, stem)`). Callers that need output /// order (recognition rows, flag files) iterate the `Vec`; callers that only /// need a table by id use the map. type LoadedTables = (HashMap, Vec); /// True if `s` is a valid Rust identifier fragment: non-empty, first char an /// ASCII letter or `_`, the rest ASCII alphanumeric or `_`. fn is_ident(s: &str) -> bool { let mut chars = s.chars(); matches!(chars.next(), Some(c) if c.is_ascii_alphabetic() || c == '_') && chars.all(|c| c.is_ascii_alphanumeric() || c == '_') } /// Discover and load every compiler-kind YAML file in `flags_dir`, keyed by /// id, validated, and ordered for recognition. /// /// Files are found by scanning the directory and peeking each file's `type:` /// kind; wrapper-kind files are skipped here (they are loaded separately by /// [`load_wrapper_tables`]). There is no hand-maintained file list: a new /// compiler is picked up by the scan alone. /// /// Order is `(extends depth descending, stem ascending)`: a table that /// `extends` another is a specialization of it (its executable names may be /// caught by the base's broadened cross-compilation pattern), so it must be /// recognized first. Depth encodes exactly that -- a child's depth is always /// greater than its parent's -- so the "recognize specific before general" /// invariant falls out of the `extends` graph with no per-file annotation. /// /// `print_rerun` controls whether `cargo:rerun-if-changed` lines are /// printed (only meaningful when called from a `build.rs`). fn load_and_index(flags_dir: &Path, print_rerun: bool) -> Result { let mut yaml_files: Vec = std::fs::read_dir(flags_dir) .with_context(|| format!("reading directory {}", flags_dir.display()))? .filter_map(|entry| entry.ok()) .filter_map(|entry| entry.file_name().into_string().ok()) .filter(|name| name.ends_with(".yaml")) .collect(); yaml_files.sort(); let mut raw_tables = HashMap::new(); let mut compiler_files = Vec::new(); for yaml_file in yaml_files { let yaml_path = flags_dir.join(&yaml_file); if print_rerun { println!("cargo:rerun-if-changed={}", yaml_path.display()); } let content = std::fs::read_to_string(&yaml_path) .with_context(|| format!("reading {}", yaml_path.display()))?; let peek: KindPeek = serde_saphyr::from_str(&content).with_context(|| format!("parsing {}", yaml_file))?; // Wrapper-kind files are loaded by load_wrapper_tables, not here. if peek.type_ != Kind::Compiler { continue; } let stem = yaml_file.strip_suffix(".yaml").expect("filtered to .yaml above").to_string(); // The stem becomes an identifier fragment in generated names // (`gcc` -> `GCC_FLAGS`), so it must be a valid Rust identifier. // Catch a bad file name here with a clear message instead of a // cryptic rustc error in the consuming crate. if !is_ident(&stem) { bail!( "compiler file '{}' has an invalid stem '{}': the stem becomes part of a generated \ identifier, so it must be ASCII letters, digits, and underscores, not starting with a digit", yaml_file, stem ); } let table = parse_table(&yaml_file, &content)?; let id = insert_by_id(&mut raw_tables, &yaml_file, table)?; // Depth is filled in below, once every table is loaded and extends // targets can be resolved. compiler_files.push(CompilerFile { stem, id, depth: 0 }); } validate_extends(&raw_tables, &compiler_files)?; // Recognition order: specializations (higher extends depth) first, then // stem. See the doc comment above; this replaces the old hand-ordered // TABLES array with an order derived from the extends graph. for compiler_file in &mut compiler_files { compiler_file.depth = extends_depth(&raw_tables, &compiler_file.id)?; } compiler_files.sort_by(|a, b| b.depth.cmp(&a.depth).then_with(|| a.stem.cmp(&b.stem))); Ok((raw_tables, compiler_files)) } /// The `extends` depth of `id`: 0 for a root, one more than its parent /// otherwise. Walks the extends chain, which `validate_extends` has already /// proven leads only to existing tables; bails on a cycle so codegen (not /// just a test) rejects one. fn extends_depth(raw_tables: &HashMap, id: &str) -> Result { let mut seen: Vec<&str> = Vec::new(); let mut cursor = id; while let Some(parent) = raw_tables .get(cursor) .expect("extends targets exist after validate_extends") .compiler .extends .as_deref() { if seen.contains(&cursor) { bail!("extends cycle detected at '{}'", cursor); } seen.push(cursor); cursor = parent; } Ok(seen.len()) } /// Validate that every table's `compiler.extends` (if set) names an id /// that was actually loaded. Bails with a message naming the offending /// file so a dangling `extends:` target fails codegen, not just a test. pub fn validate_extends( raw_tables: &HashMap, compiler_files: &[CompilerFile], ) -> Result<()> { for compiler_file in compiler_files { let table = &raw_tables[&compiler_file.id]; if let Some(ref base) = table.compiler.extends && !raw_tables.contains_key(base.as_str()) { bail!("{} extends '{}', which does not exist", compiler_file.yaml_file(), base); } } Ok(()) } /// Load YAML flag tables from a directory, printing cargo:rerun-if-changed. fn load_tables_from(flags_dir: &Path) -> Result { load_and_index(flags_dir, true) } /// Just enough of a YAML file's shape to decide which full schema to parse /// it as. Serde ignores unknown fields by default, so this deserializes /// correctly regardless of the rest of the file. #[derive(serde::Deserialize)] struct KindPeek { #[serde(rename = "type")] type_: Kind, } /// Load every `type: wrapper` YAML file in `flags_dir`, sorted by file name /// for deterministic generated output. /// /// Like [`load_and_index`], this scans the directory and peeks each file's /// kind; it collects the wrapper-kind files and skips the compiler-kind ones /// (which `load_and_index` loads). Kind is declared data, so nothing needs a /// hand-maintained list to tell the two apart. /// /// `print_rerun` controls whether `cargo:rerun-if-changed` lines are /// printed (only meaningful when called from a `build.rs`). pub fn load_wrapper_tables(flags_dir: &Path, print_rerun: bool) -> Result> { let mut yaml_files: Vec = std::fs::read_dir(flags_dir) .with_context(|| format!("reading directory {}", flags_dir.display()))? .filter_map(|entry| entry.ok()) .filter_map(|entry| entry.file_name().into_string().ok()) .filter(|name| name.ends_with(".yaml")) .collect(); yaml_files.sort(); let mut wrapper_tables = Vec::new(); for yaml_file in yaml_files { let yaml_path = flags_dir.join(&yaml_file); if print_rerun { println!("cargo:rerun-if-changed={}", yaml_path.display()); } let content = std::fs::read_to_string(&yaml_path) .with_context(|| format!("reading {}", yaml_path.display()))?; let peek: KindPeek = serde_saphyr::from_str(&content).with_context(|| format!("parsing {}", yaml_file))?; match peek.type_ { Kind::Wrapper => { let table = parse_wrapper_table(&yaml_file, &content)?; table.validate(&yaml_file)?; wrapper_tables.push((yaml_file, table)); } // Compiler-kind files are loaded by load_and_index. Kind::Compiler => continue, } } Ok(wrapper_tables) } fn write_output(out_dir: &Path, filename: &str, content: String) -> Result<()> { let out_path = out_dir.join(filename); std::fs::write(&out_path, content).with_context(|| format!("writing {}", out_path.display()))?; Ok(()) } /// Path to the YAML flag definitions in the workspace. /// /// `CARGO_MANIFEST_DIR` is `/build-support/compilers-codegen`; the YAML /// lives at `/crates/semantic/compilers`, so walk up two levels to the /// workspace root. pub(crate) fn flags_dir() -> std::path::PathBuf { let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); let workspace_root = manifest_dir .ancestors() .nth(2) .expect("CARGO_MANIFEST_DIR is build-support/compilers-codegen, two levels below the workspace root"); workspace_root.join("crates/semantic/compilers") } /// Load all YAML flag tables from the workspace compilers directory, keyed /// by `compiler.id`. pub fn load_tables() -> Result> { let (raw_tables, _compiler_files) = load_and_index(&flags_dir(), false)?; Ok(raw_tables) } /// Discover the compiler-kind files in the workspace compilers directory, in /// generated-output order (`(extends depth desc, stem)`). Used by tests and codegen /// stages that need the ordered file list without the full flag tables. pub fn load_compiler_files() -> Result> { let (_raw_tables, compiler_files) = load_and_index(&flags_dir(), false)?; Ok(compiler_files) } #[cfg(test)] mod tests { use super::*; use crate::codegen::pattern_to_rust; use crate::yaml_types::{ CompilerIdentity, EnvEntry, EnvMapping, FlagMatch, Kind, RecognizeEntry, SourceMode, Syntax, }; // -- pattern_to_rust tests -- #[test] fn pattern_exactly_with_glued_or_sep() { assert_eq!(pattern_to_rust("-I{ }*", None), "FlagPattern::ExactlyWithGluedOrSep(\"-I\")"); } #[test] fn pattern_exactly_with_eq_or_sep() { assert_eq!(pattern_to_rust("-std{=}*", None), "FlagPattern::ExactlyWithEqOrSep(\"-std\")"); } #[test] fn pattern_exactly_with_colon_or_sep() { assert_eq!(pattern_to_rust("-MF{:}*", None), "FlagPattern::ExactlyWithColonOrSep(\"-MF\")"); } #[test] fn pattern_exactly_with_colon() { assert_eq!(pattern_to_rust("-Xclang:*", None), "FlagPattern::ExactlyWithColon(\"-Xclang\")"); } #[test] fn pattern_exactly_with_eq() { assert_eq!(pattern_to_rust("-std=*", None), "FlagPattern::ExactlyWithEq(\"-std\")"); } #[test] fn pattern_prefix_with_eq_and_count() { assert_eq!(pattern_to_rust("-std=*", Some(2)), "FlagPattern::Prefix(\"-std=\", 2)"); } #[test] fn pattern_prefix() { assert_eq!(pattern_to_rust("-Wall*", None), "FlagPattern::Prefix(\"-Wall\", 0)"); } #[test] fn pattern_exactly() { assert_eq!(pattern_to_rust("-c", None), "FlagPattern::Exactly(\"-c\", 0)"); } #[test] fn pattern_exactly_with_count() { assert_eq!(pattern_to_rust("-c", Some(1)), "FlagPattern::Exactly(\"-c\", 1)"); } // -- result_to_rust tests -- #[test] fn result_known_values() { assert!(result_to_rust("output").is_ok()); assert!(result_to_rust("configures_preprocessing").is_ok()); assert!(result_to_rust("configures_compiling").is_ok()); assert!(result_to_rust("configures_assembling").is_ok()); assert!(result_to_rust("configures_linking").is_ok()); assert!(result_to_rust("stops_at_preprocessing").is_ok()); assert!(result_to_rust("stops_at_compiling").is_ok()); assert!(result_to_rust("stops_at_assembling").is_ok()); assert!(result_to_rust("info_and_exit").is_ok()); assert!(result_to_rust("driver_option").is_ok()); assert!(result_to_rust("pass_through").is_ok()); assert!(result_to_rust("none").is_ok()); } #[test] fn result_unknown_is_err() { let err = result_to_rust("bogus").unwrap_err(); assert!(err.to_string().contains("unknown result value"), "{}", err); } // -- FlagMatch::name_len tests -- #[test] fn name_len_exact_with_glued() { let m = FlagMatch { pattern: "-std{=}*".to_string(), count: None }; assert_eq!(m.name_len(), 4); } #[test] fn name_len_exact() { let m = FlagMatch { pattern: "-std".to_string(), count: None }; assert_eq!(m.name_len(), 4); } #[test] fn name_len_eq_with_count() { let m = FlagMatch { pattern: "-o=*".to_string(), count: Some(1) }; assert_eq!(m.name_len(), 3); } #[test] fn name_len_prefix() { let m = FlagMatch { pattern: "-Wall*".to_string(), count: None }; assert_eq!(m.name_len(), 5); } // -- resolve tests -- #[test] fn resolve_environment_no_extends() { let raw_tables = load_tables().unwrap(); let entries = resolve_environment("gcc", &raw_tables); assert!(!entries.is_empty()); } #[test] fn resolve_environment_with_extends() { let raw_tables = load_tables().unwrap(); let clang_entries = resolve_environment("clang", &raw_tables); let gcc_entries = resolve_environment("gcc", &raw_tables); assert!(clang_entries.len() >= gcc_entries.len()); } #[test] fn resolve_environment_circular_safe() { let mut tables: HashMap = HashMap::new(); tables.insert( "a".to_string(), FlagTable { type_: Kind::Compiler, compiler: CompilerIdentity { id: "a".to_string(), extends: Some("b".to_string()) }, recognize: None, ignore_when: None, slash_prefix: None, flags: vec![], environment: Some(vec![make_test_env_entry("VAR_A")]), source_mode: SourceMode::default(), response_file_syntax: Syntax::default(), }, ); tables.insert( "b".to_string(), FlagTable { type_: Kind::Compiler, compiler: CompilerIdentity { id: "b".to_string(), extends: Some("a".to_string()) }, recognize: None, ignore_when: None, slash_prefix: None, flags: vec![], environment: Some(vec![make_test_env_entry("VAR_B")]), source_mode: SourceMode::default(), response_file_syntax: Syntax::default(), }, ); assert_eq!(resolve_environment("a", &tables).len(), 2); } #[test] fn resolve_ignore_when_no_extends() { let mut tables = HashMap::new(); tables.insert("leaf".to_string(), make_empty_table()); let result = resolve_ignore_when("leaf", &tables); assert!(result.executables.is_empty()); assert!(result.flags.is_empty()); } #[test] fn resolve_ignore_when_transitive() { let mut tables = HashMap::new(); let mut gp = make_empty_table(); gp.ignore_when = Some(IgnoreWhen { executables: vec!["cpp".to_string()], flags: vec!["-E".to_string()] }); tables.insert("gp".to_string(), gp); let mut parent = make_empty_table(); parent.compiler.extends = Some("gp".to_string()); tables.insert("parent".to_string(), parent); let mut child = make_empty_table(); child.compiler.extends = Some("parent".to_string()); tables.insert("child".to_string(), child); let result = resolve_ignore_when("child", &tables); assert_eq!(result.executables, vec!["cpp"]); assert_eq!(result.flags, vec!["-E"]); } #[test] fn resolve_ignore_when_own_overrides() { let mut tables = HashMap::new(); let mut base = make_empty_table(); base.ignore_when = Some(IgnoreWhen { executables: vec!["cpp".to_string()], flags: vec!["-E".to_string()] }); tables.insert("base".to_string(), base); let mut child = make_empty_table(); child.compiler.extends = Some("base".to_string()); child.ignore_when = Some(IgnoreWhen { executables: vec!["cc1".to_string()], flags: vec![] }); tables.insert("child".to_string(), child); let result = resolve_ignore_when("child", &tables); assert_eq!(result.executables, vec!["cc1"]); assert_eq!(result.flags, vec!["-E"]); } #[test] fn resolve_slash_prefix_default_is_false() { let mut tables = HashMap::new(); tables.insert("leaf".to_string(), make_empty_table()); assert!(!resolve_slash_prefix("leaf", &tables)); } #[test] fn resolve_slash_prefix_transitive() { let mut tables = HashMap::new(); let mut gp = make_empty_table(); gp.slash_prefix = Some(true); tables.insert("gp".to_string(), gp); let mut parent = make_empty_table(); parent.compiler.extends = Some("gp".to_string()); tables.insert("parent".to_string(), parent); let mut child = make_empty_table(); child.compiler.extends = Some("parent".to_string()); tables.insert("child".to_string(), child); assert!(resolve_slash_prefix("child", &tables)); } #[test] fn resolve_flags_transitive() { let mut tables = HashMap::new(); let mut gp = make_empty_table(); gp.flags = vec![make_test_flag("-gp", "output")]; tables.insert("gp".to_string(), gp); let mut parent = make_empty_table(); parent.compiler.extends = Some("gp".to_string()); parent.flags = vec![make_test_flag("-p", "output")]; tables.insert("parent".to_string(), parent); let mut child = make_empty_table(); child.compiler.extends = Some("parent".to_string()); child.flags = vec![make_test_flag("-ch", "output")]; tables.insert("child".to_string(), child); let flags = resolve_flags("child", &tables).unwrap(); assert_eq!(flags.len(), 3); assert_eq!(flags[0].match_.pattern, "-ch"); assert_eq!(flags[1].match_.pattern, "-p"); assert_eq!(flags[2].match_.pattern, "-gp"); } #[test] fn resolve_flags_dedup_same_result() { let mut tables = HashMap::new(); let mut parent = make_empty_table(); parent.flags = vec![make_test_flag("-c", "stops_at_compiling")]; tables.insert("parent".to_string(), parent); let mut child = make_empty_table(); child.compiler.extends = Some("parent".to_string()); child.flags = vec![make_test_flag("-c", "stops_at_compiling")]; tables.insert("child".to_string(), child); let flags = resolve_flags("child", &tables).unwrap(); assert_eq!(flags.len(), 1); } #[test] fn resolve_flags_conflict_is_err() { let mut tables = HashMap::new(); let mut parent = make_empty_table(); parent.flags = vec![make_test_flag("-c", "stops_at_compiling")]; tables.insert("parent".to_string(), parent); let mut child = make_empty_table(); child.compiler.extends = Some("parent".to_string()); child.flags = vec![make_test_flag("-c", "output")]; tables.insert("child".to_string(), child); let err = resolve_flags("child", &tables).unwrap_err(); assert!(err.to_string().contains("conflicting"), "{}", err); } #[test] fn resolve_flags_real_no_conflicts() { let raw_tables = load_tables().unwrap(); for compiler_file in load_compiler_files().unwrap() { resolve_flags(&compiler_file.id, &raw_tables).unwrap(); } } #[test] fn resolve_flags_real_ibm_xl_includes_gcc() { let raw_tables = load_tables().unwrap(); let ibm = resolve_flags("ibm_xl", &raw_tables).unwrap(); let gcc = resolve_flags("gcc", &raw_tables).unwrap(); for gf in &gcc { assert!( ibm.iter().any(|f| f.match_.pattern == gf.match_.pattern), "ibm_xl missing gcc flag: {}", gf.match_.pattern ); } } // -- RecognizeEntry::validate tests -- #[test] fn validate_recognize_entry_valid() { let e = RecognizeEntry { executables: vec!["gcc".to_string()], cross_compilation: true, versioned: true, description: "GCC compiler".to_string(), references: vec!["https://gcc.gnu.org/".to_string()], }; assert!(e.validate().is_ok()); } #[test] fn validate_recognize_entry_empty_description() { let e = RecognizeEntry { executables: vec!["gcc".to_string()], cross_compilation: false, versioned: false, description: " ".to_string(), references: vec!["https://x".to_string()], }; assert!(e.validate().unwrap_err().to_string().contains("description must not be empty")); } #[test] fn validate_recognize_entry_empty_reference() { let e = RecognizeEntry { executables: vec!["gcc".to_string()], cross_compilation: false, versioned: false, description: "GCC compiler".to_string(), references: vec![], }; assert!(e.validate().unwrap_err().to_string().contains("references must list")); } #[test] fn validate_recognize_entry_bad_url() { let e = RecognizeEntry { executables: vec!["gcc".to_string()], cross_compilation: false, versioned: false, description: "GCC compiler".to_string(), references: vec!["gcc.gnu.org".to_string()], }; assert!(e.validate().unwrap_err().to_string().contains("http(s) URL")); } // -- EnvEntry::validate tests -- #[test] fn validate_env_entry_valid() { let entry = make_test_env_entry("CPATH"); assert!(entry.validate().is_ok()); } #[test] fn validate_env_entry_invalid_name() { let mut entry = make_test_env_entry("CPATH"); entry.variable = "123BAD".to_string(); let err = entry.validate().unwrap_err(); assert!(err.to_string().contains("invalid environment variable name"), "{}", err); } #[test] fn validate_env_entry_unknown_effect() { let mut entry = make_test_env_entry("CPATH"); entry.effect = "bogus_effect".to_string(); let err = entry.validate().unwrap_err(); assert!(err.to_string().contains("unknown effect"), "{}", err); } #[test] fn validate_env_entry_both_flag_and_expand() { let mut entry = make_test_env_entry("CPATH"); entry.mapping.expand = Some("prepend".to_string()); let err = entry.validate().unwrap_err(); assert!(err.to_string().contains("both 'flag' and 'expand'"), "{}", err); } #[test] fn validate_env_entry_neither_flag_nor_expand() { let mut entry = make_test_env_entry("CPATH"); entry.mapping.flag = None; let err = entry.validate().unwrap_err(); assert!(err.to_string().contains("neither 'flag' nor 'expand'"), "{}", err); } #[test] fn validate_env_entry_unknown_separator() { let mut entry = make_test_env_entry("CPATH"); entry.mapping.separator = "comma".to_string(); let err = entry.validate().unwrap_err(); assert!(err.to_string().contains("unknown separator"), "{}", err); } // -- EnvMapping::to_rust tests -- #[test] fn env_mapping_to_rust_no_flag_no_expand_is_err() { let mapping = EnvMapping { flag: None, expand: None, separator: "path".to_string() }; let err = mapping.to_rust().unwrap_err(); assert!(err.to_string().contains("neither 'flag' nor 'expand'"), "{}", err); } #[test] fn env_mapping_to_rust_unknown_expand_is_err() { let mapping = EnvMapping { flag: None, expand: Some("middle".to_string()), separator: "path".to_string() }; let err = mapping.to_rust().unwrap_err(); assert!(err.to_string().contains("unknown expand position"), "{}", err); } // -- Integration test -- #[test] fn generate_from_real_yaml() { let out_dir = tempfile::tempdir().unwrap(); generate(&flags_dir(), out_dir.path()).unwrap(); for compiler_file in load_compiler_files().unwrap() { let names = compiler_file.names(); let path = out_dir.path().join(&names.output_file); let content = std::fs::read_to_string(&path) .unwrap_or_else(|_| panic!("Missing output file: {}", names.output_file)); assert!(!content.is_empty()); assert!(content.contains(&names.static_name)); } assert!( std::fs::read_to_string(out_dir.path().join("recognition.rs")) .unwrap() .contains("RECOGNITION_PATTERNS") ); assert!( std::fs::read_to_string(out_dir.path().join("env_keys.rs")) .unwrap() .contains("COMPILER_ENV_KEYS") ); let wrappers_rs = std::fs::read_to_string(out_dir.path().join("wrappers.rs")).unwrap(); assert!(wrappers_rs.contains("WRAPPER_NAMES")); assert!(wrappers_rs.contains("WRAPPER_OPTIONS")); assert!(wrappers_rs.contains("\"ccache\"")); assert!(wrappers_rs.contains("\"distcc\"")); let compiler_ids_rs = std::fs::read_to_string(out_dir.path().join("compiler_ids.rs")).unwrap(); assert!(compiler_ids_rs.contains("KNOWN_IDS")); assert!(compiler_ids_rs.contains("WRAPPER_AS_NAMES")); assert!(compiler_ids_rs.contains("\"gcc\"")); assert!(compiler_ids_rs.contains("\"ccache\"")); } #[test] fn generate_compiler_ids_lists_every_family_and_launcher() { let compiler_files = load_compiler_files().unwrap(); let wrapper_tables = load_wrapper_tables(&flags_dir(), false).unwrap(); let sut = generate_compiler_ids(&compiler_files, &wrapper_tables).unwrap(); // Every discovered compiler id appears; the four launcher basenames // appear as wrapper as-names; "wrapper" is the kind, never an id. for compiler_file in &compiler_files { assert!(sut.contains(&format!("\"{}\"", compiler_file.id)), "missing id {}", compiler_file.id); } for launcher in ["ccache", "distcc", "icecc", "sccache"] { assert!(sut.contains(&format!("\"{}\"", launcher)), "missing launcher {launcher}"); } assert!(sut.contains("KNOWN_IDS")); assert!(sut.contains("WRAPPER_AS_NAMES")); } #[test] fn discovery_orders_specializations_before_the_base() { // A family that `extends` another is recognized before it: ibm_xl and // clang_cl extend clang/msvc, so they precede their bases, and clang // (extends gcc) precedes gcc. The real constraint this protects is // ibm_xl before clang -- "ibm-clang" matches clang's cross-compilation // pattern, so clang must not be tried first. let compiler_files = load_compiler_files().unwrap(); let position = |id: &str| compiler_files.iter().position(|c| c.id == id).unwrap(); assert!(position("ibm_xl") < position("clang"), "ibm_xl must precede clang"); assert!(position("clang_cl") < position("msvc"), "clang_cl must precede msvc"); assert!(position("clang") < position("gcc"), "clang must precede gcc"); } #[test] fn extends_depth_of_a_chain_counts_hops() { let raw_tables = load_tables().unwrap(); // gcc is a root (0); clang extends gcc (1); ibm_xl extends clang (2). assert_eq!(extends_depth(&raw_tables, "gcc").unwrap(), 0); assert_eq!(extends_depth(&raw_tables, "clang").unwrap(), 1); assert_eq!(extends_depth(&raw_tables, "ibm_xl").unwrap(), 2); } #[test] fn extends_depth_bails_on_a_cycle() { let mut tables = HashMap::new(); for (id, parent) in [("a", "b"), ("b", "a")] { tables.insert( id.to_string(), FlagTable { type_: Kind::Compiler, compiler: CompilerIdentity { id: id.to_string(), extends: Some(parent.to_string()) }, recognize: None, ignore_when: None, slash_prefix: None, flags: vec![], environment: None, source_mode: SourceMode::default(), response_file_syntax: Syntax::default(), }, ); } let err = extends_depth(&tables, "a").unwrap_err(); assert!(err.to_string().contains("cycle"), "{}", err); } #[test] fn invalid_stem_fails_codegen() { // A file whose stem is not a valid identifier fails codegen with a // clear message, not a cryptic rustc error in the consuming crate. let dir = tempfile::tempdir().unwrap(); std::fs::write( dir.path().join("my-compiler.yaml"), "type: compiler\ncompiler:\n id: mycompiler\nflags: []\n", ) .unwrap(); let err = load_and_index(dir.path(), false).err().unwrap(); assert!(err.to_string().contains("my-compiler"), "{}", err); assert!(err.to_string().contains("invalid stem"), "{}", err); } #[test] fn compiler_id_reserved_wrapper_fails() { let compiler_files = vec![CompilerFile { stem: "wrapper".into(), id: "wrapper".into(), depth: 0 }]; let err = generate_compiler_ids(&compiler_files, &[]).unwrap_err(); assert!(err.to_string().contains("reserved"), "{}", err); } #[test] fn compiler_id_colliding_with_a_launcher_fails() { let compiler_files = vec![CompilerFile { stem: "ccache".into(), id: "ccache".into(), depth: 0 }]; let wrapper_tables = vec![( "ccache.yaml".to_string(), WrapperTable { type_: Kind::Wrapper, recognize: vec![RecognizeEntry { description: "Compiler cache".into(), references: vec!["https://ccache.dev/".into()], executables: vec!["ccache".into()], versioned: false, cross_compilation: false, }], options: vec![], }, )]; let err = generate_compiler_ids(&compiler_files, &wrapper_tables).unwrap_err(); assert!(err.to_string().contains("ccache"), "{}", err); assert!(err.to_string().contains("collides"), "{}", err); } // -- load_wrapper_tables tests -- #[test] fn load_wrapper_tables_finds_the_four_real_launcher_files() { let wrapper_tables = load_wrapper_tables(&flags_dir(), false).unwrap(); let mut names: Vec<&str> = wrapper_tables.iter().map(|(f, _)| f.as_str()).collect(); names.sort(); assert_eq!(names, vec!["ccache.yaml", "distcc.yaml", "icecc.yaml", "sccache.yaml"]); } #[test] fn load_wrapper_tables_skips_compiler_kind_files() { // A compiler-kind file is discovered by load_and_index, not the // wrapper pass; the wrapper pass simply skips it (no error). let dir = tempfile::tempdir().unwrap(); std::fs::write(dir.path().join("stray.yaml"), "type: compiler\ncompiler:\n id: stray\nflags: []\n") .unwrap(); let wrapper_tables = load_wrapper_tables(dir.path(), false).unwrap(); assert!( wrapper_tables.is_empty(), "compiler-kind file should be skipped, got {} rows", wrapper_tables.len() ); } // -- helpers -- fn make_empty_table() -> FlagTable { FlagTable { type_: Kind::Compiler, compiler: CompilerIdentity { id: "test".to_string(), extends: None }, recognize: None, ignore_when: None, slash_prefix: None, flags: vec![], environment: None, source_mode: SourceMode::default(), response_file_syntax: Syntax::default(), } } fn make_test_flag(pattern: &str, result: &str) -> FlagEntry { FlagEntry { match_: FlagMatch { pattern: pattern.to_string(), count: None }, result: result.to_string(), } } fn make_test_env_entry(var: &str) -> EnvEntry { EnvEntry { variable: var.to_string(), effect: "configures_compiling".to_string(), mapping: EnvMapping { flag: Some("-I".to_string()), expand: None, separator: "path".to_string() }, note: None, } } } rizsotto-Bear-0c30b27/build-support/compilers-codegen/src/recognition.rs000066400000000000000000000160611527177720300265040ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later use std::collections::HashMap; use anyhow::{Context, Result, bail}; use crate::tables::CompilerFile; use crate::yaml_types::{FlagTable, WrapperTable}; /// Generate a static array of recognition pattern data from all YAML files. /// /// Returns the generated Rust source as a string containing `RECOGNITION_PATTERNS`, /// a static array of `(&str, &[&str], bool, bool, Option<&str>)` tuples: /// (compiler_id, executables, cross_compilation, versioned, description). /// /// Executables listed in `ignore_when.executables` are automatically added as /// recognition entries with `(false, false)` so the recognizer can route them /// to the right compiler id (where the interpreter will then ignore them). /// /// `compiler_files` lists the discovered compiler-kind files in /// generated-output order (`(extends depth desc, stem)`); this walks them to emit the /// recognition rows in that order, looking `raw_tables` (keyed by id) up by /// each file's declared id. /// /// `wrapper_tables` supplies the compiler-launcher (`type: wrapper`) files; /// their `recognize` rows are appended after every compiler row, sorted by /// `(description, yaml_file)`, with the literal type column `"wrapper"` and /// `cross_compilation`/`versioned` forced to `false`. pub fn generate_recognition_patterns( raw_tables: &HashMap, compiler_files: &[CompilerFile], wrapper_tables: &[(String, WrapperTable)], ) -> Result { let mut out = String::new(); out.push_str("// Generated from compilers/*.yaml -- DO NOT EDIT\n"); out.push_str( "#[allow(clippy::type_complexity, reason = \"plain generated data, not an API to simplify with a type alias\")]\n", ); out.push_str("pub static RECOGNITION_PATTERNS: &[(&str, &[&str], bool, bool, Option<&str>)] = &[\n"); // Emit compiler rows in discovery order: specializations (higher extends // depth) before the family they extend, so the base's broad pattern never // captures a specialized name first. for compiler_file in compiler_files { let table = &raw_tables[compiler_file.id.as_str()]; let id_str = table.compiler.id.as_str(); // Emit explicit recognize entries if let Some(ref recognize_entries) = table.recognize { for entry in recognize_entries { entry .validate() .with_context(|| format!("recognize entry in {}", compiler_file.yaml_file()))?; let names_str: Vec = entry.executables.iter().map(|n| format!("\"{}\"", n)).collect(); out.push_str(&format!( " (\"{}\", &[{}], {}, {}, Some(\"{}\")),\n", id_str, names_str.join(", "), entry.cross_compilation, entry.versioned, escape(&entry.description), )); } } // Auto-add own ignore_when.executables as recognition entries (no cross-compilation, no version). // Only use the table's own list, not inherited - inherited executables are already // recognized under the base compiler id. if let Some(iw) = table.ignore_when.as_ref().filter(|iw| !iw.executables.is_empty()) { let exes = &iw.executables; let names_str: Vec = exes.iter().map(|n| format!("\"{}\"", n)).collect(); out.push_str(&format!( " (\"{}\", &[{}], false, false, None),\n", id_str, names_str.join(", "), )); } } // Wrapper (launcher) rows: flattened to one tuple per recognize entry, // then sorted by (description, yaml_file) so the four current launcher // files land in a stable, review-friendly order, still after every // compiler row. let mut wrapper_rows: Vec<(&str, &str, &crate::yaml_types::RecognizeEntry)> = Vec::new(); for (yaml_file, wrapper_table) in wrapper_tables { for entry in &wrapper_table.recognize { entry.validate().with_context(|| format!("recognize entry in {}", yaml_file))?; wrapper_rows.push((entry.description.as_str(), yaml_file.as_str(), entry)); } } wrapper_rows.sort_by(|a, b| (a.0, a.1).cmp(&(b.0, b.1))); for (_, _, entry) in wrapper_rows { let names_str: Vec = entry.executables.iter().map(|n| format!("\"{}\"", n)).collect(); out.push_str(&format!( " (\"wrapper\", &[{}], false, false, Some(\"{}\")),\n", names_str.join(", "), escape(&entry.description), )); } out.push_str("];\n"); Ok(out) } fn escape(s: &str) -> String { s.replace('\\', "\\\\").replace('"', "\\\"") } /// Generate the compiler-id data needed to resolve the config `as:` field: /// `KNOWN_IDS` (every real compiler family's `compiler.id`, in discovery /// order) and `WRAPPER_AS_NAMES` (the launcher basenames that also resolve to /// the one wrapper kind). Both are the sole accepted `as:` spellings; there /// are no aliases (see `compiler-family-definition`). /// /// Emitted as `pub(crate)` statics for the semantic analysis module to /// `include!`, so `CompilerType`'s spelling resolution validates against /// generated data instead of a hand-maintained mirror of the ids. pub fn generate_compiler_ids( compiler_files: &[CompilerFile], wrapper_tables: &[(String, WrapperTable)], ) -> Result { let ids: Vec<&str> = compiler_files.iter().map(|cf| cf.id.as_str()).collect(); let mut wrapper_names: Vec<&str> = wrapper_tables .iter() .flat_map(|(_, table)| table.recognize.iter()) .flat_map(|entry| entry.executables.iter().map(String::as_str)) .collect(); wrapper_names.sort_unstable(); wrapper_names.dedup(); // A compiler id must not collide with a wrapper `as:` spelling. Spelling // resolution maps "wrapper" and every launcher basename to the // wrapper kind BEFORE consulting KNOWN_IDS, so a colliding compiler id // would be silently shadowed -- `as: ` would resolve to the launcher. // Reject it at codegen with a clear message. for &id in &ids { if id == "wrapper" { bail!("compiler id 'wrapper' is reserved for the launcher kind; rename the family"); } if wrapper_names.contains(&id) { bail!( "compiler id '{}' collides with a wrapper launcher basename; `as: {}` would resolve \ to the launcher, shadowing the compiler -- rename one of them", id, id ); } } let mut out = String::new(); out.push_str("// Generated from compilers/*.yaml -- DO NOT EDIT\n"); out.push_str("pub(crate) static KNOWN_IDS: &[&str] = &["); out.push_str(&ids.iter().map(|id| format!("\"{}\"", id)).collect::>().join(", ")); out.push_str("];\n"); out.push_str("pub(crate) static WRAPPER_AS_NAMES: &[&str] = &["); out.push_str(&wrapper_names.iter().map(|name| format!("\"{}\"", name)).collect::>().join(", ")); out.push_str("];\n"); Ok(out) } rizsotto-Bear-0c30b27/build-support/compilers-codegen/src/resolve.rs000066400000000000000000000127361527177720300256500ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later use std::collections::{HashMap, HashSet}; use anyhow::{Result, bail}; use crate::yaml_types::{EnvEntry, FlagEntry, FlagTable, IgnoreWhen}; /// Resolve flags for a compiler, with transitive inheritance and dedup. /// /// Own flags come first, then base flags (recursively). Duplicate patterns /// (same pattern + count) are removed, keeping the child's version. If a /// pattern appears with conflicting results, returns an error. pub fn resolve_flags(key: &str, raw_tables: &HashMap) -> Result> { let mut visited = HashSet::new(); let all = resolve_flags_recursive(key, raw_tables, &mut visited); dedup_flags(all) } fn resolve_flags_recursive( key: &str, raw_tables: &HashMap, visited: &mut HashSet, ) -> Vec { if !visited.insert(key.to_string()) { return vec![]; } let table = &raw_tables[key]; let mut entries = table.flags.clone(); if let Some(ref base_name) = table.compiler.extends && raw_tables.contains_key(base_name.as_str()) { entries.extend(resolve_flags_recursive(base_name, raw_tables, visited)); } entries } /// Deduplicate flags by (pattern, count). Entries appear in priority order /// (own first), so the first occurrence wins. If a later entry has the /// same (pattern, count) but a different result, that is a conflict. fn dedup_flags(flags: Vec) -> Result> { let mut seen: HashMap<(String, Option), String> = HashMap::new(); let mut result = Vec::new(); for entry in flags { let key = (entry.match_.pattern.clone(), entry.match_.count); match seen.get(&key) { None => { seen.insert(key, entry.result.clone()); result.push(entry); } Some(prev_result) => { if *prev_result != entry.result { bail!( "flag '{}' has conflicting results: '{}' vs '{}'", entry.match_.pattern, prev_result, entry.result ); } } } } Ok(result) } /// Resolve `ignore_when` for a compiler, with transitive inheritance. /// /// Walks the extends chain. At each level, a non-empty own list takes /// precedence over the inherited list (per field independently). pub fn resolve_ignore_when(key: &str, raw_tables: &HashMap) -> IgnoreWhen { let mut visited = HashSet::new(); resolve_ignore_when_recursive(key, raw_tables, &mut visited) } fn resolve_ignore_when_recursive( key: &str, raw_tables: &HashMap, visited: &mut HashSet, ) -> IgnoreWhen { if !visited.insert(key.to_string()) { return IgnoreWhen::default(); } let table = &raw_tables[key]; let own = table.ignore_when.clone().unwrap_or_default(); if let Some(ref base_name) = table.compiler.extends && raw_tables.contains_key(base_name.as_str()) { let base = resolve_ignore_when_recursive(base_name, raw_tables, visited); return IgnoreWhen { executables: if own.executables.is_empty() { base.executables } else { own.executables }, flags: if own.flags.is_empty() { base.flags } else { own.flags }, }; } own } /// Resolve `slash_prefix` for a compiler, with transitive inheritance. /// /// Returns the first explicit value found walking up the extends chain, /// or `false` if no table in the chain sets it. pub fn resolve_slash_prefix(key: &str, raw_tables: &HashMap) -> bool { let mut visited = HashSet::new(); resolve_slash_prefix_recursive(key, raw_tables, &mut visited) } fn resolve_slash_prefix_recursive( key: &str, raw_tables: &HashMap, visited: &mut HashSet, ) -> bool { if !visited.insert(key.to_string()) { return false; } let table = &raw_tables[key]; if let Some(value) = table.slash_prefix { return value; } if let Some(ref base_name) = table.compiler.extends && raw_tables.contains_key(base_name.as_str()) { return resolve_slash_prefix_recursive(base_name, raw_tables, visited); } false } /// Resolve environment entries for a compiler, with transitive inheritance. /// /// Walks the `extends` chain recursively, collecting environment entries. /// Own entries override inherited ones matched by variable name. pub fn resolve_environment(key: &str, raw_tables: &HashMap) -> Vec { let mut visited = HashSet::new(); resolve_environment_recursive(key, raw_tables, &mut visited) } fn resolve_environment_recursive( key: &str, raw_tables: &HashMap, visited: &mut HashSet, ) -> Vec { if !visited.insert(key.to_string()) { return vec![]; } let table = &raw_tables[key]; let mut entries = table.environment.clone().unwrap_or_default(); if let Some(ref base_name) = table.compiler.extends && raw_tables.contains_key(base_name.as_str()) { let base_entries = resolve_environment_recursive(base_name, raw_tables, visited); let own_vars: HashSet = entries.iter().map(|e| e.variable.clone()).collect(); for entry in base_entries { if !own_vars.contains(&entry.variable) { entries.push(entry); } } } entries } rizsotto-Bear-0c30b27/build-support/compilers-codegen/src/tables.rs000066400000000000000000000045521527177720300254400ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Compiler-table descriptors, derived from the YAML file stem. //! //! Compiler files are discovered by scanning `compilers/*.yaml` (see //! `lib.rs::load_and_index`), not listed in a hand-maintained array. Every //! generated static name and output file name derives from the file stem by //! a fixed convention, so a new compiler is a YAML-only addition. /// The generated static names and output file for one compiler table, all /// derived from its YAML file stem (`gcc` -> `GCC_FLAGS`, `flags_gcc.rs`, /// ...). `flag_based.rs` `include!`s the output file and references the /// static names, so this convention is load-bearing: it must match the /// names that module expects. pub struct TableNames { pub yaml_file: String, pub static_name: String, pub ignore_executables_name: String, pub ignore_flags_name: String, pub slash_prefix_name: String, pub env_rules_name: String, pub output_file: String, } impl TableNames { /// Derive every generated name from the file stem. pub fn from_stem(stem: &str) -> Self { let upper = stem.to_uppercase(); TableNames { yaml_file: format!("{stem}.yaml"), static_name: format!("{upper}_FLAGS"), ignore_executables_name: format!("{upper}_IGNORE_EXECUTABLES"), ignore_flags_name: format!("{upper}_IGNORE_FLAGS"), slash_prefix_name: format!("{upper}_SLASH_PREFIX"), env_rules_name: format!("{upper}_ENV_RULES"), output_file: format!("flags_{stem}.rs"), } } } /// One discovered compiler-kind YAML file: its stem (packaging, drives the /// generated names), its declared `compiler.id` (identity, drives recognition /// and lookup), and its `extends` depth (0 for a root, one more than its /// parent). Discovery sorts by `(depth descending, stem)`, so a specialization /// (higher depth) is always recognized before the family it extends -- the /// order of the generated recognition rows. pub struct CompilerFile { pub stem: String, pub id: String, pub depth: usize, } impl CompilerFile { /// The generated names for this file. pub fn names(&self) -> TableNames { TableNames::from_stem(&self.stem) } /// The source YAML file name (`.yaml`). pub fn yaml_file(&self) -> String { format!("{}.yaml", self.stem) } } rizsotto-Bear-0c30b27/build-support/compilers-codegen/src/wrappers.rs000066400000000000000000000036071527177720300260310ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later use crate::yaml_types::WrapperTable; /// Generate `WRAPPER_NAMES` (every wrapper's recognized executables, /// flattened and sorted for deterministic output) and `WRAPPER_OPTIONS` /// (per-executable argv options to skip before the real compiler, emitted /// only for wrappers that declare any -- today, distcc). /// /// `wrapper_tables` is `(yaml_file, WrapperTable)` pairs, sorted by file /// name by the caller; wrapper tables carry no identity block, so "wrapper /// name" here means an executable listed under `recognize[].executables`. pub fn generate_wrappers(wrapper_tables: &[(String, WrapperTable)]) -> String { let mut names: Vec<&str> = wrapper_tables .iter() .flat_map(|(_, table)| table.recognize.iter()) .flat_map(|entry| entry.executables.iter()) .map(|s| s.as_str()) .collect(); names.sort(); let mut out = String::new(); out.push_str("// Generated from compilers/*.yaml -- DO NOT EDIT\n"); out.push_str(&format!( "pub(super) static WRAPPER_NAMES: &[&str] = &[{}];\n", names.iter().map(|n| format!("\"{}\"", n)).collect::>().join(", ") )); out.push_str("// (name, &[(option, arity)]) -- arity = following argv tokens consumed\n"); out.push_str("pub(super) static WRAPPER_OPTIONS: &[(&str, &[(&str, u32)])] = &[\n"); for (_, table) in wrapper_tables { if table.options.is_empty() { continue; } let opts: Vec = table .options .iter() .map(|opt| format!("(\"{}\", {})", opt.match_.pattern, opt.match_.count.unwrap_or(0))) .collect(); for entry in &table.recognize { for exe in &entry.executables { out.push_str(&format!(" (\"{}\", &[{}]),\n", exe, opts.join(", "))); } } } out.push_str("];\n"); out } rizsotto-Bear-0c30b27/build-support/compilers-codegen/src/yaml_types.rs000066400000000000000000000261431527177720300263540ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later use anyhow::{Context, Result, bail}; use serde::Deserialize; /// Which schema a YAML file follows. Closed: `compiler` is a compiler /// family (`FlagTable`); `wrapper` is a compiler launcher (`WrapperTable`). /// Both kinds are discovered by scanning the directory and peeking this /// field; neither needs a hand-maintained file list. #[derive(Deserialize, Clone, Copy, PartialEq, Eq, Debug)] #[serde(rename_all = "snake_case")] pub enum Kind { Compiler, Wrapper, } /// The family identity of a compiler table: a unique `id` (the `extends:` /// target, emitted into `RECOGNITION_PATTERNS`, and mirrors the config /// `as:` value) and an optional `extends:` reference to another table's id. /// /// `extends` also fixes recognition order: a table that extends another is a /// specialization of it, so it is recognized first (see `load_and_index`). /// A new vendor variant of clang/gcc therefore gets correct ordering for /// free from the `extends` it needs for flag inheritance anyway -- there is /// no separate priority to set. #[derive(Deserialize, Clone)] pub struct CompilerIdentity { pub id: String, #[serde(default)] pub extends: Option, } #[derive(Deserialize)] pub struct FlagTable { #[serde(rename = "type")] pub type_: Kind, pub compiler: CompilerIdentity, pub recognize: Option>, pub ignore_when: Option, /// When true, arguments starting with '/' are treated as flags (MSVC-style). #[serde(default)] pub slash_prefix: Option, pub flags: Vec, #[serde(default)] pub environment: Option>, /// How this family's invocations map to database entries (post-parse). /// Not inherited through `extends`; each family states its own. #[serde(default)] pub source_mode: SourceMode, /// Response-file (`@file`) tokenization convention for this family. /// Not inherited through `extends`; each family states its own. #[serde(default)] pub response_file_syntax: Syntax, } /// How a family's sources map to compilation-database entries. Closed enum; /// the parse-side twin of the `bear` crate's runtime `SourceMode`, which /// [`SourceMode::to_rust`] emits. Default `per-source-stripped` covers /// GCC/Clang and most families. #[derive(Deserialize, Clone, Copy, Default, PartialEq, Eq, Debug)] #[serde(rename_all = "kebab-case")] pub enum SourceMode { #[default] PerSourceStripped, PerSourceFull, Combined, } impl SourceMode { /// The generated `SourceMode` expression for this value. pub fn to_rust(self) -> &'static str { match self { SourceMode::PerSourceStripped => "SourceMode::PerSourceStripped", SourceMode::PerSourceFull => "SourceMode::PerSourceFull", SourceMode::Combined => "SourceMode::Combined", } } } /// Response-file tokenization convention. Closed enum; the parse-side twin /// of the `bear` crate's runtime `Syntax`, which [`Syntax::to_rust`] emits. /// Default `gnu` covers every family except the MSVC-style ones (`msvc`, /// `clang-cl`). #[derive(Deserialize, Clone, Copy, Default, PartialEq, Eq, Debug)] #[serde(rename_all = "lowercase")] pub enum Syntax { #[default] Gnu, Msvc, } impl Syntax { /// The generated `Syntax` expression for this value. pub fn to_rust(self) -> &'static str { match self { Syntax::Gnu => "Syntax::GnuClang", Syntax::Msvc => "Syntax::Msvc", } } } /// A compiler-launcher YAML file (`ccache.yaml`, `distcc.yaml`, ...): /// no identity block, no flags -- just what it is recognized as and, /// optionally, its own argv options to skip before the real compiler. #[derive(Deserialize)] pub struct WrapperTable { #[serde(rename = "type")] pub type_: Kind, pub recognize: Vec, #[serde(default)] pub options: Vec, } impl WrapperTable { /// Validate a wrapper table: every `recognize` entry passes the usual /// description/references check and has `versioned`/`cross_compilation` /// both false (a launcher basename is matched exactly, never version- /// suffixed or treated as a cross-compilation prefix); every `options` /// entry's pattern is an exact token, not a glued/prefix/eq/colon form /// (the skip loop only compares argv tokens for equality). pub fn validate(&self, yaml_file: &str) -> Result<()> { for entry in &self.recognize { entry.validate().with_context(|| format!("recognize entry in {}", yaml_file))?; if entry.versioned { bail!( "{}: recognize entry {:?} must not set versioned: true for a wrapper", yaml_file, entry.executables ); } if entry.cross_compilation { bail!( "{}: recognize entry {:?} must not set cross_compilation: true for a wrapper", yaml_file, entry.executables ); } } for option in &self.options { let pattern = &option.match_.pattern; if pattern.contains('*') || pattern.contains('{') { bail!( "{}: wrapper option pattern '{}' must be an exact token (no '*' or '{{ }}' forms)", yaml_file, pattern ); } } Ok(()) } } #[derive(Deserialize, Clone, Debug)] pub struct WrapperOption { #[serde(rename = "match")] pub match_: FlagMatch, } #[derive(Deserialize, Clone)] pub struct RecognizeEntry { pub description: String, pub references: Vec, pub executables: Vec, #[serde(default)] pub versioned: bool, #[serde(default)] pub cross_compilation: bool, } impl RecognizeEntry { /// Validate the enrichment fields: a non-empty description and at /// least one http(s) reference URL. Enforced at codegen time so a /// compiler cannot be added without describing and citing it. pub fn validate(&self) -> Result<()> { if self.description.trim().is_empty() { bail!("recognize entry {:?}: description must not be empty", self.executables); } if self.references.is_empty() { bail!("recognize entry {:?}: references must list at least one URL", self.executables); } for url in &self.references { if !(url.starts_with("http://") || url.starts_with("https://")) { bail!("recognize entry {:?}: reference '{}' must be an http(s) URL", self.executables, url); } } Ok(()) } } #[derive(Deserialize, Clone, Default)] pub struct IgnoreWhen { #[serde(default)] pub executables: Vec, #[serde(default)] pub flags: Vec, } #[derive(Deserialize, Clone, Debug)] pub struct FlagEntry { #[serde(rename = "match")] pub match_: FlagMatch, pub result: String, } #[derive(Deserialize, Clone, Debug)] pub struct FlagMatch { pub pattern: String, pub count: Option, } impl FlagMatch { /// Compute the flag name length as `FlagPattern::flag()` would return it. pub fn name_len(&self) -> usize { let pattern = &self.pattern; if let Some(flag) = pattern.strip_suffix("{ }*") { flag.len() } else if let Some(flag) = pattern.strip_suffix("{=}*") { flag.len() } else if let Some(flag) = pattern.strip_suffix("{:}*") { flag.len() } else if let Some(flag) = pattern.strip_suffix(":*") { flag.len() } else if let Some(flag) = pattern.strip_suffix("=*") { if self.count.is_some() { flag.len() + 1 } else { flag.len() } } else if let Some(flag) = pattern.strip_suffix('*') { flag.len() } else { pattern.len() } } } #[derive(Deserialize, Clone)] pub struct EnvEntry { pub variable: String, pub effect: String, pub mapping: EnvMapping, #[serde(default)] pub note: Option, } /// Validate that `s` is a C-style identifier: /// non-empty, first char is ASCII letter or `_`, remaining chars /// are ASCII alphanumeric or `_`. fn is_valid_var_name(s: &str) -> bool { let mut chars = s.chars(); matches!(chars.next(), Some(c) if c.is_ascii_alphabetic() || c == '_') && chars.all(|c| c.is_ascii_alphanumeric() || c == '_') } impl EnvEntry { /// Validate this environment entry against the schema. pub fn validate(&self) -> Result<()> { if !is_valid_var_name(&self.variable) { bail!("invalid environment variable name '{}'", self.variable); } match self.effect.as_str() { "configures_preprocessing" | "configures_compiling" | "configures_assembling" | "configures_linking" | "stops_at_preprocessing" | "stops_at_compiling" | "stops_at_assembling" | "info_and_exit" | "driver_option" | "none" => {} other => bail!("variable '{}': unknown effect '{}'", self.variable, other), } let mapping = &self.mapping; if mapping.flag.is_some() && mapping.expand.is_some() { bail!("variable '{}': has both 'flag' and 'expand'", self.variable); } if mapping.flag.is_none() && mapping.expand.is_none() && self.effect != "none" { bail!("variable '{}': has neither 'flag' nor 'expand' (and effect is not 'none')", self.variable); } match mapping.separator.as_str() { "path" | "space" | ";" => {} other => bail!("variable '{}': unknown separator '{}'", self.variable, other), } if let Some(ref expand) = mapping.expand { match expand.as_str() { "prepend" | "append" => {} other => bail!("variable '{}': unknown expand position '{}'", self.variable, other), } } Ok(()) } } #[derive(Deserialize, Clone)] pub struct EnvMapping { #[serde(default)] pub flag: Option, #[serde(default)] pub expand: Option, pub separator: String, } impl EnvMapping { /// Convert this mapping to a Rust `EnvMapping` expression string. pub fn to_rust(&self) -> Result { if let Some(ref flag) = self.flag { let sep = match self.separator.as_str() { "path" => "EnvSeparator::Path".to_string(), ";" => "EnvSeparator::Fixed(\";\")".to_string(), other => format!("EnvSeparator::Fixed(\"{}\")", other), }; Ok(format!("EnvMapping::Flag {{ flag: \"{}\", separator: {} }}", flag, sep)) } else if let Some(ref expand) = self.expand { let pos = match expand.as_str() { "prepend" => "EnvPosition::Prepend", "append" => "EnvPosition::Append", other => bail!("unknown expand position '{}'", other), }; Ok(format!("EnvMapping::Expand {{ position: {} }}", pos)) } else { bail!("mapping has neither 'flag' nor 'expand'") } } } rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/000077500000000000000000000000001527177720300241655ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/proptest_patterns.rs000066400000000000000000000107141527177720300303360ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Property-based tests for pattern_to_rust and FlagMatch::name_len. #![allow(clippy::unwrap_used, reason = "the whole file is test code")] use compilers_codegen::codegen::pattern_to_rust; use compilers_codegen::yaml_types::FlagMatch; use proptest::prelude::*; /// Strategy for generating flag name strings (e.g., "-I", "--std", "/Fo"). fn flag_name_strategy() -> impl Strategy { prop::string::string_regex("-{1,2}[A-Za-z][A-Za-z0-9_-]{0,15}") .unwrap() .prop_filter("must not be empty after prefix", |s| s.len() > 1) } /// Strategy for generating optional count values. fn count_strategy() -> impl Strategy> { prop_oneof![Just(None), (0u32..10).prop_map(Some)] } /// Strategy for generating pattern suffixes. fn suffix_strategy() -> impl Strategy { prop_oneof![Just(""), Just("*"), Just("=*"), Just(":*"), Just("{ }*"), Just("{=}*"), Just("{:}*"),] } // pattern_to_rust always produces output starting with "FlagPattern::". proptest! { #[test] fn output_starts_with_flag_pattern( flag in flag_name_strategy(), suffix in suffix_strategy(), count in count_strategy(), ) { let pattern = format!("{}{}", flag, suffix); let output = pattern_to_rust(&pattern, count); prop_assert!( output.starts_with("FlagPattern::"), "Expected FlagPattern:: prefix, got: {}", output ); } } // pattern_to_rust maps each suffix to the expected variant. proptest! { #[test] fn suffix_determines_variant( flag in flag_name_strategy(), count in count_strategy(), ) { // { }* -> ExactlyWithGluedOrSep let output = pattern_to_rust(&format!("{}{{ }}*", flag), count); prop_assert!(output.contains("ExactlyWithGluedOrSep"), "{}", output); // {=}* -> ExactlyWithEqOrSep let output = pattern_to_rust(&format!("{}{{=}}*", flag), count); prop_assert!(output.contains("ExactlyWithEqOrSep"), "{}", output); // {:}* -> ExactlyWithColonOrSep let output = pattern_to_rust(&format!("{}{{:}}*", flag), count); prop_assert!(output.contains("ExactlyWithColonOrSep"), "{}", output); // :* -> ExactlyWithColon let output = pattern_to_rust(&format!("{}:*", flag), count); prop_assert!(output.contains("ExactlyWithColon"), "{}", output); } } // =* without count -> ExactlyWithEq; with count -> Prefix. proptest! { #[test] fn eq_star_variant_depends_on_count(flag in flag_name_strategy()) { let pattern = format!("{}=*", flag); let without_count = pattern_to_rust(&pattern, None); prop_assert!(without_count.contains("ExactlyWithEq"), "{}", without_count); let with_count = pattern_to_rust(&pattern, Some(1)); prop_assert!(with_count.contains("Prefix"), "{}", with_count); } } // Bare flag (no suffix) -> Exactly. proptest! { #[test] fn bare_flag_produces_exactly(flag in flag_name_strategy(), count in count_strategy()) { let output = pattern_to_rust(&flag, count); prop_assert!(output.contains("Exactly("), "Expected Exactly, got: {}", output); } } // flag* (star suffix, no separator) -> Prefix. proptest! { #[test] fn star_suffix_produces_prefix(flag in flag_name_strategy(), count in count_strategy()) { let pattern = format!("{}*", flag); let output = pattern_to_rust(&pattern, count); prop_assert!(output.contains("Prefix("), "Expected Prefix, got: {}", output); } } // FlagMatch::name_len is always <= the pattern length. proptest! { #[test] fn name_len_bounded_by_pattern( flag in flag_name_strategy(), suffix in suffix_strategy(), count in count_strategy(), ) { let pattern = format!("{}{}", flag, suffix); let m = FlagMatch { pattern: pattern.clone(), count }; let len = m.name_len(); prop_assert!( len <= pattern.len(), "name_len({}) = {} > pattern.len() = {}", pattern, len, pattern.len() ); } } // FlagMatch::name_len is always > 0 for non-empty patterns. proptest! { #[test] fn name_len_positive( flag in flag_name_strategy(), suffix in suffix_strategy(), count in count_strategy(), ) { let pattern = format!("{}{}", flag, suffix); let m = FlagMatch { pattern, count }; prop_assert!(m.name_len() > 0); } } rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/snapshots.rs000066400000000000000000000054021527177720300265560ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Snapshot tests for all generated Rust source files. //! //! Each test generates one output file from the real YAML definitions and //! compares it against a stored snapshot. Any change in YAML or codegen //! logic is caught as a snapshot diff. use compilers_codegen::env_keys::generate_env_keys; use compilers_codegen::families::generate_families; use compilers_codegen::recognition::{generate_compiler_ids, generate_recognition_patterns}; use compilers_codegen::{ResolvedTable, load_compiler_files, load_tables, load_wrapper_tables}; /// One snapshot per discovered compiler flag table, named /// `snapshot_flags_` so it lands in the same file the former /// hand-written per-family `snapshot_flags_*` functions produced. A new /// compiler YAML auto-gains a snapshot on the next run; accept the diff. #[test] fn snapshot_flags() { let raw_tables = load_tables().unwrap(); for compiler_file in load_compiler_files().unwrap() { let generated = ResolvedTable::new(&compiler_file, &raw_tables).unwrap().generate().unwrap(); insta::assert_snapshot!(format!("snapshot_flags_{}", compiler_file.stem), generated); } } #[test] fn snapshot_recognition() { let raw_tables = load_tables().unwrap(); let compiler_files = load_compiler_files().unwrap(); let wrapper_tables = load_wrapper_tables(&flags_dir(), false).unwrap(); insta::assert_snapshot!( generate_recognition_patterns(&raw_tables, &compiler_files, &wrapper_tables).unwrap() ); } /// Path to the real compiler-definitions directory. Mirrors the library's /// private `flags_dir()` helper (`pub(crate)`, not visible to this /// integration-test binary); `CARGO_MANIFEST_DIR` here is the same /// `build-support/compilers-codegen` directory, so the walk-up is identical. fn flags_dir() -> std::path::PathBuf { let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); let workspace_root = manifest_dir .ancestors() .nth(2) .expect("CARGO_MANIFEST_DIR is build-support/compilers-codegen, two levels below the workspace root"); workspace_root.join("crates/semantic/compilers") } #[test] fn snapshot_env_keys() { let raw_tables = load_tables().unwrap(); insta::assert_snapshot!(generate_env_keys(&raw_tables)); } #[test] fn snapshot_compiler_ids() { let compiler_files = load_compiler_files().unwrap(); let wrapper_tables = load_wrapper_tables(&flags_dir(), false).unwrap(); insta::assert_snapshot!(generate_compiler_ids(&compiler_files, &wrapper_tables).unwrap()); } #[test] fn snapshot_families() { let raw_tables = load_tables().unwrap(); let compiler_files = load_compiler_files().unwrap(); insta::assert_snapshot!(generate_families(&compiler_files, &raw_tables)); } rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/snapshots/000077500000000000000000000000001527177720300262075ustar00rootroot00000000000000snapshots__snapshot_compiler_ids.snap000066400000000000000000000011141527177720300356410ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/snapshots--- source: build-support/compilers-codegen/tests/snapshots.rs assertion_line: 60 expression: "generate_compiler_ids(&compiler_files, &wrapper_tables).unwrap()" --- // Generated from compilers/*.yaml -- DO NOT EDIT pub(crate) static KNOWN_IDS: &[&str] = &["arm_flang", "armclang", "cray_cc", "embarcadero", "ibm_xl", "intel_cc", "intel_cl", "clang", "clang_cl", "cuda", "flang", "mpi", "qnx", "cray_fortran", "fasm", "gcc", "intel_fortran", "msvc", "nasm", "nvidia_hpc", "swift", "ti_cgt", "vala"]; pub(crate) static WRAPPER_AS_NAMES: &[&str] = &["ccache", "distcc", "icecc", "sccache"]; snapshots__snapshot_env_keys.snap000066400000000000000000000005601527177720300350170ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/snapshots--- source: bear-codegen/tests/snapshots.rs expression: generate_env_keys(&raw_tables) --- // Generated from compilers/*.yaml -- DO NOT EDIT static COMPILER_ENV_KEYS: [&str; 10] = [ "ARMCOMPILER6_CLANGOPT", "CL", "CPATH", "CPLUS_INCLUDE_PATH", "C_INCLUDE_PATH", "INCLUDE", "LIB", "LIBRARY_PATH", "OBJC_INCLUDE_PATH", "_CL_", ]; snapshots__snapshot_families.snap000066400000000000000000000200031527177720300347570ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/snapshots--- source: build-support/compilers-codegen/tests/snapshots.rs assertion_line: 67 expression: "generate_families(&compiler_files, &raw_tables)" --- // Generated from compilers/*.yaml -- DO NOT EDIT include!(concat!(env!("OUT_DIR"), "/flags_arm_flang.rs")); include!(concat!(env!("OUT_DIR"), "/flags_armclang.rs")); include!(concat!(env!("OUT_DIR"), "/flags_cray_cc.rs")); include!(concat!(env!("OUT_DIR"), "/flags_embarcadero.rs")); include!(concat!(env!("OUT_DIR"), "/flags_ibm_xl.rs")); include!(concat!(env!("OUT_DIR"), "/flags_intel_cc.rs")); include!(concat!(env!("OUT_DIR"), "/flags_intel_cl.rs")); include!(concat!(env!("OUT_DIR"), "/flags_clang.rs")); include!(concat!(env!("OUT_DIR"), "/flags_clang_cl.rs")); include!(concat!(env!("OUT_DIR"), "/flags_cuda.rs")); include!(concat!(env!("OUT_DIR"), "/flags_flang.rs")); include!(concat!(env!("OUT_DIR"), "/flags_mpi.rs")); include!(concat!(env!("OUT_DIR"), "/flags_qnx.rs")); include!(concat!(env!("OUT_DIR"), "/flags_cray_fortran.rs")); include!(concat!(env!("OUT_DIR"), "/flags_fasm.rs")); include!(concat!(env!("OUT_DIR"), "/flags_gcc.rs")); include!(concat!(env!("OUT_DIR"), "/flags_intel_fortran.rs")); include!(concat!(env!("OUT_DIR"), "/flags_msvc.rs")); include!(concat!(env!("OUT_DIR"), "/flags_nasm.rs")); include!(concat!(env!("OUT_DIR"), "/flags_nvidia_hpc.rs")); include!(concat!(env!("OUT_DIR"), "/flags_swift.rs")); include!(concat!(env!("OUT_DIR"), "/flags_ti_cgt.rs")); include!(concat!(env!("OUT_DIR"), "/flags_vala.rs")); pub(super) static FAMILIES: &[FamilyDef] = &[ FamilyDef { id: "arm_flang", flags: &ARM_FLANG_FLAGS, ignore_executables: &ARM_FLANG_IGNORE_EXECUTABLES, ignore_flags: &ARM_FLANG_IGNORE_FLAGS, slash_prefix: ARM_FLANG_SLASH_PREFIX, env_rules: &ARM_FLANG_ENV_RULES, source_mode: SourceMode::PerSourceStripped, response_file_syntax: Syntax::GnuClang }, FamilyDef { id: "armclang", flags: &ARMCLANG_FLAGS, ignore_executables: &ARMCLANG_IGNORE_EXECUTABLES, ignore_flags: &ARMCLANG_IGNORE_FLAGS, slash_prefix: ARMCLANG_SLASH_PREFIX, env_rules: &ARMCLANG_ENV_RULES, source_mode: SourceMode::PerSourceStripped, response_file_syntax: Syntax::GnuClang }, FamilyDef { id: "cray_cc", flags: &CRAY_CC_FLAGS, ignore_executables: &CRAY_CC_IGNORE_EXECUTABLES, ignore_flags: &CRAY_CC_IGNORE_FLAGS, slash_prefix: CRAY_CC_SLASH_PREFIX, env_rules: &CRAY_CC_ENV_RULES, source_mode: SourceMode::PerSourceStripped, response_file_syntax: Syntax::GnuClang }, FamilyDef { id: "embarcadero", flags: &EMBARCADERO_FLAGS, ignore_executables: &EMBARCADERO_IGNORE_EXECUTABLES, ignore_flags: &EMBARCADERO_IGNORE_FLAGS, slash_prefix: EMBARCADERO_SLASH_PREFIX, env_rules: &EMBARCADERO_ENV_RULES, source_mode: SourceMode::PerSourceStripped, response_file_syntax: Syntax::GnuClang }, FamilyDef { id: "ibm_xl", flags: &IBM_XL_FLAGS, ignore_executables: &IBM_XL_IGNORE_EXECUTABLES, ignore_flags: &IBM_XL_IGNORE_FLAGS, slash_prefix: IBM_XL_SLASH_PREFIX, env_rules: &IBM_XL_ENV_RULES, source_mode: SourceMode::PerSourceStripped, response_file_syntax: Syntax::GnuClang }, FamilyDef { id: "intel_cc", flags: &INTEL_CC_FLAGS, ignore_executables: &INTEL_CC_IGNORE_EXECUTABLES, ignore_flags: &INTEL_CC_IGNORE_FLAGS, slash_prefix: INTEL_CC_SLASH_PREFIX, env_rules: &INTEL_CC_ENV_RULES, source_mode: SourceMode::PerSourceStripped, response_file_syntax: Syntax::GnuClang }, FamilyDef { id: "intel_cl", flags: &INTEL_CL_FLAGS, ignore_executables: &INTEL_CL_IGNORE_EXECUTABLES, ignore_flags: &INTEL_CL_IGNORE_FLAGS, slash_prefix: INTEL_CL_SLASH_PREFIX, env_rules: &INTEL_CL_ENV_RULES, source_mode: SourceMode::PerSourceStripped, response_file_syntax: Syntax::Msvc }, FamilyDef { id: "clang", flags: &CLANG_FLAGS, ignore_executables: &CLANG_IGNORE_EXECUTABLES, ignore_flags: &CLANG_IGNORE_FLAGS, slash_prefix: CLANG_SLASH_PREFIX, env_rules: &CLANG_ENV_RULES, source_mode: SourceMode::PerSourceStripped, response_file_syntax: Syntax::GnuClang }, FamilyDef { id: "clang_cl", flags: &CLANG_CL_FLAGS, ignore_executables: &CLANG_CL_IGNORE_EXECUTABLES, ignore_flags: &CLANG_CL_IGNORE_FLAGS, slash_prefix: CLANG_CL_SLASH_PREFIX, env_rules: &CLANG_CL_ENV_RULES, source_mode: SourceMode::PerSourceStripped, response_file_syntax: Syntax::Msvc }, FamilyDef { id: "cuda", flags: &CUDA_FLAGS, ignore_executables: &CUDA_IGNORE_EXECUTABLES, ignore_flags: &CUDA_IGNORE_FLAGS, slash_prefix: CUDA_SLASH_PREFIX, env_rules: &CUDA_ENV_RULES, source_mode: SourceMode::PerSourceStripped, response_file_syntax: Syntax::GnuClang }, FamilyDef { id: "flang", flags: &FLANG_FLAGS, ignore_executables: &FLANG_IGNORE_EXECUTABLES, ignore_flags: &FLANG_IGNORE_FLAGS, slash_prefix: FLANG_SLASH_PREFIX, env_rules: &FLANG_ENV_RULES, source_mode: SourceMode::PerSourceStripped, response_file_syntax: Syntax::GnuClang }, FamilyDef { id: "mpi", flags: &MPI_FLAGS, ignore_executables: &MPI_IGNORE_EXECUTABLES, ignore_flags: &MPI_IGNORE_FLAGS, slash_prefix: MPI_SLASH_PREFIX, env_rules: &MPI_ENV_RULES, source_mode: SourceMode::PerSourceStripped, response_file_syntax: Syntax::GnuClang }, FamilyDef { id: "qnx", flags: &QNX_FLAGS, ignore_executables: &QNX_IGNORE_EXECUTABLES, ignore_flags: &QNX_IGNORE_FLAGS, slash_prefix: QNX_SLASH_PREFIX, env_rules: &QNX_ENV_RULES, source_mode: SourceMode::PerSourceStripped, response_file_syntax: Syntax::GnuClang }, FamilyDef { id: "cray_fortran", flags: &CRAY_FORTRAN_FLAGS, ignore_executables: &CRAY_FORTRAN_IGNORE_EXECUTABLES, ignore_flags: &CRAY_FORTRAN_IGNORE_FLAGS, slash_prefix: CRAY_FORTRAN_SLASH_PREFIX, env_rules: &CRAY_FORTRAN_ENV_RULES, source_mode: SourceMode::PerSourceStripped, response_file_syntax: Syntax::GnuClang }, FamilyDef { id: "fasm", flags: &FASM_FLAGS, ignore_executables: &FASM_IGNORE_EXECUTABLES, ignore_flags: &FASM_IGNORE_FLAGS, slash_prefix: FASM_SLASH_PREFIX, env_rules: &FASM_ENV_RULES, source_mode: SourceMode::PerSourceStripped, response_file_syntax: Syntax::GnuClang }, FamilyDef { id: "gcc", flags: &GCC_FLAGS, ignore_executables: &GCC_IGNORE_EXECUTABLES, ignore_flags: &GCC_IGNORE_FLAGS, slash_prefix: GCC_SLASH_PREFIX, env_rules: &GCC_ENV_RULES, source_mode: SourceMode::PerSourceStripped, response_file_syntax: Syntax::GnuClang }, FamilyDef { id: "intel_fortran", flags: &INTEL_FORTRAN_FLAGS, ignore_executables: &INTEL_FORTRAN_IGNORE_EXECUTABLES, ignore_flags: &INTEL_FORTRAN_IGNORE_FLAGS, slash_prefix: INTEL_FORTRAN_SLASH_PREFIX, env_rules: &INTEL_FORTRAN_ENV_RULES, source_mode: SourceMode::PerSourceStripped, response_file_syntax: Syntax::GnuClang }, FamilyDef { id: "msvc", flags: &MSVC_FLAGS, ignore_executables: &MSVC_IGNORE_EXECUTABLES, ignore_flags: &MSVC_IGNORE_FLAGS, slash_prefix: MSVC_SLASH_PREFIX, env_rules: &MSVC_ENV_RULES, source_mode: SourceMode::PerSourceStripped, response_file_syntax: Syntax::Msvc }, FamilyDef { id: "nasm", flags: &NASM_FLAGS, ignore_executables: &NASM_IGNORE_EXECUTABLES, ignore_flags: &NASM_IGNORE_FLAGS, slash_prefix: NASM_SLASH_PREFIX, env_rules: &NASM_ENV_RULES, source_mode: SourceMode::PerSourceStripped, response_file_syntax: Syntax::GnuClang }, FamilyDef { id: "nvidia_hpc", flags: &NVIDIA_HPC_FLAGS, ignore_executables: &NVIDIA_HPC_IGNORE_EXECUTABLES, ignore_flags: &NVIDIA_HPC_IGNORE_FLAGS, slash_prefix: NVIDIA_HPC_SLASH_PREFIX, env_rules: &NVIDIA_HPC_ENV_RULES, source_mode: SourceMode::PerSourceStripped, response_file_syntax: Syntax::GnuClang }, FamilyDef { id: "swift", flags: &SWIFT_FLAGS, ignore_executables: &SWIFT_IGNORE_EXECUTABLES, ignore_flags: &SWIFT_IGNORE_FLAGS, slash_prefix: SWIFT_SLASH_PREFIX, env_rules: &SWIFT_ENV_RULES, source_mode: SourceMode::PerSourceFull, response_file_syntax: Syntax::GnuClang }, FamilyDef { id: "ti_cgt", flags: &TI_CGT_FLAGS, ignore_executables: &TI_CGT_IGNORE_EXECUTABLES, ignore_flags: &TI_CGT_IGNORE_FLAGS, slash_prefix: TI_CGT_SLASH_PREFIX, env_rules: &TI_CGT_ENV_RULES, source_mode: SourceMode::PerSourceStripped, response_file_syntax: Syntax::GnuClang }, FamilyDef { id: "vala", flags: &VALA_FLAGS, ignore_executables: &VALA_IGNORE_EXECUTABLES, ignore_flags: &VALA_IGNORE_FLAGS, slash_prefix: VALA_SLASH_PREFIX, env_rules: &VALA_ENV_RULES, source_mode: SourceMode::Combined, response_file_syntax: Syntax::GnuClang }, ]; snapshots__snapshot_flags_arm_flang.snap000066400000000000000000000411331527177720300362770ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/snapshots--- source: build-support/compilers-codegen/tests/snapshots.rs expression: generated --- // Generated from compilers/arm_flang.yaml -- DO NOT EDIT static ARM_FLANG_FLAGS: [FlagRule; 129] = [ FlagRule::new(FlagPattern::Exactly("-iwithprefixbefore", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-fdisable-real-10", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-fno-underscoring", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-fno-xor-operator", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-ffile-prefix-map"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-fdefault-real-8", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-fdisable-real-3", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-fno-realloc-lhs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-pass-exit-codes", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-fimplicit-none", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-flinker-output"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-fno-backslash", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-fno-automatic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-funderscoring", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-fxor-operator", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Xpreprocessor", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nodefaultlibs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-flarge-sizes", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-frealloc-lhs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-fno-unsigned", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-dumpbase-ext", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("--target-help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Prefix("-fplugin-arg-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-nostartfiles", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-ffixed-form", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-traditional", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-iwithprefix", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-fbackslash", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-ffree-form", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-module-dir"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("--romc-path"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEq("-iplugindir"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-static-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xassembler", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))), FlagRule::new(FlagPattern::Exactly("-funsigned", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-trigraphs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-idirafter", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-imultilib", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-arch_only", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEq("-fconvert"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-dumpbase", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("--version", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-isysroot", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("--sysroot"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nostdlib", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-rdynamic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-symbolic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-aux-info", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-dumpdir", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-wrapper", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithEq("-fplugin"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-include"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-imacros", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-pthread", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-isystem", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-iprefix", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("-fuse-ld"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xlinker", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-fmodule", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Xflang", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-iquote", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nolibc", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-no-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-static", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-shared", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-nocpp", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("--help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::ExactlyWithEq("-specs"), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-undef", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-remap", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("-entry"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-pipe", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-arch", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-ansi", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-save", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-cpp", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-###", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-MMD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-Wp,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-Wl,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-Wa,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-std"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-tno", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-MM", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MG", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MP", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MF", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MT", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MQ", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-CC", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-no", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-J"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-x", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-c", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-S", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Assembling))), FlagRule::new(FlagPattern::Exactly("-E", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-o"), ArgumentKind::Output), FlagRule::new(FlagPattern::Exactly("-v", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-A"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-D"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-U"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-M", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-C", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-P", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-H", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-I"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-L"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-B"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-l"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-e", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-r", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-s", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-T", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-u", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-z", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-O", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-g", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-f", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-m", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-p", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-W", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-d", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-Q", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-X", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-Y", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("--", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("@", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), ]; static ARM_FLANG_IGNORE_EXECUTABLES: [&str; 7] = ["cc1", "cc1plus", "cc1obj", "cc1objplus", "f951", "collect2", "lto1", ]; static ARM_FLANG_IGNORE_FLAGS: [&str; 0] = []; static ARM_FLANG_SLASH_PREFIX: bool = false; static ARM_FLANG_ENV_RULES: [EnvRule; 5] = [ EnvRule::new("CPATH", EnvMapping::Flag { flag: "-I", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("C_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("CPLUS_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("OBJC_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("LIBRARY_PATH", EnvMapping::Flag { flag: "-L", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), ]; snapshots__snapshot_flags_armclang.snap000066400000000000000000000640331527177720300361410ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/snapshots--- source: build-support/compilers-codegen/tests/snapshots.rs expression: generated --- // Generated from compilers/armclang.yaml -- DO NOT EDIT static ARMCLANG_FLAGS: [FlagRule; 207] = [ FlagRule::new(FlagPattern::Exactly("--no_depend_system_headers", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--depend_system_headers", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--depend_single_line", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-iwithprefixbefore", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-allowable_client", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("--analyzer-output"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-index-header-map", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-multiply_defined", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--gcc-install-dir"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-ffile-prefix-map"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-Xopenmp-target=", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("--profile-blocks", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--cuda-host-only", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-current_version"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-dependency-file", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-gcodeview-ghash", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-analyzer-config", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-pass-exit-codes", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--depend_format"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("--depend_target"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Xopenmp-target", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-dependency-dot", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("--gcc-toolchain"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--cuda-gpu-arch"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-flinker-output"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--library_type"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEq("--autocomplete"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-bundle_loader", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-single_module", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xpreprocessor", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nodefaultlibs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-bind_at_load", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--dyld-prefix"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEq("-faligned-new"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("--hip-version"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-install_name", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("--traditional", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-resource-dir", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-dumpbase-ext", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("--target-help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Prefix("-fplugin-arg-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-nostartfiles", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("--precompile", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-client_name", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-dumpmachine", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-dumpversion", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-ibuiltininc", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-seglinkedit", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-traditional", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-plugin-arg-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-iwithprefix", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-dead_strip", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-dylib_file", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-dynamiclib", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-force_load", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-image_base", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEq("--unwindlib"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xassembler", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--cuda-path"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-iplugindir"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-static-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xanalyzer", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-arch_only", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-framework", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-gcodeview", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--hip-link", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-segcreate", 3), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEq("-unwindlib"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--language"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-emit-llvm", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--hip-path"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-trigraphs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-idirafter", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-imultilib", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-all_load", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("--analyze", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-dsym-dir"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-dylinker", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-emit-ast", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--migrate", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--profile", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-seg1addr"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--sysroot"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-twolevel", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-umbrella", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-dumpbase", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("--version", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-isysroot", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("--sysroot"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nostdlib", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-rdynamic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-symbolic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-aux-info", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("--depend"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("--prefix"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("--config", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-dynamic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--output"), ArgumentKind::Output), FlagRule::new(FlagPattern::Exactly("-segaddr", 2), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-segprot", 3), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--target"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-dumpdir", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-wrapper", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithEq("-fplugin"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-include"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-imacros", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-pthread", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-isystem", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-iprefix", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("-fuse-ld"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xlinker", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-fmodule", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-ObjC++", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-bundle", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-objcmt", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-object", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-target", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Xclang", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-triple", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-plugin", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-iquote", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nolibc", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-no-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-static", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-shared", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--apcs"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-Xarch", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-Xcuda", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-mllvm"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--pipe", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Prefix("--save", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Prefix("--help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::ExactlyWithEq("-specs"), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-undef", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-remap", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("-entry"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-ObjC", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-arch", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-cuid"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-init", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-lazy", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-sect", 3), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-seg_", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-sub_", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-time", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-load", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-pipe", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-ansi", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-save", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-###", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-MMD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-Wp,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-Wl,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-Wa,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-std"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-tno", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-EB", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-MJ", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MM", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MG", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MP", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MF", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MT", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MQ", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-CC", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-no", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-F"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Z", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-a", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-r", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-x", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-c", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-S", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Assembling))), FlagRule::new(FlagPattern::Exactly("-E", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-o"), ArgumentKind::Output), FlagRule::new(FlagPattern::Exactly("-v", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-A"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-D"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-U"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-M", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-C", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-P", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-H", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-I"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-L"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-B"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-l"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-e", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-r", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-s", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-T", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-u", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-z", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-O", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-g", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-f", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-m", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-p", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-W", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-d", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-Q", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-X", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-Y", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("--", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("@", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), ]; static ARMCLANG_IGNORE_EXECUTABLES: [&str; 7] = ["cc1", "cc1plus", "cc1obj", "cc1objplus", "f951", "collect2", "lto1", ]; static ARMCLANG_IGNORE_FLAGS: [&str; 1] = ["-cc1", ]; static ARMCLANG_SLASH_PREFIX: bool = false; static ARMCLANG_ENV_RULES: [EnvRule; 6] = [ EnvRule::new("ARMCOMPILER6_CLANGOPT", EnvMapping::Expand { position: EnvPosition::Prepend }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), EnvRule::new("CPATH", EnvMapping::Flag { flag: "-I", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("C_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("CPLUS_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("OBJC_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("LIBRARY_PATH", EnvMapping::Flag { flag: "-L", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), ]; snapshots__snapshot_flags_clang.snap000066400000000000000000000614061527177720300354420ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/snapshots--- source: build-support/compilers-codegen/tests/snapshots.rs expression: generated --- // Generated from compilers/clang.yaml -- DO NOT EDIT static CLANG_FLAGS: [FlagRule; 199] = [ FlagRule::new(FlagPattern::Exactly("-iwithprefixbefore", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-allowable_client", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("--analyzer-output"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-index-header-map", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-multiply_defined", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--gcc-install-dir"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-ffile-prefix-map"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-Xopenmp-target=", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("--profile-blocks", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--cuda-host-only", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-current_version"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-dependency-file", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-gcodeview-ghash", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-analyzer-config", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-pass-exit-codes", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-Xopenmp-target", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-dependency-dot", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("--gcc-toolchain"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--cuda-gpu-arch"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-flinker-output"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEq("--autocomplete"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-bundle_loader", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-single_module", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xpreprocessor", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nodefaultlibs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-bind_at_load", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--dyld-prefix"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEq("-faligned-new"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("--hip-version"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-install_name", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("--traditional", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-resource-dir", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-dumpbase-ext", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("--target-help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Prefix("-fplugin-arg-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-nostartfiles", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("--precompile", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-client_name", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-dumpmachine", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-dumpversion", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-ibuiltininc", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-seglinkedit", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-traditional", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-plugin-arg-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-iwithprefix", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-dead_strip", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-dylib_file", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-dynamiclib", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-force_load", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-image_base", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEq("--unwindlib"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xassembler", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--cuda-path"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-iplugindir"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-static-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xanalyzer", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-arch_only", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-framework", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-gcodeview", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--hip-link", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-segcreate", 3), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEq("-unwindlib"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--language"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-emit-llvm", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--hip-path"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-trigraphs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-idirafter", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-imultilib", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-all_load", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("--analyze", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-dsym-dir"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-dylinker", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-emit-ast", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--migrate", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--profile", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-seg1addr"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--sysroot"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-twolevel", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-umbrella", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-dumpbase", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("--version", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-isysroot", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("--sysroot"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nostdlib", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-rdynamic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-symbolic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-aux-info", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEq("--prefix"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("--config", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-dynamic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--output"), ArgumentKind::Output), FlagRule::new(FlagPattern::Exactly("-segaddr", 2), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-segprot", 3), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--target"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-dumpdir", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-wrapper", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithEq("-fplugin"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-include"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-imacros", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-pthread", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-isystem", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-iprefix", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("-fuse-ld"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xlinker", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-fmodule", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-ObjC++", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-bundle", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-objcmt", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-object", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-target", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Xclang", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-triple", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-plugin", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-iquote", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nolibc", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-no-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-static", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-shared", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-Xarch", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-Xcuda", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-mllvm"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--pipe", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Prefix("--save", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Prefix("--help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::ExactlyWithEq("-specs"), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-undef", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-remap", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("-entry"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-ObjC", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-arch", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-cuid"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-init", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-lazy", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-sect", 3), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-seg_", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-sub_", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-time", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-load", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-pipe", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-ansi", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-save", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-###", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-MMD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-Wp,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-Wl,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-Wa,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-std"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-tno", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-EB", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-MJ", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MM", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MG", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MP", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MF", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MT", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MQ", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-CC", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-no", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-F"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Z", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-a", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-r", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-x", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-c", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-S", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Assembling))), FlagRule::new(FlagPattern::Exactly("-E", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-o"), ArgumentKind::Output), FlagRule::new(FlagPattern::Exactly("-v", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-A"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-D"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-U"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-M", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-C", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-P", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-H", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-I"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-L"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-B"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-l"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-e", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-r", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-s", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-T", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-u", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-z", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-O", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-g", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-f", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-m", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-p", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-W", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-d", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-Q", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-X", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-Y", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("--", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("@", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), ]; static CLANG_IGNORE_EXECUTABLES: [&str; 7] = ["cc1", "cc1plus", "cc1obj", "cc1objplus", "f951", "collect2", "lto1", ]; static CLANG_IGNORE_FLAGS: [&str; 1] = ["-cc1", ]; static CLANG_SLASH_PREFIX: bool = false; static CLANG_ENV_RULES: [EnvRule; 5] = [ EnvRule::new("CPATH", EnvMapping::Flag { flag: "-I", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("C_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("CPLUS_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("OBJC_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("LIBRARY_PATH", EnvMapping::Flag { flag: "-L", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), ]; snapshots__snapshot_flags_clang_cl.snap000066400000000000000000000524231527177720300361170ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/snapshots--- source: build-support/compilers-codegen/tests/snapshots.rs assertion_line: 23 expression: generated --- // Generated from compilers/clang_cl.yaml -- DO NOT EDIT static CLANG_CL_FLAGS: [FlagRule; 174] = [ FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-fms-compatibility-version"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-fprofile-instr-generate", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-fprofile-instr-generate"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-fcrash-diagnostics-dir"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-ftrivial-auto-var-init"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/external:anglebrackets", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-fno-color-diagnostics", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/Qfast_transcendentals", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-fno-coverage-mapping", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-fsanitize-blacklist"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-fcolor-diagnostics", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-fprofile-instr-use"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-fno-ms-extensions", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-fcoverage-mapping", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithColon("/execution-charset"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/validate-charset", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-fms-extensions", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/vctoolsversion"), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithColon("/source-charset"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/showFilenames", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/Qspectre-load", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-fmsc-version"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-fno-sanitize"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--driver-mode"), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithColon("/external:env"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("/showIncludes", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("/await:strict", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/permissive-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithColon("/Qvec-report"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithColon("/diagnostics"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithColon("/errorReport"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/vctoolsdir"), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/winsysroot"), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/external:I"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("/external:W", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-fsanitize"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithColon("/constexpr"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--version", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::ExactlyWithColon("/volatile"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Qspectre", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/analyze-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-fno-lto", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--target"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-fuse-ld"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithColon("/analyze"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/analyze", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Xclang", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/bigobj", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/openmp", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/nologo", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithColon("/clang"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/imsvc"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithColon("/favor"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithColon("/guard"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/utf-8", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/await", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-flto", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-flto"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-msse", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-mavx", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::ExactlyWithColon("/arch"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Qpar", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Wall", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/link", 0), ArgumentKind::Other(PassEffect::PassThrough)), FlagRule::new(FlagPattern::Exactly("-m32", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-m64", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-###", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/Fo:"), ArgumentKind::Output), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/Fe:"), ArgumentKind::Output), FlagRule::new(FlagPattern::ExactlyWithColonOrSep("/doc"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithColon("/std"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/GR-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/MDd", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/MTd", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/clr", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/RTC", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/sdl", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Zo-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/WX-", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/LDd", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("/EP", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("/Fo", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/Fe", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("/Fo", 0), ArgumentKind::Output), FlagRule::new(FlagPattern::Prefix("/Fe", 0), ArgumentKind::Output), FlagRule::new(FlagPattern::Prefix("/Fd", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/FA", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/Fa", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/Fi", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("/Fm", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("/FR", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/FI"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/Tc"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/Tp"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/TC", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/TP", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithColon("/Zc"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/O1", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/O2", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/Ob", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Od", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Og", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Oi", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Os", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Ot", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Ox", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Oy", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/GL", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Gw", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Gy", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/EH", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithColon("/fp"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/GA", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Gd", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Ge", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/GF", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Gh", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/GH", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Gr", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/GR", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/Gs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/GT", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Gv", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Gz", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/GZ", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/MD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/MT", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/ZI", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Zi", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Z7", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Zf", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithColon("/ZH"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Zo", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/WX", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/W0", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/W1", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/W2", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/W3", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/W4", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithColon("/Wv"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/Wv", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/w1"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/w2"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/w3"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/w4"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/wd"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/we"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/wo"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/FC", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("/Yc", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/Yu", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/Fp", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/YX", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Y-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/LD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("/LN", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/AI"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/FU"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/MP", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("/FS", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-v", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("/?", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("/c", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/E", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("/P", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/I"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/D"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/U"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("/u", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("/X", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("/w", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/F", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("/J", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), ]; static CLANG_CL_IGNORE_EXECUTABLES: [&str; 3] = ["c1", "c1xx", "c2", ]; static CLANG_CL_IGNORE_FLAGS: [&str; 0] = []; static CLANG_CL_SLASH_PREFIX: bool = true; static CLANG_CL_ENV_RULES: [EnvRule; 4] = [ EnvRule::new("CL", EnvMapping::Expand { position: EnvPosition::Prepend }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), EnvRule::new("_CL_", EnvMapping::Expand { position: EnvPosition::Append }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), EnvRule::new("INCLUDE", EnvMapping::Flag { flag: "/I", separator: EnvSeparator::Fixed(";") }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("LIB", EnvMapping::Flag { flag: "/LIBPATH:", separator: EnvSeparator::Fixed(";") }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), ]; snapshots__snapshot_flags_cray_cc.snap000066400000000000000000000616161527177720300357640ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/snapshots--- source: build-support/compilers-codegen/tests/snapshots.rs expression: generated --- // Generated from compilers/cray_cc.yaml -- DO NOT EDIT static CRAY_CC_FLAGS: [FlagRule; 200] = [ FlagRule::new(FlagPattern::Exactly("-iwithprefixbefore", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-allowable_client", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("--analyzer-output"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-index-header-map", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-multiply_defined", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--gcc-install-dir"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-ffile-prefix-map"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-Xopenmp-target=", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("--profile-blocks", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--cuda-host-only", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-current_version"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-dependency-file", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-gcodeview-ghash", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-analyzer-config", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-pass-exit-codes", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-Xopenmp-target", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-dependency-dot", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("--gcc-toolchain"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--cuda-gpu-arch"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-flinker-output"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEq("--autocomplete"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-bundle_loader", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-single_module", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xpreprocessor", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nodefaultlibs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-bind_at_load", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--dyld-prefix"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEq("-faligned-new"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("--hip-version"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-install_name", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("--traditional", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-resource-dir", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-dumpbase-ext", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("--target-help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Prefix("-fplugin-arg-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-nostartfiles", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("--precompile", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-client_name", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-dumpmachine", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-dumpversion", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-ibuiltininc", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-seglinkedit", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-traditional", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-plugin-arg-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-iwithprefix", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-dead_strip", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-dylib_file", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-dynamiclib", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-force_load", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-image_base", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEq("--unwindlib"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xassembler", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--cuda-path"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-iplugindir"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-static-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xanalyzer", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-arch_only", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-framework", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-gcodeview", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--hip-link", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-segcreate", 3), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEq("-unwindlib"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--language"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-emit-llvm", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--hip-path"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-trigraphs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-idirafter", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-imultilib", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-all_load", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("--analyze", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-dsym-dir"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-dylinker", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-emit-ast", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--migrate", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--profile", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-seg1addr"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--sysroot"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-twolevel", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-umbrella", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-dumpbase", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("--version", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-isysroot", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("--sysroot"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nostdlib", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-rdynamic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-symbolic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-aux-info", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEq("--prefix"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("--config", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-dynamic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--output"), ArgumentKind::Output), FlagRule::new(FlagPattern::Exactly("-segaddr", 2), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-segprot", 3), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--target"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-dumpdir", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-wrapper", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithEq("-fplugin"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-include"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-imacros", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-pthread", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-isystem", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-iprefix", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("-fuse-ld"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xlinker", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-fmodule", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-fcray-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-ObjC++", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-bundle", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-objcmt", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-object", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-target", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Xclang", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-triple", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-plugin", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-iquote", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nolibc", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-no-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-static", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-shared", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-Xarch", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-Xcuda", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-mllvm"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--pipe", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Prefix("--save", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Prefix("--help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::ExactlyWithEq("-specs"), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-undef", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-remap", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("-entry"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-ObjC", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-arch", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-cuid"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-init", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-lazy", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-sect", 3), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-seg_", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-sub_", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-time", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-load", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-pipe", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-ansi", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-save", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-###", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-MMD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-Wp,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-Wl,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-Wa,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-std"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-tno", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-EB", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-MJ", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MM", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MG", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MP", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MF", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MT", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MQ", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-CC", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-no", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-F"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Z", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-a", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-r", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-x", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-c", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-S", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Assembling))), FlagRule::new(FlagPattern::Exactly("-E", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-o"), ArgumentKind::Output), FlagRule::new(FlagPattern::Exactly("-v", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-A"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-D"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-U"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-M", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-C", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-P", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-H", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-I"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-L"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-B"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-l"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-e", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-r", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-s", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-T", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-u", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-z", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-O", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-g", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-f", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-m", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-p", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-W", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-d", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-Q", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-X", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-Y", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("--", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("@", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), ]; static CRAY_CC_IGNORE_EXECUTABLES: [&str; 7] = ["cc1", "cc1plus", "cc1obj", "cc1objplus", "f951", "collect2", "lto1", ]; static CRAY_CC_IGNORE_FLAGS: [&str; 1] = ["-cc1", ]; static CRAY_CC_SLASH_PREFIX: bool = false; static CRAY_CC_ENV_RULES: [EnvRule; 5] = [ EnvRule::new("CPATH", EnvMapping::Flag { flag: "-I", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("C_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("CPLUS_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("OBJC_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("LIBRARY_PATH", EnvMapping::Flag { flag: "-L", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), ]; snapshots__snapshot_flags_cray_fortran.snap000066400000000000000000000170651527177720300370510ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/snapshots--- source: bear-codegen/tests/snapshots.rs expression: "generate_flag_file(\"cray_fortran\")" --- // Generated from compilers/cray_fortran.yaml -- DO NOT EDIT static CRAY_FORTRAN_FLAGS: [FlagRule; 65] = [ FlagRule::new(FlagPattern::Exactly("--no-custom-ld-script", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-no-add-rpath-shared", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("--custom-ld-script="), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-add-rpath-shared", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-target-network="), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-no-add-runpath", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("--no-as-needed", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-target-accel="), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-no-gcc-rpath", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-no-add-rpath", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-no-as-needed", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-add-runpath", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-target-cpu="), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("--as-needed", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-qno-openmp", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-gcc-rpath", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-add-rpath", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-as-needed", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Mnoopenmp", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-default64", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-noopenmp", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-dynamic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-openmp", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-shared", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-static", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("--cray", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-cray", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-VVV", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-mp", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-VV", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-A"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-b"), ArgumentKind::Output), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-D"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-d"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-e"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-f"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-G"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-h"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-I"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-J"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-K"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-l"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-L"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-m"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-M"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-N"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-O"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-o"), ArgumentKind::Output), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-p"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-Q"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-r"), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-R"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-s"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-U"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-W"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-x"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-Y"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-c", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-E", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-F", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-g", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-S", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Assembling))), FlagRule::new(FlagPattern::Exactly("-T", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-v", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-V", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), ]; static CRAY_FORTRAN_IGNORE_EXECUTABLES: [&str; 0] = []; static CRAY_FORTRAN_IGNORE_FLAGS: [&str; 0] = []; static CRAY_FORTRAN_SLASH_PREFIX: bool = false; static CRAY_FORTRAN_ENV_RULES: [EnvRule; 0] = [ ]; snapshots__snapshot_flags_cuda.snap000066400000000000000000000507111527177720300352670ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/snapshots--- source: build-support/compilers-codegen/tests/snapshots.rs expression: "generate_flag_file(\"cuda\")" --- // Generated from compilers/cuda.yaml -- DO NOT EDIT static CUDA_FLAGS: [FlagRule; 161] = [ FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--relocatable-device-code"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--expt-relaxed-constexpr", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--expt-extended-lambda", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--generate-line-info", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--gpu-architecture"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("--disable-warnings", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-iwithprefixbefore", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--compiler-bindir"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--extended-lambda", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-ffile-prefix-map"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-extended-lambda", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-pass-exit-codes", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--generate-code"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--use_fast_math", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-flinker-output"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--maxrregcount"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-use_fast_math", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--device-debug", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--include-path"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-Xpreprocessor", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nodefaultlibs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("--device-link", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-maxrregcount"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("--output-file"), ArgumentKind::Output), FlagRule::new(FlagPattern::Exactly("-dumpbase-ext", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("--target-help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Prefix("-fplugin-arg-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-nostartfiles", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("--preprocess", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-traditional", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-iwithprefix", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--cuda-path"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--prec-sqrt", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-iplugindir"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-static-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xassembler", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--gpu-code"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--device-c", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--device-w", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-Xcompiler", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--prec-div", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--optimize"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--keep-dir"), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-trigraphs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-idirafter", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-imultilib", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-arch_only", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("--compile", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-lineinfo", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--profile", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--verbose", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("--version", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--machine"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-dumpbase", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-isysroot", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("--sysroot"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nostdlib", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-rdynamic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-symbolic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-aux-info", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-gencode"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--cudart"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-Xlinker", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("--fatbin", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-dumpdir", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-wrapper", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithEq("-fplugin"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-include"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-imacros", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-pthread", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-isystem", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-iprefix", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("-fuse-ld"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xlinker", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-fmodule", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--cubin", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-iquote", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nolibc", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-no-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-static", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-shared", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-dlink", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("--link", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-ccbin"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--fmad", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("--keep", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Prefix("--help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::ExactlyWithEq("-specs"), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-undef", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-remap", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("-entry"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-arch"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-code"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--lib", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("--run", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("--ftz", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--ptx", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--std"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-pipe", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-arch", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-ansi", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-save", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-rdc", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-###", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-MMD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-Wp,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-Wl,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-Wa,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-std"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-tno", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-dc", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-dw", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-MM", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MG", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MP", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MF", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MT", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MQ", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-CC", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-no", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-c", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-G", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-O"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-o"), ArgumentKind::Output), FlagRule::new(FlagPattern::Exactly("-E", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-v", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Prefix("-W", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-m"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-x", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-S", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Assembling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-A"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-D"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-U"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-M", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-C", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-P", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-H", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-I"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-L"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-B"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-l"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-e", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-r", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-s", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-T", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-u", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-z", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-O", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-g", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-f", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-m", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-p", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-d", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-Q", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-X", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-Y", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("--", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("@", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), ]; static CUDA_IGNORE_EXECUTABLES: [&str; 7] = ["cc1", "cc1plus", "cc1obj", "cc1objplus", "f951", "collect2", "lto1", ]; static CUDA_IGNORE_FLAGS: [&str; 0] = []; static CUDA_SLASH_PREFIX: bool = false; static CUDA_ENV_RULES: [EnvRule; 5] = [ EnvRule::new("CPATH", EnvMapping::Flag { flag: "-I", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("C_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("CPLUS_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("OBJC_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("LIBRARY_PATH", EnvMapping::Flag { flag: "-L", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), ]; snapshots__snapshot_flags_embarcadero.snap000066400000000000000000000614521527177720300366230ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/snapshots--- source: build-support/compilers-codegen/tests/snapshots.rs expression: generated --- // Generated from compilers/embarcadero.yaml -- DO NOT EDIT static EMBARCADERO_FLAGS: [FlagRule; 199] = [ FlagRule::new(FlagPattern::Exactly("-iwithprefixbefore", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-allowable_client", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("--analyzer-output"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-index-header-map", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-multiply_defined", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--gcc-install-dir"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-ffile-prefix-map"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-Xopenmp-target=", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("--profile-blocks", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--cuda-host-only", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-current_version"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-dependency-file", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-gcodeview-ghash", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-analyzer-config", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-pass-exit-codes", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-Xopenmp-target", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-dependency-dot", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("--gcc-toolchain"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--cuda-gpu-arch"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-flinker-output"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEq("--autocomplete"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-bundle_loader", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-single_module", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xpreprocessor", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nodefaultlibs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-bind_at_load", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--dyld-prefix"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEq("-faligned-new"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("--hip-version"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-install_name", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("--traditional", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-resource-dir", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-dumpbase-ext", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("--target-help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Prefix("-fplugin-arg-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-nostartfiles", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("--precompile", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-client_name", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-dumpmachine", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-dumpversion", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-ibuiltininc", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-seglinkedit", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-traditional", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-plugin-arg-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-iwithprefix", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-dead_strip", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-dylib_file", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-dynamiclib", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-force_load", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-image_base", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEq("--unwindlib"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xassembler", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--cuda-path"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-iplugindir"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-static-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xanalyzer", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-arch_only", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-framework", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-gcodeview", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--hip-link", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-segcreate", 3), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEq("-unwindlib"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--language"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-emit-llvm", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--hip-path"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-trigraphs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-idirafter", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-imultilib", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-all_load", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("--analyze", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-dsym-dir"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-dylinker", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-emit-ast", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--migrate", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--profile", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-seg1addr"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--sysroot"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-twolevel", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-umbrella", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-dumpbase", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("--version", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-isysroot", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("--sysroot"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nostdlib", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-rdynamic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-symbolic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-aux-info", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEq("--prefix"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("--config", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-dynamic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--output"), ArgumentKind::Output), FlagRule::new(FlagPattern::Exactly("-segaddr", 2), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-segprot", 3), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--target"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-dumpdir", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-wrapper", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithEq("-fplugin"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-include"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-imacros", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-pthread", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-isystem", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-iprefix", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("-fuse-ld"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xlinker", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-fmodule", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-ObjC++", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-bundle", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-objcmt", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-object", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-target", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Xclang", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-triple", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-plugin", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-iquote", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nolibc", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-no-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-static", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-shared", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-Xarch", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-Xcuda", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-mllvm"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--pipe", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Prefix("--save", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Prefix("--help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::ExactlyWithEq("-specs"), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-undef", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-remap", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("-entry"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-ObjC", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-arch", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-cuid"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-init", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-lazy", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-sect", 3), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-seg_", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-sub_", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-time", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-load", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-pipe", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-ansi", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-save", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-###", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-MMD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-Wp,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-Wl,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-Wa,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-std"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-tno", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-EB", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-MJ", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MM", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MG", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MP", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MF", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MT", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MQ", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-CC", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-no", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-F"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Z", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-a", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-r", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-x", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-c", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-S", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Assembling))), FlagRule::new(FlagPattern::Exactly("-E", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-o"), ArgumentKind::Output), FlagRule::new(FlagPattern::Exactly("-v", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-A"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-D"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-U"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-M", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-C", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-P", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-H", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-I"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-L"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-B"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-l"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-e", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-r", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-s", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-T", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-u", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-z", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-O", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-g", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-f", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-m", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-p", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-W", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-d", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-Q", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-X", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-Y", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("--", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("@", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), ]; static EMBARCADERO_IGNORE_EXECUTABLES: [&str; 7] = ["cc1", "cc1plus", "cc1obj", "cc1objplus", "f951", "collect2", "lto1", ]; static EMBARCADERO_IGNORE_FLAGS: [&str; 1] = ["-cc1", ]; static EMBARCADERO_SLASH_PREFIX: bool = false; static EMBARCADERO_ENV_RULES: [EnvRule; 5] = [ EnvRule::new("CPATH", EnvMapping::Flag { flag: "-I", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("C_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("CPLUS_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("OBJC_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("LIBRARY_PATH", EnvMapping::Flag { flag: "-L", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), ]; snapshots__snapshot_flags_fasm.snap000066400000000000000000000014121527177720300352730ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/snapshots--- source: build-support/compilers-codegen/tests/snapshots.rs expression: "generate_flag_file(\"fasm\")" --- // Generated from compilers/fasm.yaml -- DO NOT EDIT static FASM_FLAGS: [FlagRule; 4] = [ FlagRule::new(FlagPattern::Exactly("-m", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-p", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-s", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-d"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), ]; static FASM_IGNORE_EXECUTABLES: [&str; 0] = []; static FASM_IGNORE_FLAGS: [&str; 0] = []; static FASM_SLASH_PREFIX: bool = false; static FASM_ENV_RULES: [EnvRule; 0] = [ ]; snapshots__snapshot_flags_flang.snap000066400000000000000000000411311527177720300354360ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/snapshots--- source: build-support/compilers-codegen/tests/snapshots.rs expression: "generate_flag_file(\"flang\")" --- // Generated from compilers/flang.yaml -- DO NOT EDIT static FLANG_FLAGS: [FlagRule; 129] = [ FlagRule::new(FlagPattern::Exactly("-iwithprefixbefore", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-fdisable-real-10", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-fno-underscoring", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-fno-xor-operator", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-ffile-prefix-map"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-fdefault-real-8", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-fdisable-real-3", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-fno-realloc-lhs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-pass-exit-codes", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-fimplicit-none", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-flinker-output"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-fno-backslash", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-fno-automatic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-funderscoring", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-fxor-operator", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Xpreprocessor", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nodefaultlibs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-flarge-sizes", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-frealloc-lhs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-fno-unsigned", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-dumpbase-ext", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("--target-help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Prefix("-fplugin-arg-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-nostartfiles", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-ffixed-form", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-traditional", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-iwithprefix", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-fbackslash", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-ffree-form", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-module-dir"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("--romc-path"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEq("-iplugindir"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-static-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xassembler", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))), FlagRule::new(FlagPattern::Exactly("-funsigned", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-trigraphs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-idirafter", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-imultilib", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-arch_only", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEq("-fconvert"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-dumpbase", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("--version", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-isysroot", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("--sysroot"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nostdlib", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-rdynamic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-symbolic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-aux-info", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-dumpdir", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-wrapper", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithEq("-fplugin"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-include"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-imacros", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-pthread", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-isystem", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-iprefix", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("-fuse-ld"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xlinker", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-fmodule", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Xflang", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-iquote", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nolibc", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-no-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-static", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-shared", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-nocpp", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("--help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::ExactlyWithEq("-specs"), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-undef", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-remap", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("-entry"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-pipe", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-arch", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-ansi", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-save", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-cpp", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-###", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-MMD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-Wp,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-Wl,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-Wa,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-std"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-tno", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-MM", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MG", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MP", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MF", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MT", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MQ", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-CC", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-no", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-J"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-x", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-c", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-S", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Assembling))), FlagRule::new(FlagPattern::Exactly("-E", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-o"), ArgumentKind::Output), FlagRule::new(FlagPattern::Exactly("-v", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-A"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-D"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-U"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-M", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-C", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-P", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-H", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-I"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-L"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-B"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-l"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-e", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-r", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-s", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-T", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-u", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-z", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-O", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-g", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-f", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-m", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-p", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-W", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-d", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-Q", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-X", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-Y", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("--", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("@", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), ]; static FLANG_IGNORE_EXECUTABLES: [&str; 7] = ["cc1", "cc1plus", "cc1obj", "cc1objplus", "f951", "collect2", "lto1", ]; static FLANG_IGNORE_FLAGS: [&str; 0] = []; static FLANG_SLASH_PREFIX: bool = false; static FLANG_ENV_RULES: [EnvRule; 5] = [ EnvRule::new("CPATH", EnvMapping::Flag { flag: "-I", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("C_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("CPLUS_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("OBJC_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("LIBRARY_PATH", EnvMapping::Flag { flag: "-L", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), ]; snapshots__snapshot_flags_gcc.snap000066400000000000000000000325641527177720300351150ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/snapshots--- source: build-support/compilers-codegen/tests/snapshots.rs expression: "generate_flag_file(\"gcc\")" --- // Generated from compilers/gcc.yaml -- DO NOT EDIT static GCC_FLAGS: [FlagRule; 104] = [ FlagRule::new(FlagPattern::Exactly("-iwithprefixbefore", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("-ffile-prefix-map"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-pass-exit-codes", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithEq("-flinker-output"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xpreprocessor", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nodefaultlibs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-dumpbase-ext", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("--target-help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Prefix("-fplugin-arg-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-nostartfiles", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-traditional", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-iwithprefix", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("-iplugindir"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-static-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xassembler", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))), FlagRule::new(FlagPattern::Exactly("-trigraphs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-idirafter", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-imultilib", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-arch_only", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-dumpbase", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("--version", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-isysroot", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("--sysroot"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nostdlib", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-rdynamic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-symbolic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-aux-info", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-dumpdir", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-wrapper", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithEq("-fplugin"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-include"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-imacros", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-pthread", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-isystem", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-iprefix", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("-fuse-ld"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xlinker", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-fmodule", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-iquote", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nolibc", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-no-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-static", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-shared", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("--help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::ExactlyWithEq("-specs"), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-undef", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-remap", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("-entry"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-pipe", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-arch", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-ansi", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-save", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-###", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-MMD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-Wp,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-Wl,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-Wa,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-std"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-tno", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-MM", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MG", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MP", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MF", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MT", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MQ", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-CC", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-no", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-x", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-c", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-S", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Assembling))), FlagRule::new(FlagPattern::Exactly("-E", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-o"), ArgumentKind::Output), FlagRule::new(FlagPattern::Exactly("-v", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-A"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-D"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-U"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-M", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-C", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-P", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-H", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-I"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-L"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-B"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-l"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-e", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-r", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-s", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-T", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-u", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-z", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-O", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-g", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-f", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-m", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-p", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-W", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-d", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-Q", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-X", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-Y", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("--", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("@", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), ]; static GCC_IGNORE_EXECUTABLES: [&str; 7] = ["cc1", "cc1plus", "cc1obj", "cc1objplus", "f951", "collect2", "lto1", ]; static GCC_IGNORE_FLAGS: [&str; 0] = []; static GCC_SLASH_PREFIX: bool = false; static GCC_ENV_RULES: [EnvRule; 5] = [ EnvRule::new("CPATH", EnvMapping::Flag { flag: "-I", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("C_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("CPLUS_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("OBJC_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("LIBRARY_PATH", EnvMapping::Flag { flag: "-L", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), ]; snapshots__snapshot_flags_ibm_xl.snap000066400000000000000000000700131527177720300356220ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/snapshots--- source: build-support/compilers-codegen/tests/snapshots.rs expression: generated --- // Generated from compilers/ibm_xl.yaml -- DO NOT EDIT static IBM_XL_FLAGS: [FlagRule; 225] = [ FlagRule::new(FlagPattern::Exactly("-iwithprefixbefore", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-allowable_client", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("--analyzer-output"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-index-header-map", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-multiply_defined", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--gcc-install-dir"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-ffile-prefix-map"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-Xopenmp-target=", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("--profile-blocks", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--cuda-host-only", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-current_version"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-dependency-file", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-gcodeview-ghash", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-analyzer-config", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-pass-exit-codes", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-Xopenmp-target", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-dependency-dot", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("--gcc-toolchain"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--cuda-gpu-arch"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-flinker-output"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEq("--autocomplete"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-bundle_loader", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-single_module", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xpreprocessor", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nodefaultlibs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-bind_at_load", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--dyld-prefix"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEq("-faligned-new"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("--hip-version"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-install_name", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("--traditional", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-resource-dir", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-dumpbase-ext", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("--target-help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Prefix("-fplugin-arg-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-nostartfiles", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("--precompile", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-client_name", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-dumpmachine", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-dumpversion", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-ibuiltininc", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-seglinkedit", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-traditional", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-plugin-arg-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-iwithprefix", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-qnoaltivec", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-dead_strip", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-dylib_file", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-dynamiclib", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-force_load", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-image_base", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEq("--unwindlib"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xassembler", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--cuda-path"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-iplugindir"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-static-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-qnostrict", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-qnoreport", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-qnoinline", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-qnounroll", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Xanalyzer", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-arch_only", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-framework", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-gcodeview", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--hip-link", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-segcreate", 3), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEq("-unwindlib"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--language"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-emit-llvm", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--hip-path"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-trigraphs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-idirafter", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-imultilib", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-qaltivec", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-all_load", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("--analyze", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-dsym-dir"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-dylinker", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-emit-ast", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--migrate", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--profile", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-seg1addr"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--sysroot"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-twolevel", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-umbrella", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-dumpbase", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("--version", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-isysroot", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("--sysroot"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nostdlib", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-rdynamic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-symbolic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-aux-info", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-qstrict", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-qmaxmem"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-qnosimd", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-qreport", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-qnolist", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-qinline"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-qunroll"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("--prefix"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("--config", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-dynamic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--output"), ArgumentKind::Output), FlagRule::new(FlagPattern::Exactly("-segaddr", 2), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-segprot", 3), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--target"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-dumpdir", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-wrapper", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithEq("-fplugin"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-include"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-imacros", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-pthread", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-isystem", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-iprefix", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("-fuse-ld"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xlinker", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-fmodule", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-qnohot", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-qnosmp", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-qnoipa", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-qnopdf", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-ObjC++", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-bundle", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-objcmt", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-object", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-target", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Xclang", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-triple", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-plugin", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-iquote", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nolibc", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-no-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-static", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-shared", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-qtune"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-qarch"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-qpdf1", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-qpdf2", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-qsimd"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-qlist", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-Xarch", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-Xcuda", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-mllvm"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--pipe", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Prefix("--save", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Prefix("--help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::ExactlyWithEq("-specs"), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-undef", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-remap", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("-entry"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-qhot", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-qsmp"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-qipa"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-ObjC", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-arch", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-cuid"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-init", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-lazy", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-sect", 3), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-seg_", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-sub_", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-time", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-load", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-pipe", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-ansi", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-save", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-###", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-MMD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-Wp,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-Wl,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-Wa,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-std"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-tno", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-EB", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-MJ", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MM", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MG", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MP", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MF", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MT", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MQ", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-CC", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-no", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-F"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Z", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-a", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-r", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-x", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-c", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-S", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Assembling))), FlagRule::new(FlagPattern::Exactly("-E", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-o"), ArgumentKind::Output), FlagRule::new(FlagPattern::Exactly("-v", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-A"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-D"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-U"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-M", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-C", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-P", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-H", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-I"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-L"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-B"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-l"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-e", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-r", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-s", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-T", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-u", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-z", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-O", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-g", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-f", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-m", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-p", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-W", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-d", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-Q", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-X", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-Y", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("--", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("@", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), ]; static IBM_XL_IGNORE_EXECUTABLES: [&str; 7] = ["cc1", "cc1plus", "cc1obj", "cc1objplus", "f951", "collect2", "lto1", ]; static IBM_XL_IGNORE_FLAGS: [&str; 1] = ["-cc1", ]; static IBM_XL_SLASH_PREFIX: bool = false; static IBM_XL_ENV_RULES: [EnvRule; 5] = [ EnvRule::new("CPATH", EnvMapping::Flag { flag: "-I", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("C_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("CPLUS_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("OBJC_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("LIBRARY_PATH", EnvMapping::Flag { flag: "-L", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), ]; snapshots__snapshot_flags_intel_cc.snap000066400000000000000000001011451527177720300361310ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/snapshots--- source: build-support/compilers-codegen/tests/snapshots.rs expression: generated --- // Generated from compilers/intel_cc.yaml -- DO NOT EDIT static INTEL_CC_FLAGS: [FlagRule; 262] = [ FlagRule::new(FlagPattern::Exactly("-qopt-multiple-gather-scatter-by-shuffles", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-qopt-assume-safe-padding", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-qopt-streaming-stores"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-qopt-mem-layout-trans"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-qopt-report-format"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-qopt-report-phase"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-iwithprefixbefore", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-qopt-report-file"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-diag-file-append"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-allowable_client", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("--analyzer-output"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-index-header-map", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-multiply_defined", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--gcc-install-dir"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-ffile-prefix-map"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-par-num-threads"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-diag-id-numbers", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-Xopenmp-target=", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("--profile-blocks", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--cuda-host-only", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-current_version"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-dependency-file", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-gcodeview-ghash", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-analyzer-config", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-pass-exit-codes", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-qopt-zmm-usage"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-fimf-precision"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Xopenmp-target", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-dependency-dot", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("--gcc-toolchain"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--cuda-gpu-arch"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-flinker-output"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-qopenmp-stubs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-qopt-prefetch"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-par-threshold"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-shared-libgcc", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-static-libgcc", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEq("--autocomplete"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-bundle_loader", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-single_module", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xpreprocessor", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nodefaultlibs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-qopenmp-link"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-qopenmp-simd", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-diag-disable"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-diag-warning"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-shared-intel", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-static-intel", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-no-prec-sqrt", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-no-traceback", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-bind_at_load", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--dyld-prefix"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEq("-faligned-new"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("--hip-version"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-install_name", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("--traditional", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-resource-dir", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-dumpbase-ext", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("--target-help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Prefix("-fplugin-arg-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-nostartfiles", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-qopt-report"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-no-parallel", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-diag-enable"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-diag-remark"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-no-prec-div", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-heap-arrays"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--precompile", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-client_name", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-dumpmachine", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-dumpversion", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-ibuiltininc", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-seglinkedit", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-traditional", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-plugin-arg-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-iwithprefix", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-qno-openmp", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-diag-error"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-dead_strip", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-dylib_file", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-dynamiclib", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-force_load", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-image_base", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEq("--unwindlib"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xassembler", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--cuda-path"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-iplugindir"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-static-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-diag-file"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-diag-dump", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-prec-sqrt", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-traceback", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Xanalyzer", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-arch_only", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-framework", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-gcodeview", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--hip-link", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-segcreate", 3), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEq("-unwindlib"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--language"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-emit-llvm", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--hip-path"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-trigraphs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-idirafter", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-imultilib", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-parallel", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-prof-dir"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-prof-gen", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-prof-use", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-fp-model"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-prec-div", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-all_load", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("--analyze", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-dsym-dir"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-dylinker", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-emit-ast", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--migrate", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--profile", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-seg1addr"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--sysroot"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-twolevel", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-umbrella", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-dumpbase", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("--version", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-isysroot", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("--sysroot"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nostdlib", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-rdynamic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-symbolic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-aux-info", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-qopenmp", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-no-simd", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("--prefix"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("--config", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-dynamic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--output"), ArgumentKind::Output), FlagRule::new(FlagPattern::Exactly("-segaddr", 2), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-segprot", 3), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--target"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-dumpdir", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-wrapper", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithEq("-fplugin"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-include"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-imacros", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-pthread", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-isystem", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-iprefix", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("-fuse-ld"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xlinker", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-fmodule", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-no-ipo", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-no-ftz", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-no-vec", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-no-sox", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-ObjC++", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-bundle", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-objcmt", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-object", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-target", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Xclang", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-triple", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-plugin", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-iquote", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nolibc", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-no-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-static", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-shared", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-ipo-c", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-ipo-S", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-debug", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-align", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-Xarch", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-Xcuda", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-mllvm"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--pipe", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Prefix("--save", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Prefix("--help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::ExactlyWithEq("-specs"), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-undef", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-remap", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("-entry"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-qmkl"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-simd", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-ObjC", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-arch", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-cuid"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-init", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-lazy", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-sect", 3), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-seg_", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-sub_", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-time", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-load", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-pipe", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-ansi", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-save", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-ipo", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-ftz", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-vec", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-sox", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-###", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-MMD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-Wp,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-Wl,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-Wa,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-std"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-tno", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-ax", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-ip", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-EB", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-MJ", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MM", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MG", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MP", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MF", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MT", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MQ", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-CC", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-no", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-x", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-V", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-F"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Z", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-a", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-r", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-x", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-c", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-S", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Assembling))), FlagRule::new(FlagPattern::Exactly("-E", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-o"), ArgumentKind::Output), FlagRule::new(FlagPattern::Exactly("-v", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-A"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-D"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-U"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-M", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-C", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-P", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-H", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-I"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-L"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-B"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-l"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-e", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-r", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-s", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-T", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-u", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-z", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-O", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-g", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-f", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-m", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-p", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-W", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-d", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-Q", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-X", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-Y", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("--", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("@", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), ]; static INTEL_CC_IGNORE_EXECUTABLES: [&str; 7] = ["cc1", "cc1plus", "cc1obj", "cc1objplus", "f951", "collect2", "lto1", ]; static INTEL_CC_IGNORE_FLAGS: [&str; 1] = ["-cc1", ]; static INTEL_CC_SLASH_PREFIX: bool = false; static INTEL_CC_ENV_RULES: [EnvRule; 5] = [ EnvRule::new("CPATH", EnvMapping::Flag { flag: "-I", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("C_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("CPLUS_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("OBJC_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("LIBRARY_PATH", EnvMapping::Flag { flag: "-L", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), ]; snapshots__snapshot_flags_intel_cl.snap000066400000000000000000000524231527177720300361460ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/snapshots--- source: build-support/compilers-codegen/tests/snapshots.rs assertion_line: 23 expression: generated --- // Generated from compilers/intel_cl.yaml -- DO NOT EDIT static INTEL_CL_FLAGS: [FlagRule; 174] = [ FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-fms-compatibility-version"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-fprofile-instr-generate", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-fprofile-instr-generate"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-fcrash-diagnostics-dir"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-ftrivial-auto-var-init"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/external:anglebrackets", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-fno-color-diagnostics", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/Qfast_transcendentals", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-fno-coverage-mapping", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-fsanitize-blacklist"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-fcolor-diagnostics", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-fprofile-instr-use"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-fno-ms-extensions", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-fcoverage-mapping", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithColon("/execution-charset"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/validate-charset", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-fms-extensions", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/vctoolsversion"), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithColon("/source-charset"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/showFilenames", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/Qspectre-load", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-fmsc-version"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-fno-sanitize"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--driver-mode"), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithColon("/external:env"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("/showIncludes", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("/await:strict", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/permissive-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithColon("/Qvec-report"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithColon("/diagnostics"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithColon("/errorReport"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/vctoolsdir"), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/winsysroot"), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/external:I"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("/external:W", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-fsanitize"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithColon("/constexpr"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--version", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::ExactlyWithColon("/volatile"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Qspectre", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/analyze-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-fno-lto", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--target"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-fuse-ld"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithColon("/analyze"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/analyze", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Xclang", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/bigobj", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/openmp", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/nologo", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithColon("/clang"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/imsvc"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithColon("/favor"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithColon("/guard"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/utf-8", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/await", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-flto", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("-flto"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-msse", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-mavx", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::ExactlyWithColon("/arch"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Qpar", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Wall", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/link", 0), ArgumentKind::Other(PassEffect::PassThrough)), FlagRule::new(FlagPattern::Exactly("-m32", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-m64", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-###", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/Fo:"), ArgumentKind::Output), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/Fe:"), ArgumentKind::Output), FlagRule::new(FlagPattern::ExactlyWithColonOrSep("/doc"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithColon("/std"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/GR-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/MDd", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/MTd", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/clr", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/RTC", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/sdl", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Zo-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/WX-", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/LDd", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("/EP", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("/Fo", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/Fe", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("/Fo", 0), ArgumentKind::Output), FlagRule::new(FlagPattern::Prefix("/Fe", 0), ArgumentKind::Output), FlagRule::new(FlagPattern::Prefix("/Fd", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/FA", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/Fa", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/Fi", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("/Fm", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("/FR", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/FI"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/Tc"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/Tp"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/TC", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/TP", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithColon("/Zc"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/O1", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/O2", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/Ob", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Od", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Og", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Oi", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Os", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Ot", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Ox", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Oy", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/GL", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Gw", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Gy", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/EH", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithColon("/fp"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/GA", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Gd", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Ge", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/GF", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Gh", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/GH", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Gr", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/GR", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/Gs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/GT", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Gv", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Gz", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/GZ", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/MD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/MT", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/ZI", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Zi", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Z7", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Zf", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithColon("/ZH"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Zo", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/WX", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/W0", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/W1", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/W2", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/W3", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/W4", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithColon("/Wv"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/Wv", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/w1"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/w2"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/w3"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/w4"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/wd"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/we"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/wo"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/FC", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("/Yc", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/Yu", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/Fp", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/YX", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Y-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/LD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("/LN", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/AI"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/FU"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/MP", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("/FS", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-v", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("/?", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("/c", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/E", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("/P", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/I"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/D"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/U"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("/u", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("/X", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("/w", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/F", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("/J", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), ]; static INTEL_CL_IGNORE_EXECUTABLES: [&str; 3] = ["c1", "c1xx", "c2", ]; static INTEL_CL_IGNORE_FLAGS: [&str; 0] = []; static INTEL_CL_SLASH_PREFIX: bool = true; static INTEL_CL_ENV_RULES: [EnvRule; 4] = [ EnvRule::new("CL", EnvMapping::Expand { position: EnvPosition::Prepend }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), EnvRule::new("_CL_", EnvMapping::Expand { position: EnvPosition::Append }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), EnvRule::new("INCLUDE", EnvMapping::Flag { flag: "/I", separator: EnvSeparator::Fixed(";") }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("LIB", EnvMapping::Flag { flag: "/LIBPATH:", separator: EnvSeparator::Fixed(";") }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), ]; snapshots__snapshot_flags_intel_fortran.snap000066400000000000000000000212161527177720300372170ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/snapshots--- source: bear-codegen/tests/snapshots.rs expression: "generate_flag_file(\"intel_fortran\")" --- // Generated from compilers/intel_fortran.yaml -- DO NOT EDIT static INTEL_FORTRAN_FLAGS: [FlagRule; 71] = [ FlagRule::new(FlagPattern::Exactly("-debug-parameters", 1), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-nogen-interfaces", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-preprocess-only", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-gen-interfaces", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-static-libgcc", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-shared-libgcc", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-nodefaultlibs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-shared-intel", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-static-intel", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-nostartfiles", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-Xoption,link", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-dumpmachine", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Prefix("-Xoption,fpp", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-Xoption,cpp", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-Xoption,asm", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("--version", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-nostdlib", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-isysroot", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("--sysroot"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-gen-dep", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-pthread", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-isystem", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-include", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-Xlinker", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-include"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-dryrun", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-shared", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-static", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-iquote", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-debug", 1), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-nofpp", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-undef", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("--help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Prefix("-diag-", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("--help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-MMD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-fpp", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("-std"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Ep", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MF", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-FA", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Prefix("-Fa", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Prefix("-Wl", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-Wp", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-no", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-T", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-C", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-u", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-V", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-X", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-c", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-E", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-o", 1), ArgumentKind::Output), FlagRule::new(FlagPattern::Exactly("-P", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-r", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-S", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Assembling))), FlagRule::new(FlagPattern::Exactly("-s", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-D"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-I"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-L"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-l"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-U"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("--", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-f", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-g", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-m", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-O", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-v", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Prefix("-x", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("@", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), ]; static INTEL_FORTRAN_IGNORE_EXECUTABLES: [&str; 0] = []; static INTEL_FORTRAN_IGNORE_FLAGS: [&str; 0] = []; static INTEL_FORTRAN_SLASH_PREFIX: bool = false; static INTEL_FORTRAN_ENV_RULES: [EnvRule; 0] = [ ]; snapshots__snapshot_flags_mpi.snap000066400000000000000000000341151527177720300351400ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/snapshots--- source: build-support/compilers-codegen/tests/snapshots.rs expression: "generate_flag_file(\"mpi\")" --- // Generated from compilers/mpi.yaml -- DO NOT EDIT static MPI_FLAGS: [FlagRule; 111] = [ FlagRule::new(FlagPattern::Exactly("-iwithprefixbefore", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("-ffile-prefix-map"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-pass-exit-codes", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithEq("-flinker-output"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xpreprocessor", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nodefaultlibs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-compile_info", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-dumpbase-ext", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("--target-help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Prefix("-fplugin-arg-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-nostartfiles", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-traditional", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-iwithprefix", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("-iplugindir"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-static-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xassembler", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))), FlagRule::new(FlagPattern::Exactly("-link_info", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-trigraphs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-idirafter", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-imultilib", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-arch_only", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-dumpbase", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("--version", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-isysroot", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("--sysroot"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nostdlib", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-rdynamic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-symbolic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-aux-info", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-dumpdir", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-wrapper", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithEq("-fplugin"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-include"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-imacros", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-pthread", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-isystem", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-iprefix", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("-fuse-ld"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xlinker", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-fmodule", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-showme", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-iquote", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nolibc", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-no-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-static", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-shared", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("--help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::ExactlyWithEq("-specs"), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-undef", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-remap", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("-entry"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-show", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-pipe", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-arch", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-ansi", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-save", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-cxx"), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-###", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-MMD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-Wp,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-Wl,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-Wa,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-std"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-tno", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-cc"), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-fc"), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-MM", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MG", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MP", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MF", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MT", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MQ", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-CC", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-no", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-x", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-c", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-S", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Assembling))), FlagRule::new(FlagPattern::Exactly("-E", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-o"), ArgumentKind::Output), FlagRule::new(FlagPattern::Exactly("-v", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-A"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-D"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-U"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-M", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-C", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-P", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-H", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-I"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-L"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-B"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-l"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-e", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-r", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-s", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-T", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-u", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-z", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-O", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-g", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-f", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-m", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-p", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-W", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-d", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-Q", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-X", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-Y", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("--", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("@", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), ]; static MPI_IGNORE_EXECUTABLES: [&str; 7] = ["cc1", "cc1plus", "cc1obj", "cc1objplus", "f951", "collect2", "lto1", ]; static MPI_IGNORE_FLAGS: [&str; 0] = []; static MPI_SLASH_PREFIX: bool = false; static MPI_ENV_RULES: [EnvRule; 5] = [ EnvRule::new("CPATH", EnvMapping::Flag { flag: "-I", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("C_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("CPLUS_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("OBJC_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("LIBRARY_PATH", EnvMapping::Flag { flag: "-L", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), ]; snapshots__snapshot_flags_msvc.snap000066400000000000000000000414501527177720300353230ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/snapshots--- source: build-support/compilers-codegen/tests/snapshots.rs assertion_line: 23 expression: generated --- // Generated from compilers/msvc.yaml -- DO NOT EDIT static MSVC_FLAGS: [FlagRule; 138] = [ FlagRule::new(FlagPattern::Exactly("/external:anglebrackets", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/Qfast_transcendentals", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithColon("/execution-charset"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/validate-charset", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithColon("/source-charset"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Qspectre-load", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithColon("/external:env"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("/showIncludes", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("/await:strict", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/permissive-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithColon("/Qvec-report"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithColon("/diagnostics"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithColon("/errorReport"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/external:I"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("/external:W", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithColon("/constexpr"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithColon("/volatile"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Qspectre", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/analyze-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithColon("/analyze"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/analyze", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/bigobj", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/openmp", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/nologo", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithColon("/favor"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithColon("/guard"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/utf-8", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/await", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::ExactlyWithColon("/arch"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Qpar", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Wall", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/link", 0), ArgumentKind::Other(PassEffect::PassThrough)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/Fo:"), ArgumentKind::Output), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/Fe:"), ArgumentKind::Output), FlagRule::new(FlagPattern::ExactlyWithColonOrSep("/doc"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithColon("/std"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/GR-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/MDd", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/MTd", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/clr", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/RTC", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/sdl", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Zo-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/WX-", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/LDd", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("/EP", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("/Fo", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/Fe", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("/Fo", 0), ArgumentKind::Output), FlagRule::new(FlagPattern::Prefix("/Fe", 0), ArgumentKind::Output), FlagRule::new(FlagPattern::Prefix("/Fd", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/FA", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/Fa", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/Fi", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("/Fm", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("/FR", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/FI"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/Tc"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/Tp"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/TC", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/TP", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithColon("/Zc"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/O1", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/O2", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/Ob", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Od", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Og", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Oi", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Os", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Ot", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Ox", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Oy", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/GL", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Gw", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Gy", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/EH", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithColon("/fp"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/GA", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Gd", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Ge", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/GF", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Gh", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/GH", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Gr", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/GR", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/Gs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/GT", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Gv", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Gz", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/GZ", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/MD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/MT", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/ZI", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Zi", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Z7", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Zf", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithColon("/ZH"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Zo", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/WX", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/W0", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/W1", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/W2", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/W3", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/W4", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithColon("/Wv"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/Wv", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/w1"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/w2"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/w3"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/w4"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/wd"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/we"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/wo"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/FC", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("/Yc", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/Yu", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/Fp", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/YX", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/Y-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/LD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("/LN", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/AI"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/FU"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("/MP", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("/FS", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("/?", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("/c", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("/E", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("/P", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/I"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/D"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/U"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("/u", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("/X", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("/w", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("/F", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("/J", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), ]; static MSVC_IGNORE_EXECUTABLES: [&str; 3] = ["c1", "c1xx", "c2", ]; static MSVC_IGNORE_FLAGS: [&str; 0] = []; static MSVC_SLASH_PREFIX: bool = true; static MSVC_ENV_RULES: [EnvRule; 4] = [ EnvRule::new("CL", EnvMapping::Expand { position: EnvPosition::Prepend }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), EnvRule::new("_CL_", EnvMapping::Expand { position: EnvPosition::Append }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), EnvRule::new("INCLUDE", EnvMapping::Flag { flag: "/I", separator: EnvSeparator::Fixed(";") }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("LIB", EnvMapping::Flag { flag: "/LIBPATH:", separator: EnvSeparator::Fixed(";") }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), ]; snapshots__snapshot_flags_nasm.snap000066400000000000000000000106441527177720300353120ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/snapshots--- source: build-support/compilers-codegen/tests/snapshots.rs expression: "generate_flag_file(\"nasm\")" --- // Generated from compilers/nasm.yaml -- DO NOT EDIT static NASM_FLAGS: [FlagRule; 34] = [ FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-MF"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-MD"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-MT"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-MQ"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MG", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MP", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MW", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-f"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-o"), ArgumentKind::Output), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-I"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-i"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-D"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-d"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-U"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-u"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-P"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-p"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-M", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-E", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-e", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-l"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-F"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-a", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-t", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-g", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-O", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-w", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-W", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-X", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-Z"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-s", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-v", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-h", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Prefix("--", 0), ArgumentKind::Other(PassEffect::None)), ]; static NASM_IGNORE_EXECUTABLES: [&str; 0] = []; static NASM_IGNORE_FLAGS: [&str; 0] = []; static NASM_SLASH_PREFIX: bool = false; static NASM_ENV_RULES: [EnvRule; 0] = [ ]; snapshots__snapshot_flags_nvidia_hpc.snap000066400000000000000000000257221527177720300364630ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/snapshots--- source: bear-codegen/tests/snapshots.rs expression: "generate_flag_file(\"nvidia_hpc\")" --- // Generated from compilers/nvidia_hpc.yaml -- DO NOT EDIT static NVIDIA_HPC_FLAGS: [FlagRule; 89] = [ FlagRule::new(FlagPattern::Exactly("-Mnostack_arrays", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Mnoautoinline", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-nodefaultlibs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Mstack_arrays", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-noswitcherror", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-Mnofprelaxed", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Mautoinline", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Mpreprocess", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-Mfprelaxed", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Mnoinline", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Mnounroll", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Mnoconcur", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Mnostdlib", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Mnostdinc", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-Mnobounds", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Mnochkstk", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Mnoflushz", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-Mneginfo"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-Bdynamic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-nostdlib", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("--version", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-Mkeepasm", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Minline", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Munroll", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Mnovect", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-Mconcur"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-mcmodel"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Bstatic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-Minform"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-Mnosave", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Mbounds", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Mchkstk", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Mflushz", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Mnoipa", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Mnolre", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-nocuda", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-shared", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-static", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-soname"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Mnodaz", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Mnofma", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-dryrun", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-Werror", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-Mvect"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-noacc", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-Mcuda"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-Minfo"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-Mprof"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-rpath"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Msave", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-Manno", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-fast", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-Mipa"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Mlre", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-cuda", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-nomp", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-gopt", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Mdaz", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Mfma", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-show", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-std"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-acc"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-gpu"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-m32", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-m64", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-Wl,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-###", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-mp"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-tp"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-pg", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-o"), ArgumentKind::Output), FlagRule::new(FlagPattern::Exactly("-c", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-S", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Assembling))), FlagRule::new(FlagPattern::Exactly("-E", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-I"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-D"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-U"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-O", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-g", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-L"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-l"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-r", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-w", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-W", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-V", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-v", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Prefix("@", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), ]; static NVIDIA_HPC_IGNORE_EXECUTABLES: [&str; 0] = []; static NVIDIA_HPC_IGNORE_FLAGS: [&str; 0] = []; static NVIDIA_HPC_SLASH_PREFIX: bool = false; static NVIDIA_HPC_ENV_RULES: [EnvRule; 0] = [ ]; snapshots__snapshot_flags_qnx.snap000066400000000000000000000327241527177720300351650ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/snapshots--- source: build-support/compilers-codegen/tests/snapshots.rs expression: "generate_flag_file(\"qnx\")" --- // Generated from compilers/qnx.yaml -- DO NOT EDIT static QNX_FLAGS: [FlagRule; 105] = [ FlagRule::new(FlagPattern::Exactly("-iwithprefixbefore", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("-ffile-prefix-map"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-pass-exit-codes", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithEq("-flinker-output"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xpreprocessor", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nodefaultlibs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-dumpbase-ext", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("--target-help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Prefix("-fplugin-arg-", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-nostartfiles", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-traditional", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-iwithprefix", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("-iplugindir"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-static-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xassembler", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))), FlagRule::new(FlagPattern::Exactly("-trigraphs", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-idirafter", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-imultilib", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-arch_only", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-dumpbase", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("--version", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-isysroot", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("--sysroot"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nostdlib", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-rdynamic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-symbolic", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-aux-info", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-dumpdir", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-wrapper", 1), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithEq("-fplugin"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-include"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-imacros", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-pthread", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-isystem", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-iprefix", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("-fuse-ld"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xlinker", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-fmodule", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-iquote", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-nolibc", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-no-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-static", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-shared", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("--help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::ExactlyWithEq("-specs"), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-undef", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-remap", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("-entry"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-pipe", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-arch", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-ansi", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-save", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-###", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-MMD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-Wp,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-pie", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-Wl,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-Wa,", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-std"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-tno", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-MM", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MG", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MP", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MD", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MF", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MT", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-MQ", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-CC", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-no", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-V", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::Exactly("-x", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-c", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-S", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Assembling))), FlagRule::new(FlagPattern::Exactly("-E", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-o"), ArgumentKind::Output), FlagRule::new(FlagPattern::Exactly("-v", 0), ArgumentKind::Other(PassEffect::DriverOption)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-A"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-D"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-U"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-M", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-C", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-P", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-H", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-I"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-L"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-B"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-l"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-e", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-r", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-s", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-T", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-u", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-z", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Prefix("-O", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-g", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-f", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-m", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-p", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-W", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-d", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-Q", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-X", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-Y", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("--", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("@", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), ]; static QNX_IGNORE_EXECUTABLES: [&str; 7] = ["cc1", "cc1plus", "cc1obj", "cc1objplus", "f951", "collect2", "lto1", ]; static QNX_IGNORE_FLAGS: [&str; 0] = []; static QNX_SLASH_PREFIX: bool = false; static QNX_ENV_RULES: [EnvRule; 5] = [ EnvRule::new("CPATH", EnvMapping::Flag { flag: "-I", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("C_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("CPLUS_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("OBJC_INCLUDE_PATH", EnvMapping::Flag { flag: "-isystem", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), EnvRule::new("LIBRARY_PATH", EnvMapping::Flag { flag: "-L", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), ]; snapshots__snapshot_flags_swift.snap000066400000000000000000000130211527177720300355000ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/snapshots--- source: build-support/compilers-codegen/tests/snapshots.rs expression: "generate_flag_file(\"swift\")" --- // Generated from compilers/swift.yaml -- DO NOT EDIT static SWIFT_FLAGS: [FlagRule; 43] = [ FlagRule::new(FlagPattern::Exactly("-whole-module-optimization", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-emit-clang-header-path", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-emit-objc-header-path", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-import-objc-header", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-module-cache-path", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-working-directory", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-emit-module-path", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-index-store-path", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-parse-as-library", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-output-file-map", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-emit-executable", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-enable-testing", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-swift-version", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-emit-library", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-emit-object", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-module-name", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-num-threads", 1), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-emit-module", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-framework", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-Xfrontend", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--version", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-Xlinker", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-version", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-target", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-static", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Onone", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Osize", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-sdk", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-Xcc", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-wmo", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-o"), ArgumentKind::Output), FlagRule::new(FlagPattern::Exactly("-c", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-j"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-D"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-I"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-F"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-L"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-l"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("-O", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-g", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-v", 0), ArgumentKind::Other(PassEffect::None)), ]; static SWIFT_IGNORE_EXECUTABLES: [&str; 1] = ["swift-frontend", ]; static SWIFT_IGNORE_FLAGS: [&str; 1] = ["-frontend", ]; static SWIFT_SLASH_PREFIX: bool = false; static SWIFT_ENV_RULES: [EnvRule; 0] = [ ]; snapshots__snapshot_flags_ti_cgt.snap000066400000000000000000000236351527177720300356310ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/snapshots--- source: build-support/compilers-codegen/tests/snapshots.rs assertion_line: 23 expression: generated --- // Generated from compilers/ti_cgt.yaml -- DO NOT EDIT static TI_CGT_FLAGS: [FlagRule; 78] = [ FlagRule::new(FlagPattern::Exactly("--preproc_with_comment", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("--preproc_with_compile", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("--preproc_dependency"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("--preproc_with_line", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("--compiler_revision", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::ExactlyWithEq("--preproc_includes"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("--skip_assembler", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Assembling))), FlagRule::new(FlagPattern::ExactlyWithEq("--preproc_macros"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("--list_directory"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("--temp_directory"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("--obj_directory"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("--asm_directory"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("--abs_directory"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--compile_only", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("--include_path"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("--preproc_only", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("--pp_directory"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("--asm_undefine"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))), FlagRule::new(FlagPattern::Exactly("--tool_version", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::ExactlyWithEq("--output_file"), ArgumentKind::Output), FlagRule::new(FlagPattern::ExactlyWithEq("--search_path"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::Exactly("--run_linker", 0), ArgumentKind::Other(PassEffect::PassThrough)), FlagRule::new(FlagPattern::ExactlyWithEq("--preinclude"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("--asm_define"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))), FlagRule::new(FlagPattern::ExactlyWithEq("--undefine"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("--cpp_file"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("--asm_file"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))), FlagRule::new(FlagPattern::ExactlyWithEq("--obj_file"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEq("--cmd_file"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEq("--library"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithEq("--define"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithEq("--c_file"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-version", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("--help", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Exactly("-rtti", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-neon", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-ppo", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-ppc", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-ppl", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-ppd", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-ppi", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-ppm", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Exactly("-ppa", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-fe"), ArgumentKind::Output), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-fr"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-fs"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-ff"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-fb"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-ft"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-fc"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-fp"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-fa"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-fo"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-ad"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-au"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))), FlagRule::new(FlagPattern::Exactly("-fg", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-rr"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-c", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-n", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Assembling))), FlagRule::new(FlagPattern::Exactly("-z", 0), ArgumentKind::Other(PassEffect::PassThrough)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-I"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-D"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-U"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing))), FlagRule::new(FlagPattern::Prefix("-a", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Assembling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-l"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-i"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-@"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Prefix("-m", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-O", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-o", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-p", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-e", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-g", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-k", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-q", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Prefix("-s", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-h", 0), ArgumentKind::Other(PassEffect::InfoAndExit)), FlagRule::new(FlagPattern::Prefix("--", 0), ArgumentKind::Other(PassEffect::None)), ]; static TI_CGT_IGNORE_EXECUTABLES: [&str; 0] = []; static TI_CGT_IGNORE_FLAGS: [&str; 0] = []; static TI_CGT_SLASH_PREFIX: bool = false; static TI_CGT_ENV_RULES: [EnvRule; 0] = [ ]; snapshots__snapshot_flags_vala.snap000066400000000000000000000121471527177720300352770ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/snapshots--- source: bear-codegen/tests/snapshots.rs expression: "generate_flag_file(\"vala\")" --- // Generated from compilers/vala.yaml -- DO NOT EDIT static VALA_FLAGS: [FlagRule; 42] = [ FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--internal-header"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--shared-library"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--gresourcesdir"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--internal-vapi"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--use-fast-vapi"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--metadatadir"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--target-glib"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--gresources"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--includedir"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--pkg-config"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--directory"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--fast-vapi"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--dump-tree"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--run-args"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("--compile", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--vapidir"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--basedir"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--library"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--depfile"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--symbols"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--output"), ArgumentKind::Output), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--define"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--girdir"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--header"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("--ccode", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--main"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--vapi"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--deps"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--pkg"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("--Xcc", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEq("--Xcc"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--gir"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithEqOrSep("--cc"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-o"), ArgumentKind::Output), FlagRule::new(FlagPattern::Exactly("-C", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-c", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-D"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-b"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::Exactly("-X", 1), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-d"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-H"), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-h"), ArgumentKind::Other(PassEffect::None)), ]; static VALA_IGNORE_EXECUTABLES: [&str; 0] = []; static VALA_IGNORE_FLAGS: [&str; 0] = []; static VALA_SLASH_PREFIX: bool = false; static VALA_ENV_RULES: [EnvRule; 0] = [ ]; snapshots__snapshot_recognition.snap000066400000000000000000000076561527177720300355310ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/snapshots--- source: build-support/compilers-codegen/tests/snapshots.rs expression: "generate_recognition_patterns(&raw_tables, &compiler_files,\n&wrapper_tables).unwrap()" --- // Generated from compilers/*.yaml -- DO NOT EDIT #[allow(clippy::type_complexity, reason = "plain generated data, not an API to simplify with a type alias")] pub static RECOGNITION_PATTERNS: &[(&str, &[&str], bool, bool, Option<&str>)] = &[ ("arm_flang", &["armflang"], false, false, Some("Arm Compiler for Linux Fortran")), ("armclang", &["armclang", "armclang++"], false, true, Some("Arm Compiler 6")), ("cray_cc", &["craycc", "crayCC", "craycxx"], false, true, Some("Cray C/C++")), ("embarcadero", &["bcc64x", "bcc64", "bcc32x"], false, false, Some("Embarcadero C++Builder (Clang-based)")), ("ibm_xl", &["ibm-clang", "ibm-clang++"], false, true, Some("IBM Open XL C/C++")), ("ibm_xl", &["xlclang", "xlclang++"], false, true, Some("IBM XL C/C++")), ("intel_cc", &["icx", "icpx"], false, true, Some("Intel oneAPI C/C++")), ("intel_cc", &["icc", "icpc"], false, true, Some("Intel C++ Compiler Classic")), ("intel_cc", &["mpiicc", "mpiicpc", "mpiicx", "mpiicpx"], false, false, Some("Intel MPI C/C++ wrapper")), ("intel_cl", &["icx-cl"], false, false, Some("Intel oneAPI DPC++/C++ (MSVC mode)")), ("clang", &["clang", "clang++"], true, true, Some("Clang")), ("clang", &["amdclang", "amdclang++", "hipcc"], false, false, Some("AMD ROCm Clang/HIP")), ("clang", &["emcc", "em++", "emcc.py", "em++.py"], false, false, Some("Emscripten")), ("clang", &["tiarmclang"], false, false, Some("Texas Instruments Arm Clang")), ("clang_cl", &["clang-cl"], false, true, Some("Clang (MSVC mode)")), ("cuda", &["nvcc"], true, true, Some("NVIDIA CUDA")), ("flang", &["flang", "flang-new"], true, true, Some("Flang")), ("flang", &["amdflang"], false, false, Some("AMD ROCm Flang")), ("mpi", &["mpicc", "mpicxx", "mpic++", "mpiCC", "mpifort", "mpif77", "mpif90"], false, true, Some("MPI compiler wrapper")), ("qnx", &["qcc", "q++"], false, false, Some("QNX qcc")), ("cray_fortran", &["crayftn", "ftn"], false, true, Some("Cray Fortran")), ("fasm", &["fasm"], false, false, Some("flat assembler")), ("gcc", &["gcc", "g++", "gfortran", "egfortran", "f95"], true, true, Some("GCC")), ("gcc", &["xc8-cc", "xc8"], false, false, Some("Microchip XC8")), ("gcc", &["cc1", "cc1plus", "cc1obj", "cc1objplus", "f951", "collect2", "lto1"], false, false, None), ("intel_fortran", &["ifort", "ifx"], false, true, Some("Intel Fortran")), ("intel_fortran", &["mpiifort", "mpiifx"], false, false, Some("Intel Fortran MPI wrapper")), ("msvc", &["cl"], false, false, Some("Microsoft Visual C++")), ("msvc", &["c1", "c1xx", "c2"], false, false, None), ("nasm", &["nasm", "yasm"], false, false, Some("NASM / YASM assembler")), ("nvidia_hpc", &["nvc", "nvc++", "nvfortran"], false, true, Some("NVIDIA HPC SDK")), ("nvidia_hpc", &["pgcc", "pgc++", "pgfortran"], false, true, Some("PGI (legacy NVIDIA HPC)")), ("swift", &["swiftc"], false, false, Some("Swift")), ("swift", &["swift-frontend"], false, false, None), ("ti_cgt", &["armcl"], false, false, Some("TI Code Generation Tools (Arm)")), ("ti_cgt", &["cl6x"], false, false, Some("TI Code Generation Tools (C6000)")), ("ti_cgt", &["cl7x"], false, false, Some("TI Code Generation Tools (C7000)")), ("ti_cgt", &["cl2000"], false, false, Some("TI Code Generation Tools (C2000)")), ("ti_cgt", &["cl430"], false, false, Some("TI Code Generation Tools (MSP430)")), ("ti_cgt", &["clpru"], false, false, Some("TI Code Generation Tools (PRU)")), ("vala", &["valac"], true, true, Some("Vala")), ("wrapper", &["ccache"], false, false, Some("Compiler cache")), ("wrapper", &["sccache"], false, false, Some("Compiler cache")), ("wrapper", &["distcc"], false, false, Some("Distributed compiler")), ("wrapper", &["icecc"], false, false, Some("Distributed compiler")), ]; rizsotto-Bear-0c30b27/build-support/compilers-codegen/tests/yaml_validation.rs000066400000000000000000000277051527177720300277220ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! YAML schema validation tests. //! //! These tests validate all YAML compiler definitions at test time, //! providing better error messages than build-time panics. use compilers_codegen::codegen::{pattern_to_rust, result_to_rust}; use compilers_codegen::resolve::resolve_environment; use compilers_codegen::tables::CompilerFile; use compilers_codegen::{ insert_by_id, load_compiler_files, load_tables, parse_table, parse_wrapper_table, validate_extends, }; /// Every discovered compiler-kind file loads and is keyed by its id. #[test] fn all_yaml_files_parse() { let tables = load_tables().unwrap(); let compiler_files = load_compiler_files().unwrap(); assert!(!compiler_files.is_empty(), "no compiler files discovered"); for cf in &compiler_files { assert!(tables.contains_key(&cf.id), "discovered {} but it did not load", cf.yaml_file()); } } /// Every `extends` reference points to an existing table. #[test] fn extends_references_are_valid() { let tables = load_tables().unwrap(); for (id, table) in &tables { if let Some(ref base_name) = table.compiler.extends { assert!( tables.contains_key(base_name.as_str()), "{} extends '{}', which does not exist", id, base_name ); } } } /// Every flag entry uses a known result string. #[test] fn all_flag_results_are_valid() { let tables = load_tables().unwrap(); for (id, table) in &tables { for entry in &table.flags { result_to_rust(&entry.result) .unwrap_or_else(|e| panic!("{}: flag '{}': {}", id, entry.match_.pattern, e)); } } } /// Every flag pattern produces valid codegen output. #[test] fn all_flag_patterns_produce_valid_codegen() { let tables = load_tables().unwrap(); for (id, table) in &tables { for entry in &table.flags { let output = pattern_to_rust(&entry.match_.pattern, entry.match_.count); assert!( output.starts_with("FlagPattern::"), "{}: pattern '{}' produced unexpected output: {}", id, entry.match_.pattern, output ); } } } /// Every environment entry in every YAML file passes validation. #[test] fn all_env_entries_are_valid() { let tables = load_tables().unwrap(); let mut errors = Vec::new(); for id in tables.keys() { let entries = resolve_environment(id, &tables); for entry in &entries { if entry.effect == "none" { continue; } if let Err(e) = entry.validate() { errors.push(format!("{}: {}", id, e)); } } } assert!(errors.is_empty(), "Environment validation errors:\n{}", errors.join("\n")); } /// Every environment variable name is a valid C identifier. #[test] fn env_variable_names_are_c_identifiers() { fn is_valid_var_name(s: &str) -> bool { let mut chars = s.chars(); matches!(chars.next(), Some(c) if c.is_ascii_alphabetic() || c == '_') && chars.all(|c| c.is_ascii_alphanumeric() || c == '_') } let tables = load_tables().unwrap(); for (id, table) in &tables { if let Some(ref env) = table.environment { for entry in env { assert!( is_valid_var_name(&entry.variable), "{}: '{}' is not a valid C identifier", id, entry.variable ); } } } } /// No two YAML files extend into a cycle. #[test] fn no_circular_extends() { let tables = load_tables().unwrap(); for start in tables.keys() { let mut visited = std::collections::HashSet::new(); let mut current = Some(start.clone()); while let Some(k) = current { assert!(visited.insert(k.clone()), "{}: circular extends chain detected at '{}'", start, k); current = tables.get(k.as_str()).and_then(|t| t.compiler.extends.clone()); } } } /// Every table has at least one `recognize` entry. /// /// `type:` is mandatory now, so every loaded table is typed by construction; /// this asserts the invariant unconditionally instead of gating on /// `Kind::Compiler` (the only kind any table uses today). #[test] fn typed_tables_have_recognition_entries() { let tables = load_tables().unwrap(); for (id, table) in &tables { assert!( table.recognize.as_ref().is_some_and(|r| !r.is_empty()), "{}: has type but no recognize entries", id ); } } /// Every table has at least one flag entry (own or inherited). #[test] fn all_tables_have_flags() { let tables = load_tables().unwrap(); for (id, table) in &tables { let has_own = !table.flags.is_empty(); let has_inherited = table .compiler .extends .as_ref() .and_then(|base| tables.get(base.as_str())) .is_some_and(|base| !base.flags.is_empty()); assert!(has_own || has_inherited, "{}: no flags defined (own or inherited)", id); } } // -- Negative-case tests for the `type:`/`compiler:` schema split -- // // These exercise the loader against small inline YAML fixtures rather than // mutating the real compiler YAML files on disk. const MINIMAL_FLAGS: &str = "flags: []\n"; /// Two tables declaring the same `compiler.id` fail to load, and the error /// names the file that redefines the id. #[test] fn duplicate_ids_fail_naming_the_file() { let yaml = format!("type: compiler\ncompiler:\n id: dup\n{}", MINIMAL_FLAGS); let first = parse_table("first.yaml", &yaml).unwrap(); let second = parse_table("second.yaml", &yaml).unwrap(); let mut tables = std::collections::HashMap::new(); insert_by_id(&mut tables, "first.yaml", first).unwrap(); let err = insert_by_id(&mut tables, "second.yaml", second).unwrap_err(); assert!(err.to_string().contains("dup"), "{}", err); assert!(err.to_string().contains("second.yaml"), "{}", err); } /// An `extends:` target that no loaded table declares as its id fails /// validation, and the error names the file with the dangling reference. #[test] fn dangling_extends_target_is_detected() { let yaml = format!("type: compiler\ncompiler:\n id: leaf\n extends: nonexistent\n{}", MINIMAL_FLAGS); let table = parse_table("leaf.yaml", &yaml).unwrap(); let mut tables = std::collections::HashMap::new(); let id = insert_by_id(&mut tables, "leaf.yaml", table).unwrap(); let compiler_files = vec![CompilerFile { stem: "leaf".to_string(), id, depth: 0 }]; let err = validate_extends(&tables, &compiler_files).unwrap_err(); assert!(err.to_string().contains("leaf.yaml"), "{}", err); assert!(err.to_string().contains("nonexistent"), "{}", err); } /// A `compiler:` block missing the mandatory `id:` fails to parse, and the /// error names the offending file. #[test] fn missing_id_on_compiler_table_fails() { let yaml = format!("type: compiler\ncompiler:\n extends: base\n{}", MINIMAL_FLAGS); let err = parse_table("no_id.yaml", &yaml).err().unwrap(); assert!(err.to_string().contains("no_id.yaml"), "{}", err); } /// An unrecognized `type:` value fails to parse, and the error names the /// offending file. #[test] fn unknown_type_value_fails() { let yaml = format!("type: bogus\ncompiler:\n id: x\n{}", MINIMAL_FLAGS); let err = parse_table("bad_type.yaml", &yaml).err().unwrap(); assert!(err.to_string().contains("bad_type.yaml"), "{}", err); } /// An unrecognized `source_mode:` value fails to parse (closed enum), and the /// error names the offending file. #[test] fn unknown_source_mode_value_fails() { let yaml = format!("type: compiler\ncompiler:\n id: x\nsource_mode: bogus\n{}", MINIMAL_FLAGS); let err = parse_table("bad_source_mode.yaml", &yaml).err().unwrap(); assert!(err.to_string().contains("bad_source_mode.yaml"), "{}", err); } /// An unrecognized `response_file_syntax:` value fails to parse (closed enum), /// and the error names the offending file. #[test] fn unknown_response_file_syntax_value_fails() { let yaml = format!("type: compiler\ncompiler:\n id: x\nresponse_file_syntax: bogus\n{}", MINIMAL_FLAGS); let err = parse_table("bad_syntax.yaml", &yaml).err().unwrap(); assert!(err.to_string().contains("bad_syntax.yaml"), "{}", err); } // -- Negative-case tests for `WrapperTable` -- // // These exercise `WrapperTable::validate` against small inline YAML // fixtures rather than mutating the real launcher YAML files on disk. const MINIMAL_WRAPPER_RECOGNIZE: &str = concat!( "recognize:\n", " - description: \"Compiler cache\"\n", " references:\n", " - \"https://example.com/docs\"\n", " executables: [\"fake-wrapper\"]\n", ); /// A minimal, valid wrapper table (no options) passes validation. #[test] fn valid_wrapper_table_passes_validation() { let yaml = format!("type: wrapper\n{}", MINIMAL_WRAPPER_RECOGNIZE); let table = parse_wrapper_table("fake_wrapper.yaml", &yaml).unwrap(); assert!(table.validate("fake_wrapper.yaml").is_ok()); } /// A wrapper `recognize` entry with `versioned: true` fails validation, /// naming the offending file. #[test] fn wrapper_recognize_versioned_true_fails() { let yaml = "type: wrapper\nrecognize:\n - description: \"Compiler cache\"\n references:\n - \"https://example.com/docs\"\n executables: [\"fake-wrapper\"]\n versioned: true\n"; let table = parse_wrapper_table("fake_wrapper.yaml", yaml).unwrap(); let err = table.validate("fake_wrapper.yaml").unwrap_err(); assert!(err.to_string().contains("fake_wrapper.yaml"), "{}", err); assert!(err.to_string().contains("versioned"), "{}", err); } /// A wrapper `recognize` entry with `cross_compilation: true` fails /// validation, naming the offending file. #[test] fn wrapper_recognize_cross_compilation_true_fails() { let yaml = "type: wrapper\nrecognize:\n - description: \"Compiler cache\"\n references:\n - \"https://example.com/docs\"\n executables: [\"fake-wrapper\"]\n cross_compilation: true\n"; let table = parse_wrapper_table("fake_wrapper.yaml", yaml).unwrap(); let err = table.validate("fake_wrapper.yaml").unwrap_err(); assert!(err.to_string().contains("fake_wrapper.yaml"), "{}", err); assert!(err.to_string().contains("cross_compilation"), "{}", err); } /// A wrapper `options` entry with a glued/prefix pattern (`-j{ }*`) fails /// validation, naming the offending file and pattern -- the skip loop only /// compares argv tokens for equality, so the schema must reject anything /// that implies glued/prefix/eq/colon matching. #[test] fn wrapper_option_glued_pattern_fails() { let yaml = format!( "type: wrapper\n{}options:\n - match: {{pattern: \"-j{{ }}*\"}}\n", MINIMAL_WRAPPER_RECOGNIZE ); let table = parse_wrapper_table("fake_wrapper.yaml", &yaml).unwrap(); let err = table.validate("fake_wrapper.yaml").unwrap_err(); assert!(err.to_string().contains("fake_wrapper.yaml"), "{}", err); assert!(err.to_string().contains("-j{ }*"), "{}", err); } /// A wrapper `options` entry with a prefix-star pattern (`-j*`) fails /// validation for the same reason. #[test] fn wrapper_option_prefix_star_pattern_fails() { let yaml = format!("type: wrapper\n{}options:\n - match: {{pattern: \"-j*\"}}\n", MINIMAL_WRAPPER_RECOGNIZE); let table = parse_wrapper_table("fake_wrapper.yaml", &yaml).unwrap(); let err = table.validate("fake_wrapper.yaml").unwrap_err(); assert!(err.to_string().contains("fake_wrapper.yaml"), "{}", err); } /// An exact-token `options` pattern (no `*` or `{ }`) passes validation. #[test] fn wrapper_option_exact_token_passes() { let yaml = format!( "type: wrapper\n{}options:\n - match: {{pattern: \"-j\", count: 1}}\n", MINIMAL_WRAPPER_RECOGNIZE ); let table = parse_wrapper_table("fake_wrapper.yaml", &yaml).unwrap(); assert!(table.validate("fake_wrapper.yaml").is_ok()); } rizsotto-Bear-0c30b27/build-support/platform-checks/000077500000000000000000000000001527177720300225065ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/platform-checks/CLAUDE.md000066400000000000000000000033631527177720300237720ustar00rootroot00000000000000## platform-checks Build-time helper library. Each consumer calls it from its own `build.rs`, and it probes the C toolchain of that consumer's target for symbols. Probes run once per consumer. The probe list and the API are in `src/lib.rs`. `SYSTEM_EXTENSIONS` there is the feature-macro list for probes and consumer C sources. ## Constraints - A consumer takes this crate as a `[build-dependencies]` entry and calls `platform_checks::detect()`, then `platform_checks::emit_cfg()` with the result and `platform_checks::emit_check_cfg()`, from its own `build.rs`. The `cfg` keys apply only to the calling crate. - The crate has no `build.rs`. Probing there would see the host as the target in cross builds. - `detect()` returns nothing on non-unix targets and panics on a unix target where `execve` is missing. ## Adding a probe 1. Add the symbol name to `SYMBOL_PROBES` in `src/lib.rs`. The probe links a program that calls it, so it passes only if the target libc exports it. For a header, add it to `HEADER_PROBES` instead; the probe compiles an `#include` and the key is `has_header_` plus the name with every non-alphanumeric character as `_` (`crt_externs.h` is `has_header_crt_externs_h`). 2. Reference the resulting `cfg(has_symbol_X)` from consumer source. 3. Provided the consumer's `build.rs` calls `platform_checks::emit_check_cfg()`, the lint allowlist is covered automatically; no `Cargo.toml` `[lints.rust]` editing needed. ## What this crate does NOT decide It detects target capabilities, not consumer behavior. Which symbols the preload shim exports is decided in `crates/intercept-preload`. Adding an intercepted function touches `SYMBOL_PROBES` here as one of three edits; `crates/intercept-preload/CLAUDE.md` lists them. rizsotto-Bear-0c30b27/build-support/platform-checks/Cargo.toml000066400000000000000000000007511527177720300244410ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later [package] name = "platform-checks" description = "Build-time utilities for system capability detection" version.workspace = true authors.workspace = true repository.workspace = true homepage.workspace = true license.workspace = true edition.workspace = true rust-version.workspace = true publish = false [lib] name = "platform_checks" path = "src/lib.rs" [dependencies] tempfile.workspace = true cc.workspace = true [lints] workspace = true rizsotto-Bear-0c30b27/build-support/platform-checks/src/000077500000000000000000000000001527177720300232755ustar00rootroot00000000000000rizsotto-Bear-0c30b27/build-support/platform-checks/src/lib.rs000066400000000000000000000210031527177720300244050ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Build-time utilities for system capability detection. //! //! Consumer `build.rs` scripts call [`detect`] to probe the C toolchain of //! their own target for symbols, replay the result as `cargo:rustc-cfg=` //! directives by calling [`emit_cfg`], and register the full `check-cfg` //! allowlist by calling [`emit_check_cfg`]. The probes run inside the //! consumer's build script, so `TARGET`, `CC_` and `CFLAGS` are //! the consumer's, not the host's. use std::io::Write; use std::path::Path; use std::process::Stdio; const SYMBOL_PROBES: &[&str] = &[ "execve", "execv", "execvpe", "execvp", "execvP", "exect", "execl", "execlp", "execle", "posix_spawn", "pclose", "posix_spawnp", "popen", "system", "_NSGetEnviron", ]; const HEADER_PROBES: &[&str] = &["crt_externs.h"]; /// Feature macros that autoconf's `AC_USE_SYSTEM_EXTENSIONS` defines /// unconditionally on every system, as (name, value). Consumers pass them to /// the C compiler so their C sources need no `#define` of their own. pub const SYSTEM_EXTENSIONS: &[(&str, &str)] = &[ ("_ALL_SOURCE", "1"), ("_COSMO_SOURCE", "1"), ("_DARWIN_C_SOURCE", "1"), ("_GNU_SOURCE", "1"), ("_HPUX_ALT_XOPEN_SOCKET_API", "1"), ("_NETBSD_SOURCE", "1"), ("_OPENBSD_SOURCE", "1"), ("_POSIX_PTHREAD_SEMANTICS", "1"), ("__STDC_WANT_IEC_60559_ATTRIBS_EXT__", "1"), ("__STDC_WANT_IEC_60559_BFP_EXT__", "1"), ("__STDC_WANT_IEC_60559_DFP_EXT__", "1"), ("__STDC_WANT_IEC_60559_EXT__", "1"), ("__STDC_WANT_IEC_60559_FUNCS_EXT__", "1"), ("__STDC_WANT_IEC_60559_TYPES_EXT__", "1"), ("__STDC_WANT_LIB_EXT2__", "1"), ("__STDC_WANT_MATH_SPEC_FUNCS__", "1"), ("_TANDEM_SOURCE", "1"), ]; /// What the target C toolchain provides. pub struct Detected { pub symbols: Vec<&'static str>, pub headers: Vec<&'static str>, } /// Probe the target C toolchain and return the symbols and headers it /// provides. Call this once from a consumer's `build.rs`. /// /// Returns an empty list on non-unix targets without probing. Panics on a /// unix target when `execve` is not found, because that means the target /// toolchain is broken and every other probe would be wrong too. pub fn detect() -> Detected { emit_rerun_if_env_changed(); let family = std::env::var("CARGO_CFG_TARGET_FAMILY").unwrap_or_default(); if !family.split(',').any(|f| f == "unix") { return Detected { symbols: Vec::new(), headers: Vec::new() }; } let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set"); let probe_dir = tempfile::tempdir_in(out_dir).expect("Failed to create probe directory"); let mut symbols: Vec<&'static str> = Vec::new(); for symbol in SYMBOL_PROBES { match check_symbol_exists(symbol, probe_dir.path()) { Ok(()) => symbols.push(symbol), Err(error) if *symbol == "execve" => { let target = std::env::var("TARGET").unwrap_or_default(); let compiler = cc::Build::new().get_compiler().path().display().to_string(); panic!( "symbol `execve` not found for target {target} with compiler {compiler}: \ the target C toolchain is broken or missing\n{error}" ); } Err(_) => {} } } let headers: Vec<&'static str> = HEADER_PROBES .iter() .copied() .filter(|header| check_header_exists(header, probe_dir.path()).is_ok()) .collect(); Detected { symbols, headers } } /// Emit `cargo:rustc-cfg=has_symbol_*` and `has_header_*` directives /// for everything detected. Call this from a /// consumer's `build.rs` so the directives apply to that crate. pub fn emit_cfg(detected: &Detected) { for symbol in &detected.symbols { println!("cargo:rustc-cfg=has_symbol_{symbol}"); } for header in &detected.headers { println!("cargo:rustc-cfg={}", header_cfg_name(header)); } } /// Emit `cargo:rustc-check-cfg=` directives for every probed flag, so /// rustc accepts them in `cfg(...)` attributes regardless of detection /// outcome. Call this from a consumer's `build.rs`. pub fn emit_check_cfg() { for symbol in SYMBOL_PROBES { println!("cargo:rustc-check-cfg=cfg(has_symbol_{symbol})"); } for header in HEADER_PROBES { println!("cargo:rustc-check-cfg=cfg({})", header_cfg_name(header)); } } /// The `cfg` key, and the C macro name, for a detected header. pub fn header_cfg_name(header: &str) -> String { let mangled: String = header.chars().map(|c| if c.is_ascii_alphanumeric() { c } else { '_' }).collect(); format!("has_header_{mangled}") } /// `cc` is used with `cargo_metadata(false)`, so it does not tell Cargo /// which environment variables select the compiler. Do it here. fn emit_rerun_if_env_changed() { for var in ["CC", "CFLAGS", "TARGET_CC", "TARGET_CFLAGS"] { println!("cargo:rerun-if-env-changed={var}"); } if let Ok(target) = std::env::var("TARGET") { let underscored = target.replace('-', "_"); for var in ["CC", "CFLAGS"] { println!("cargo:rerun-if-env-changed={var}_{target}"); println!("cargo:rerun-if-env-changed={var}_{underscored}"); } } } /// Autoconf's `AC_LANG_FUNC_LINK_TRY`: declare the function ourselves and /// link, so the probe passes only if the target libc exports the symbol. fn check_symbol_exists(symbol: &str, probe_dir: &Path) -> Result<(), String> { let check_code = format!( r#" #define {symbol} innocuous_{symbol} #include #undef {symbol} char {symbol}(void); #if defined __stub_{symbol} || defined __stub___{symbol} choke me #endif int main(void) {{ return {symbol}(); }} "# ); let result = run_probe(&format!("check_{symbol}"), &check_code, probe_dir, true); let outcome = if result.is_ok() { "found" } else { "missing" }; println!("cargo:warning=Checking for symbol: {symbol} ... {outcome}"); result } /// Compile a source that includes the header. Compile only: the header may /// exist while its functions are not linkable, and that is the symbol probe's job. fn check_header_exists(header: &str, probe_dir: &Path) -> Result<(), String> { let check_code = format!( r#" #include <{header}> int main(void) {{ return 0; }} "# ); let name = format!("check_{}", header_cfg_name(header)); let result = run_probe(&name, &check_code, probe_dir, false); let outcome = if result.is_ok() { "found" } else { "missing" }; println!("cargo:warning=Checking for header: {header} ... {outcome}"); result } /// Run the compiler on `code`. On failure, the error holds the compiler's /// output, so a broken toolchain can be reported. fn run_probe(name: &str, code: &str, probe_dir: &Path, link: bool) -> Result<(), String> { let (mut file, path) = tempfile::Builder::new() .prefix(name) .suffix(".c") .tempfile_in(probe_dir) .expect("Failed to create temp file for probe") .keep() .expect("Failed to keep temp file"); file.write_all(code.as_bytes()).expect("Failed to write to temp file"); file.flush().expect("Failed to flush temp file"); let mut build = cc::Build::new(); for (name, value) in SYSTEM_EXTENSIONS { build.define(name, *value); } let compiler = build .cargo_metadata(false) .cargo_output(false) .cargo_warnings(false) .inherit_rustflags(true) .get_compiler(); let mut command = compiler.to_command(); command.arg("-fno-builtin"); if !link { command.arg("-c"); } let output = command .arg(&path) .arg("-o") .arg(probe_dir.join(name)) .stdin(Stdio::null()) .output() .map_err(|error| format!("failed to run {}: {error}", command_line(&command)))?; if output.status.success() { Ok(()) } else { Err(format!( "{}\n{}{}", command_line(&command), String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) )) } } /// The program and its arguments, without the environment: `Command`'s /// `Debug` output lists every variable, secrets included. fn command_line(command: &std::process::Command) -> String { std::iter::once(command.get_program()) .chain(command.get_args()) .map(|part| part.to_string_lossy()) .collect::>() .join(" ") } rizsotto-Bear-0c30b27/crates/000077500000000000000000000000001527177720300160745ustar00rootroot00000000000000rizsotto-Bear-0c30b27/crates/bear-completions/000077500000000000000000000000001527177720300213375ustar00rootroot00000000000000rizsotto-Bear-0c30b27/crates/bear-completions/CLAUDE.md000066400000000000000000000011411527177720300226130ustar00rootroot00000000000000## bear-completions The reason this is a separate crate is in `docs/rationale/completions-own-crate.md`. ## How it's invoked Not by `cargo build` for the main install. The distributor runs the binary explicitly (`INSTALL.md` documents the command); it writes shell-completion files into a directory which `scripts/install.sh` then picks up if present. The install script does not run the generator itself. ## Dependency on bear Depends on `bear` only for `bear::args::cli()` (the `clap` builder). This is a one-way edge from `bear-completions` to `bear`; nothing in `bear` references `bear-completions`. rizsotto-Bear-0c30b27/crates/bear-completions/Cargo.toml000066400000000000000000000007661527177720300233000ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later [package] name = "bear-completions" description = "Shell completion script generator for Bear." version.workspace = true authors.workspace = true repository.workspace = true homepage.workspace = true license.workspace = true edition.workspace = true rust-version.workspace = true publish = false [[bin]] name = "generate-completions" path = "src/main.rs" [dependencies] bear = { path = "../bear" } clap_complete.workspace = true [lints] workspace = true rizsotto-Bear-0c30b27/crates/bear-completions/src/000077500000000000000000000000001527177720300221265ustar00rootroot00000000000000rizsotto-Bear-0c30b27/crates/bear-completions/src/main.rs000066400000000000000000000016051527177720300234220ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Generates shell completion scripts for the `bear` command. //! //! Usage: //! generate-completions //! //! Writes completion files for bash, zsh, fish, and elvish into the //! given directory. use bear::args::cli; use clap_complete::Shell; use clap_complete::generate_to; use std::path::PathBuf; fn main() { let outdir = std::env::args().nth(1).map(PathBuf::from).unwrap_or_else(|| { eprintln!("usage: generate-completions "); std::process::exit(1); }); std::fs::create_dir_all(&outdir).expect("failed to create output directory"); let mut cmd = cli(); for shell in [Shell::Bash, Shell::Zsh, Shell::Fish, Shell::Elvish] { generate_to(shell, &mut cmd, "bear", &outdir) .unwrap_or_else(|e| panic!("failed to generate {shell} completions: {e}")); } } rizsotto-Bear-0c30b27/crates/bear-driver/000077500000000000000000000000001527177720300202765ustar00rootroot00000000000000rizsotto-Bear-0c30b27/crates/bear-driver/Cargo.toml000066400000000000000000000011701527177720300222250ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later [package] name = "bear-driver" description = "Build supervisor binary for Bear that orchestrates interception and output." version.workspace = true authors.workspace = true repository.workspace = true homepage.workspace = true license.workspace = true edition.workspace = true rust-version.workspace = true publish = false [[bin]] name = "bear-driver" path = "src/main.rs" [dependencies] bear = { path = "../bear" } intercept = { path = "../intercept" } intercept-supervisor = { path = "../intercept-supervisor" } anyhow.workspace = true log.workspace = true [lints] workspace = true rizsotto-Bear-0c30b27/crates/bear-driver/src/000077500000000000000000000000001527177720300210655ustar00rootroot00000000000000rizsotto-Bear-0c30b27/crates/bear-driver/src/main.rs000066400000000000000000000047771527177720300223760ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! This binary is the build supervisor (driver) of the application. //! //! It orchestrates interception and output: it captures the application //! context, derives the installation layout from the current executable, and //! parses the command line arguments. From those inputs it configures the //! selected mode -- which loads the configuration it needs -- and runs it, //! returning the build's exit code. //! //! The heavy lifting lives elsewhere: the `bear` library provides argument //! parsing, configuration, and the modes, while `intercept_supervisor` //! supplies the application context and installation layout. use bear::{args, modes}; use intercept_supervisor::{context, installation}; use std::env; use std::process::ExitCode; /// Driver function of the application. fn main() -> ExitCode { // Initialize the logging system. `RUST_LOG` is honored when set; when // unset, default to `warn` so recoverable anomalies (e.g. parse-sh skip // reports) reach stderr without opting in. intercept::logging::init("bear"); // Report a startup error through the logger, so it gets the same // `bear: error:` format as every other diagnostic. run().unwrap_or_else(|error| { log::error!("{error}"); ExitCode::FAILURE }) } fn run() -> anyhow::Result { // Get the package name and version from Cargo let pkg_name = env!("CARGO_PKG_NAME"); let pkg_version = env!("CARGO_PKG_VERSION"); log::info!("{pkg_name} v{pkg_version}"); let os = env::consts::OS; let family = env::consts::FAMILY; let arch = env::consts::ARCH; log::info!("Running on... {family}/{os} {arch}"); // Capture application context. let context = context::Context::capture()?; log::info!("{context}"); // Log the installation layout derived from the current executable. let layout = installation::InstallationLayout::try_from(context.current_executable.as_path())?; log::info!("{layout}"); // Parse the command line arguments. let matches = args::cli().get_matches(); let arguments = args::Arguments::try_from(matches)?; log::info!("{arguments}"); // Run the application. Configuration is loaded per mode inside `configure` // (parse-sh consults none, so it loads none). let application = modes::Mode::configure(context, arguments)?; log::debug!("Configuration complete, running the build now..."); let result = application.run(); log::debug!("Exit code: {result:?}"); Ok(result) } rizsotto-Bear-0c30b27/crates/bear-wrapper/000077500000000000000000000000001527177720300204635ustar00rootroot00000000000000rizsotto-Bear-0c30b27/crates/bear-wrapper/Cargo.toml000066400000000000000000000011301527177720300224060ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later [package] name = "bear-wrapper" description = "Compiler wrapper binary for Bear that reports intercepted executions." version.workspace = true authors.workspace = true repository.workspace = true homepage.workspace = true license.workspace = true edition.workspace = true rust-version.workspace = true publish = false [[bin]] name = "bear-wrapper" path = "src/main.rs" [dependencies] intercept = { path = "../intercept" } intercept-supervisor = { path = "../intercept-supervisor" } anyhow.workspace = true log.workspace = true [lints] workspace = true rizsotto-Bear-0c30b27/crates/bear-wrapper/src/000077500000000000000000000000001527177720300212525ustar00rootroot00000000000000rizsotto-Bear-0c30b27/crates/bear-wrapper/src/main.rs000066400000000000000000000071331527177720300225500ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! This module implements a wrapper around an arbitrary executable. //! //! The wrapper is used to intercept the execution of the executable and //! report it to a remote server. The wrapper is named after the executable //! via a soft link (or a hard copy on platforms where soft links are not //! supported). The wrapper process is called instead of the original executable. //! This is arranged by the process that supervise the build process. //! The build supervisor creates a directory with soft links and place //! that directory at the beginning of the PATH variable. Which guarantees //! that the wrapper is called instead of the original executable. //! //! The wrapper reads a JSON configuration file from the wrapper directory //! to find the real executable path. It reports the execution of the real //! executable and then calls the real executable with the same arguments. use anyhow::{Context, Result}; use intercept::Execution; use intercept::reporter::{Reporter, ReporterFactory}; use intercept_supervisor::supervise::{GroupPolicy, supervise_execution}; use intercept_supervisor::wrapper::{CONFIG_FILENAME, WrapperConfig, WrapperConfigReader}; /// Implementation of the wrapper process. /// /// The process exit code is the same as the executed process exit code. /// Besides the functionality described in the module documentation, the /// wrapper process logs the execution and the relevant steps leading to /// the execution. fn main() -> Result<()> { intercept::logging::init("wrapper"); // Capture the current process execution details let execution = Execution::capture().with_context(|| "Failed to capture the execution")?; // Load configuration let config = load_config(&execution.executable)?; // Find the real executable using config let real_executable = find_from_config(&config, &execution.executable)?; let real_execution = execution.with_executable(&real_executable); // Reporting failures shall not fail this process. Therefore, errors will be logged // but not propagated. The process will continue to execute the real executable. if let Err(err) = report(&config, &real_execution) { log::error!("Failed to report the execution: {err}"); } // Execute the real executable with the same arguments let exit_status = supervise_execution(real_execution, GroupPolicy::Inherit)?; // Return the child process status code std::process::exit(exit_status.code().unwrap_or(1)); } /// Report the execution to the remote collector. fn report(config: &WrapperConfig, real_execution: &Execution) -> Result<()> { let reporter = ReporterFactory::create(config.collector_address); reporter.report(real_execution.clone())?; Ok(()) } /// Find the real executable using configuration. fn find_from_config(config: &WrapperConfig, current_exe: &std::path::Path) -> Result { let executable_name = current_exe .file_name() .and_then(|name| name.to_str()) .with_context(|| "Cannot get executable name")?; config .get_executable(executable_name) .cloned() .with_context(|| format!("Executable '{}' not found in configuration", executable_name)) } /// Load JSON configuration file. fn load_config(current_exe: &std::path::Path) -> Result { let wrapper_dir = current_exe.parent().with_context(|| "Cannot get wrapper directory")?; let config_path = wrapper_dir.join(CONFIG_FILENAME); WrapperConfigReader::read_from_file(&config_path) .with_context(|| format!("Cannot read config file: {}", config_path.display())) } rizsotto-Bear-0c30b27/crates/bear/000077500000000000000000000000001527177720300170055ustar00rootroot00000000000000rizsotto-Bear-0c30b27/crates/bear/CLAUDE.md000066400000000000000000000024511527177720300202660ustar00rootroot00000000000000## Bear crate ## Key directories | Directory | Responsibility | |---|---| | `src/modes/` | Modes of operation | | `src/environment.rs` | Config-to-primitive adapter over `intercept_supervisor::runner` | | `src/output/` | Output generation (JSON compilation database, statistics) | | `src/config/` | Configuration loading, validation, types | ## Before modifying - **CLI arguments** (`src/args.rs`): uses `clap` derive macros. Update the man page; `man/CLAUDE.md` says how. Regenerate `site/src/reference/command-line.md` with `scripts/generate-command-line-reference.py`. - **Configuration types** (`src/config/types.rs`): changes here affect YAML config parsing. Update validation in `src/config/validation.rs`. ## The config-to-semantic boundary (load-bearing) `config` is a pure schema layer: it depends on neither `semantic` nor on codegen output, and it stores a configured `as:` spelling as the string the user wrote. `semantic` owns the resolution of that spelling onto a compiler identity. This crate is the single place that sees both sides, and `Mode::configure` is the single conversion point (startup resolution: `docs/rationale/compiler-family-definition.md`). Do not resolve a spelling in `config`, and do not hand a config type to `semantic`. Rationale in `docs/rationale/workspace-crate-layout.md`. rizsotto-Bear-0c30b27/crates/bear/Cargo.toml000066400000000000000000000017371527177720300207450ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later [package] name = "bear" description = "Bear is a tool that generates a compilation database for clang tooling." keywords = ["clang", "clang-tooling", "compilation-database"] version.workspace = true authors.workspace = true repository.workspace = true homepage.workspace = true license.workspace = true edition.workspace = true rust-version.workspace = true publish = false [lib] name = "bear" path = "src/lib.rs" [dependencies] intercept = { path = "../intercept" } intercept-supervisor = { path = "../intercept-supervisor" } semantic = { path = "../semantic" } thiserror.workspace = true serde.workspace = true serde_json.workspace = true serde-saphyr.workspace = true clap.workspace = true log.workspace = true shell-words.workspace = true libc.workspace = true glob.workspace = true [dev-dependencies] semantic = { path = "../semantic", features = ["testing"] } mockall.workspace = true tempfile.workspace = true [lints] workspace = true rizsotto-Bear-0c30b27/crates/bear/src/000077500000000000000000000000001527177720300175745ustar00rootroot00000000000000rizsotto-Bear-0c30b27/crates/bear/src/args.rs000066400000000000000000000625671527177720300211160ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! This module contains the command line interface of the application. //! //! The command line parsing is implemented using the `clap` library. //! The module defines types to represent a structured form of program invocation. //! The `Arguments` type is used to represent all possible invocations of the program. use clap::{ArgAction, ArgMatches, Command, arg, command}; use std::fmt; /// Common constants used in the module. const MODE_INTERCEPT_SUBCOMMAND: &str = "intercept"; const MODE_SEMANTIC_SUBCOMMAND: &str = "semantic"; const MODE_PARSE_SH_SUBCOMMAND: &str = "parse-sh"; const DEFAULT_OUTPUT_FILE: &str = "compile_commands.json"; /// Default for `parse-sh`'s and `semantic`'s `--input`: the `-` sentinel, /// meaning standard input, so these non-executing modes read a pipe with no /// flags. Their output defaults to the compilation database name instead /// (`-` names standard output explicitly). `intercept` has no event-file /// default at all: it cannot fall back to standard output (the build owns /// that stream), so its destination must be named explicitly (see /// `docs/rationale/event-file-defaults.md`). const DEFAULT_STDIO: &str = "-"; /// Returns true when `path` is the [`DEFAULT_STDIO`] sentinel meaning /// standard input or standard output, depending on context. pub(crate) fn is_stdio(path: &std::path::Path) -> bool { path.as_os_str() == DEFAULT_STDIO } /// Represents the command line arguments of the application. #[derive(Debug, PartialEq)] pub struct Arguments { /// The path of the configuration file. pub config: Option, /// The mode of the application. pub mode: Mode, } /// Represents the mode of the application. #[derive(Debug, PartialEq)] pub enum Mode { Intercept { input: BuildCommand, output: BuildEvents }, Semantic { input: BuildEvents, output: BuildSemantic }, Combined { input: BuildCommand, output: BuildSemantic }, ParseSh { input: ShScript, output: BuildSemantic }, PrintCompilers, } /// Represents the execution of a command. #[derive(Debug, PartialEq)] pub struct BuildCommand { /// The command arguments to execute. (This is a non-empty vector of strings.) pub arguments: Vec, } /// Represents the semantic output configuration. #[derive(Debug, PartialEq)] pub struct BuildSemantic { /// The output file path. pub path: std::path::PathBuf, /// Whether to append to an existing file. pub append: bool, } /// Represents the build events configuration. #[derive(Debug, PartialEq)] pub struct BuildEvents { /// The path to the events file. pub path: std::path::PathBuf, } /// Represents the shell text input for the `parse-sh` mode. #[derive(Debug, PartialEq)] pub struct ShScript { /// The path of the shell text to parse (`-` reads from standard input). pub path: std::path::PathBuf, /// The initial working directory for the parsed commands. When unset, the /// process's current directory is used. pub directory: Option, } impl fmt::Display for Arguments { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "Arguments:")?; match &self.config { Some(config) => writeln!(f, "Configuration File: {}", config)?, None => writeln!(f, "Configuration File: ")?, } write!(f, "Mode: {}", self.mode) } } impl fmt::Display for Mode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Mode::Intercept { input, output } => { writeln!(f, "Intercept")?; writeln!(f, " Input: {}", input)?; write!(f, " Output: {}", output) } Mode::Semantic { input, output } => { writeln!(f, "Semantic Analysis")?; writeln!(f, " Input: {}", input)?; write!(f, " Output: {}", output) } Mode::Combined { input, output } => { writeln!(f, "Combined (Intercept + Semantic Analysis)")?; writeln!(f, " Input: {}", input)?; write!(f, " Output: {}", output) } Mode::ParseSh { input, output } => { writeln!(f, "Parse Shell Text")?; writeln!(f, " Input: {}", input)?; write!(f, " Output: {}", output) } Mode::PrintCompilers => write!(f, "Print Compilers"), } } } impl fmt::Display for BuildCommand { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Build Command: {}", self.arguments.join(" ")) } } impl fmt::Display for BuildSemantic { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Semantic Output: {} (append: {})", self.path.display(), self.append) } } impl fmt::Display for BuildEvents { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Events Output: {}", self.path.display()) } } impl fmt::Display for ShScript { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.directory { Some(dir) => write!(f, "Shell Script: {} (directory: {})", self.path.display(), dir.display()), None => write!(f, "Shell Script: {} (directory: )", self.path.display()), } } } impl TryFrom for Arguments { type Error = ParseError; fn try_from(matches: ArgMatches) -> Result { let config = matches.get_one::("config").map(String::to_string); let mode = Mode::try_from(matches)?; // `--print-compilers` only lists the compilers Bear recognizes from // the static, built-in tables and consults no config, so accepting // `--config` would silently ignore it. Reject it instead. if config.is_some() && matches!(mode, Mode::PrintCompilers) { return Err(ParseError::ConfigNotApplicableToPrintCompilers); } Ok(Arguments { config, mode }) } } impl TryFrom for Mode { type Error = ParseError; fn try_from(matches: ArgMatches) -> Result { match matches.subcommand() { Some((MODE_INTERCEPT_SUBCOMMAND, intercept_matches)) => { let input = BuildCommand::try_from(intercept_matches)?; let path = intercept_matches .get_one::("output") .map(std::path::PathBuf::from) .expect("output is required"); Ok(Mode::Intercept { input, output: BuildEvents { path } }) } Some((MODE_SEMANTIC_SUBCOMMAND, semantic_matches)) => { if semantic_matches.get_flag("print-compilers") { return Ok(Mode::PrintCompilers); } let path = semantic_matches .get_one::("input") .map(std::path::PathBuf::from) .expect("input is defaulted"); let output = BuildSemantic::try_from(semantic_matches)?; Ok(Mode::Semantic { input: BuildEvents { path }, output }) } Some((MODE_PARSE_SH_SUBCOMMAND, parse_sh_matches)) => { let input = parse_sh_matches .get_one::("input") .map(std::path::PathBuf::from) .expect("input is defaulted"); let directory = parse_sh_matches.get_one::("directory").map(std::path::PathBuf::from); let output = BuildSemantic::try_from(parse_sh_matches)?; Ok(Mode::ParseSh { input: ShScript { path: input, directory }, output }) } None => { let input = BuildCommand::try_from(&matches)?; let output = BuildSemantic::try_from(&matches)?; Ok(Mode::Combined { input, output }) } _ => Err(ParseError::UnrecognizedSubcommand), } } } impl TryFrom<&ArgMatches> for BuildCommand { type Error = ParseError; fn try_from(matches: &ArgMatches) -> Result { let arguments: Vec<_> = matches.get_many("BUILD_COMMAND").ok_or(ParseError::MissingBuildCommand)?.cloned().collect(); // The arguments must not be empty, and that is enforced by the CLI definition. Ok(BuildCommand { arguments }) } } impl TryFrom<&ArgMatches> for BuildSemantic { type Error = ParseError; fn try_from(matches: &ArgMatches) -> Result { let path = matches.get_one::("output").map(std::path::PathBuf::from).expect("output is defaulted"); // `--overwrite` is deliberately not read here. It names the behaviour // that is already the default, so today it only has to be accepted and // to conflict with `--append`. It becomes load-bearing when appending // turns into the default. let append = *matches.get_one::("append").unwrap_or(&false); Ok(BuildSemantic { path, append }) } } #[derive(Debug, thiserror::Error)] pub enum ParseError { #[error("Unrecognized subcommand")] UnrecognizedSubcommand, #[error("Missing build command")] MissingBuildCommand, #[error( "The --config option does not apply to semantic --print-compilers: it only lists the \ compilers Bear recognizes from its built-in tables and consults no config" )] ConfigNotApplicableToPrintCompilers, } /// Represents the command line interface of the application. /// /// This describes how the user can interact with the application. /// The different modes of the application are represented as subcommands. /// The application can be run in intercept mode, semantic mode, parse-sh /// mode, or the default mode where both intercept and semantic are executed. pub fn cli() -> Command { // The binary is `bear-driver` but users invoke it as `bear` via a // shell wrapper, so we hardcode the user-facing name instead of // letting `command!()` pick up CARGO_BIN_NAME. `bin_name` has to be // set as well: `name` labels the command, while usage and error // messages are rendered from `bin_name`, which clap otherwise takes // from argv[0] and would print as `bear-driver`. command!() .name("bear") .bin_name("bear") .subcommand_required(false) .subcommand_negates_reqs(true) .subcommand_precedence_over_arg(true) .arg_required_else_help(true) .args(&[arg!(-c --config "Path of the config file")]) .subcommand( Command::new(MODE_INTERCEPT_SUBCOMMAND) .about("intercepts command execution") .args(&[ arg!( "Build command") .action(ArgAction::Append) .value_terminator("--") .num_args(1..) .last(true) .required(true), arg!(-o --output "Path of the event file to write").required(true), ]) .arg_required_else_help(true), ) .subcommand( Command::new(MODE_SEMANTIC_SUBCOMMAND) .about("detect semantics of command executions") .args(&[ arg!(-i --input "Path of the event file to read") .default_value(DEFAULT_STDIO) .hide_default_value(false), arg!(-o --output "Path of the result file") .default_value(DEFAULT_OUTPUT_FILE) .hide_default_value(false), arg!(-a --append "Append result to an existing output file").action(ArgAction::SetTrue), arg!(--overwrite "Overwrite an existing output file (the default)") .action(ArgAction::SetTrue) .conflicts_with("append"), arg!(--"print-compilers" "Print the compilers Bear recognizes and exit") .action(ArgAction::SetTrue), ]) .arg_required_else_help(false) .after_help( "Reads the event stream that `bear intercept` (or a third-party \ producer) writes and produces the compilation database, e.g.:\n\n \ bear semantic --input events.json", ), ) .subcommand( Command::new(MODE_PARSE_SH_SUBCOMMAND) .about("produces the compilation database from build-system dry-run text (e.g. `make -n` output)") .args(&[ arg!(-i --input "Path of the shell text to parse") .default_value(DEFAULT_STDIO) .hide_default_value(false), arg!(-o --output "Path of the result file") .default_value(DEFAULT_OUTPUT_FILE) .hide_default_value(false), arg!(-a --append "Append result to an existing output file").action(ArgAction::SetTrue), arg!(--overwrite "Overwrite an existing output file (the default)") .action(ArgAction::SetTrue) .conflicts_with("append"), arg!(-C --directory "Initial working directory for the parsed commands"), ]) .arg_required_else_help(false) .after_help( "Parses the text a build system prints in dry-run mode and writes the \ compilation database, without running a build, e.g.:\n\n \ make -n | bear parse-sh", ), ) .args(&[ arg!( "Build command") .action(ArgAction::Append) .value_terminator("--") .num_args(1..) .last(true) .required(true), arg!(-o --output "Path of the result file") .default_value(DEFAULT_OUTPUT_FILE) .hide_default_value(false), arg!(-a --append "Append result to an existing output file").action(ArgAction::SetTrue), arg!(--overwrite "Overwrite an existing output file (the default)") .action(ArgAction::SetTrue) .conflicts_with("append"), ]) } #[cfg(test)] mod test { use super::*; #[test] fn test_intercept_call() { let execution = vec!["bear", "-c", "~/bear.yaml", "intercept", "-o", "custom.json", "--", "make", "all"]; let matches = cli().get_matches_from(execution); let arguments = Arguments::try_from(matches).unwrap(); assert_eq!( arguments, Arguments { config: Some("~/bear.yaml".into()), mode: Mode::Intercept { input: BuildCommand { arguments: vec!["make", "all"].into_iter().map(String::from).collect() }, output: BuildEvents { path: "custom.json".into() }, }, } ); } // Requirements: interception-events-format // // Interception has no default event-file name: it cannot fall back to // standard output (the build owns that stream), so the destination // must be named and omitting it is a usage error. #[test] fn test_intercept_requires_output() { let execution = vec!["bear", "intercept", "--", "make", "all"]; let sut = cli().try_get_matches_from(execution); let error = sut.expect_err("intercept without --output must be a usage error"); assert_eq!(error.kind(), clap::error::ErrorKind::MissingRequiredArgument); } #[test] fn test_semantic_call() { let execution = vec!["bear", "-c", "~/bear.yaml", "semantic", "-i", "custom.json", "-o", "result.json", "-a"]; let matches = cli().get_matches_from(execution); let arguments = Arguments::try_from(matches).unwrap(); assert_eq!( arguments, Arguments { config: Some("~/bear.yaml".into()), mode: Mode::Semantic { input: BuildEvents { path: "custom.json".into() }, output: BuildSemantic { path: "result.json".into(), append: true }, }, } ); } // Requirements: interception-events-format // // `bear semantic` is a filter: with no input named it reads the event // stream from standard input, so a producer pipes into it flag-free. #[test] fn test_semantic_defaults() { let execution = vec!["bear", "semantic"]; let matches = cli().get_matches_from(execution); let arguments = Arguments::try_from(matches).unwrap(); assert_eq!( arguments, Arguments { config: None, mode: Mode::Semantic { input: BuildEvents { path: "-".into() }, output: BuildSemantic { path: "compile_commands.json".into(), append: false }, }, } ); } // Requirements: interception-events-format #[test] fn test_semantic_input_stdin() { let execution = vec!["bear", "semantic", "-i", "-"]; let matches = cli().get_matches_from(execution); let arguments = Arguments::try_from(matches).unwrap(); assert_eq!( arguments, Arguments { config: None, mode: Mode::Semantic { input: BuildEvents { path: "-".into() }, output: BuildSemantic { path: "compile_commands.json".into(), append: false }, }, } ); } // Requirements: interception-events-format // // `-` parses successfully here: the CLI layer treats it as an ordinary // path value. The rejection (a mode that runs the build must not accept // `-` for output) happens at `Mode::configure` time, not at parse time, // and is covered by the integration tests for that requirement. #[test] fn test_intercept_output_dash_parses() { let execution = vec!["bear", "intercept", "-o", "-", "--", "make", "all"]; let matches = cli().get_matches_from(execution); let arguments = Arguments::try_from(matches).unwrap(); assert_eq!( arguments, Arguments { config: None, mode: Mode::Intercept { input: BuildCommand { arguments: vec!["make", "all"].into_iter().map(String::from).collect() }, output: BuildEvents { path: "-".into() }, }, } ); } // Requirements: interception-shell-text-parsing // // `parse-sh` reads shell text from standard input and writes the // compilation database under its ecosystem-contracted name, so the // headline usage is flag-free: `make -n | bear parse-sh`. #[test] fn test_parse_sh_defaults() { let execution = vec!["bear", "parse-sh"]; let matches = cli().get_matches_from(execution); let arguments = Arguments::try_from(matches).unwrap(); assert_eq!( arguments, Arguments { config: None, mode: Mode::ParseSh { input: ShScript { path: "-".into(), directory: None }, output: BuildSemantic { path: "compile_commands.json".into(), append: false }, }, } ); } // Requirements: interception-shell-text-parsing #[test] fn test_parse_sh_call() { let execution = vec!["bear", "parse-sh", "-i", "in.sh", "-o", "db.json", "-a", "-C", "/build"]; let matches = cli().get_matches_from(execution); let arguments = Arguments::try_from(matches).unwrap(); assert_eq!( arguments, Arguments { config: None, mode: Mode::ParseSh { input: ShScript { path: "in.sh".into(), directory: Some("/build".into()) }, output: BuildSemantic { path: "db.json".into(), append: true }, }, } ); } // Requirements: interception-shell-text-parsing // // Configuration shapes the semantic analysis parse-sh runs, exactly as // in the other database-producing modes, so `--config` is accepted. #[test] fn test_parse_sh_accepts_config() { let execution = vec!["bear", "-c", "~/bear.yaml", "parse-sh"]; let matches = cli().get_matches_from(execution); let arguments = Arguments::try_from(matches).unwrap(); assert_eq!( arguments, Arguments { config: Some("~/bear.yaml".into()), mode: Mode::ParseSh { input: ShScript { path: "-".into(), directory: None }, output: BuildSemantic { path: "compile_commands.json".into(), append: false }, }, } ); } // Requirements: recognition-compiler-names #[test] fn test_semantic_print_compilers() { let execution = vec!["bear", "semantic", "--print-compilers"]; let matches = cli().get_matches_from(execution); let arguments = Arguments::try_from(matches).unwrap(); assert_eq!(arguments, Arguments { config: None, mode: Mode::PrintCompilers }); } // Requirements: recognition-compiler-names #[test] fn test_semantic_print_compilers_rejects_config() { // arrange let execution = vec!["bear", "-c", "~/bear.yaml", "semantic", "--print-compilers"]; // act let matches = cli().get_matches_from(execution); let sut = Arguments::try_from(matches); // assert assert!(matches!(sut, Err(ParseError::ConfigNotApplicableToPrintCompilers))); } #[test] fn test_all_call() { let execution = vec!["bear", "-c", "~/bear.yaml", "-o", "result.json", "-a", "--", "make", "all"]; let matches = cli().get_matches_from(execution); let arguments = Arguments::try_from(matches).unwrap(); assert_eq!( arguments, Arguments { config: Some("~/bear.yaml".to_string()), mode: Mode::Combined { input: BuildCommand { arguments: vec!["make", "all"].into_iter().map(String::from).collect() }, output: BuildSemantic { path: "result.json".into(), append: true }, }, } ); } // Requirements: output-append // // `--overwrite` names the behaviour that is already the default, so it // parses everywhere `--append` does and leaves the outcome untouched. #[test] fn test_overwrite_is_accepted_in_every_output_mode() { let executions = vec![ vec!["bear", "--overwrite", "--", "make", "all"], vec!["bear", "semantic", "--overwrite"], vec!["bear", "parse-sh", "--overwrite"], ]; for execution in executions { let matches = cli().try_get_matches_from(&execution).expect("--overwrite is accepted"); let arguments = Arguments::try_from(matches).unwrap(); let output = match &arguments.mode { Mode::Combined { output, .. } => output, Mode::Semantic { output, .. } => output, Mode::ParseSh { output, .. } => output, other => panic!("unexpected mode {:?} for {:?}", other, execution), }; assert_eq!(output, &BuildSemantic { path: "compile_commands.json".into(), append: false }); } } // Requirements: output-append // // Asking to accumulate and to overwrite in one invocation is a script // bug, and it is worth reporting now rather than after the default flips. #[test] fn test_overwrite_conflicts_with_append() { // Both orders, because a script that gets this wrong may well pass // the flags in either one. let executions = vec![ vec!["bear", "-a", "--overwrite", "--", "make", "all"], vec!["bear", "--overwrite", "-a", "--", "make", "all"], vec!["bear", "semantic", "--append", "--overwrite"], vec!["bear", "semantic", "--overwrite", "--append"], vec!["bear", "parse-sh", "--append", "--overwrite"], vec!["bear", "parse-sh", "--overwrite", "--append"], ]; for execution in executions { let result = cli().try_get_matches_from(&execution); let error = result.expect_err(&format!("case {execution:?} must be rejected")); assert_eq!(error.kind(), clap::error::ErrorKind::ArgumentConflict, "case {execution:?}"); } } #[test] fn test_all_defaults() { let execution = vec!["bear", "--", "make", "all"]; let matches = cli().get_matches_from(execution); let arguments = Arguments::try_from(matches).unwrap(); assert_eq!( arguments, Arguments { config: None, mode: Mode::Combined { input: BuildCommand { arguments: vec!["make", "all"].into_iter().map(String::from).collect(), }, output: BuildSemantic { path: "compile_commands.json".into(), append: false }, }, } ); } } rizsotto-Bear-0c30b27/crates/bear/src/config/000077500000000000000000000000001527177720300210415ustar00rootroot00000000000000rizsotto-Bear-0c30b27/crates/bear/src/config/loader.rs000066400000000000000000000425661527177720300226720ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later use super::Main; use super::validation::Validator; use log::{debug, info}; use std::fs::OpenOptions; use std::path::{Path, PathBuf}; use thiserror::Error; pub struct Loader {} impl Loader { /// Loads the configuration from the specified file or the default locations. /// /// If the configuration file is specified, it will be used. Otherwise, the default locations /// will be searched for the configuration file. If the configuration file is not found, the /// default configuration will be returned. #[allow(clippy::result_large_err, reason = "config loading runs once at startup")] pub fn load( context: &intercept_supervisor::context::Context, filename: &Option, ) -> Result { if let Some(path) = filename { // If the configuration file is specified, use it. Self::from_file(Path::new(path)) } else { // Otherwise, try to find the configuration file in the default locations. let locations = Self::file_locations(context); for location in locations { debug!("Checking configuration file: {}", location.display()); if location.exists() { return Self::from_file(location.as_path()); } } // If the configuration file is not found, return the default configuration. debug!("Configuration file not found. Using the default configuration."); Ok(Main::default()) } } /// The default locations searched for the configuration file. /// /// The search order is: /// - the current working directory /// - on Unix: /// - `$XDG_CONFIG_HOME` if set, else `$HOME/.config` /// - `/Bear/` /// - on Windows: /// - `%LOCALAPPDATA%` /// - `%LOCALAPPDATA%\Bear` /// - `%APPDATA%` /// - `%APPDATA%\Bear` /// /// Each directory is probed for a `bear.yml` file. The first existing file /// is used; remaining locations are not consulted. fn file_locations(context: &intercept_supervisor::context::Context) -> Vec { let mut locations: Vec = Vec::new(); locations.push(context.current_directory.clone().join("bear.yml")); #[cfg(unix)] { let base = std::env::var_os("XDG_CONFIG_HOME") .filter(|v| !v.is_empty()) .map(PathBuf::from) .or_else(|| { std::env::var_os("HOME") .filter(|v| !v.is_empty()) .map(|home| PathBuf::from(home).join(".config")) }); if let Some(dir) = base { locations.push(dir.join("bear.yml")); locations.push(dir.join("Bear").join("bear.yml")); } } #[cfg(windows)] { for var in &["LOCALAPPDATA", "APPDATA"] { if let Some(value) = std::env::var_os(var).filter(|v| !v.is_empty()) { let dir = PathBuf::from(value); locations.push(dir.join("bear.yml")); locations.push(dir.join("Bear").join("bear.yml")); } } } // Drop adjacent duplicates (e.g. when $XDG_CONFIG_HOME == $HOME/.config). locations.dedup(); locations } /// Loads the configuration from the specified file. #[allow(clippy::result_large_err, reason = "config loading runs once at startup")] pub fn from_file(path: &Path) -> Result { info!("Loading configuration file: {}", path.display()); let reader = OpenOptions::new() .read(true) .open(path) .map_err(|source| ConfigError::FileAccess { path: path.to_path_buf(), source })?; let content: Main = Self::from_reader(reader) .map_err(|source| ConfigError::ParseError { path: path.to_path_buf(), source })?; // Validate the loaded configuration Main::validate(&content) .map_err(|source| ConfigError::ValidationError { path: path.to_path_buf(), source })?; Ok(content) } /// Define the deserialization format of the config file. pub(crate) fn from_reader(rdr: R) -> Result where R: std::io::Read, T: serde::de::DeserializeOwned, { serde_saphyr::from_reader(rdr) } } /// Renders a parse error without the source snippet, which would spread one /// diagnostic over several lines. fn one_line(error: &serde_saphyr::Error) -> String { let mut options = serde_saphyr::RenderOptions::default(); options.snippets = serde_saphyr::SnippetMode::Off; error.render_with_options(options) } /// Represents all possible configuration-related errors. #[derive(Debug, Error)] pub enum ConfigError { /// Error when opening or reading a configuration file. #[error("cannot read {path}: {source}", path = path.display())] FileAccess { path: PathBuf, #[source] source: std::io::Error, }, /// Error when parsing the configuration file format. #[error("cannot parse {path}: {}", one_line(source), path = path.display())] ParseError { path: PathBuf, #[source] source: serde_saphyr::Error, }, /// Error when the schema version is not supported. #[error("Unsupported schema version: {found}. Expected: {expected}")] UnsupportedSchema { found: String, expected: String }, /// Error when configuration validation fails. #[error("invalid configuration in {path}: {source}", path = path.display())] ValidationError { path: PathBuf, #[source] source: crate::config::validation::ValidationError, }, } #[cfg(test)] mod test { use super::super::*; use super::*; use std::fs; // Requirements: output-generated-file-filter #[test] fn test_wrapper_config() { let content: &[u8] = br#" schema: 4.2 intercept: mode: wrapper compilers: - path: /usr/local/bin/cc as: gcc - path: /usr/bin/cc ignore: true - path: /usr/bin/clang++ flags: add: ["-I/opt/MPI/include"] remove: ["-Wall"] sources: directories: - path: "/opt/project/sources" action: include - path: "/opt/project/tests" action: exclude files: - pattern: "moc_*.cpp" action: exclude duplicates: match_on: [file, directory] format: paths: directory: canonical file: canonical entries: use_array_format: true include_output_field: true "#; let result = Loader::from_reader(content).unwrap(); let expected = Main { schema: String::from("4.2"), intercept: Intercept::Wrapper, compilers: vec![ Compiler { path: PathBuf::from("/usr/local/bin/cc"), as_: Some(String::from("gcc")), ignore: false, }, Compiler { path: PathBuf::from("/usr/bin/cc"), as_: None, ignore: true }, Compiler { path: PathBuf::from("/usr/bin/clang++"), as_: None, ignore: false }, ], sources: SourceFilter { directories: vec![ DirectoryRule { path: PathBuf::from("/opt/project/sources"), action: DirectoryAction::Include, }, DirectoryRule { path: PathBuf::from("/opt/project/tests"), action: DirectoryAction::Exclude, }, ], files: vec![FileRule { pattern: String::from("moc_*.cpp"), action: DirectoryAction::Exclude, }], }, duplicates: DuplicateFilter { match_on: vec![OutputFields::File, OutputFields::Directory] }, format: Format { paths: PathFormat { directory: PathResolver::Canonical, file: PathResolver::Canonical }, entries: EntryFormat { use_array_format: true, include_output_field: true }, arguments: ArgumentsFormat::default(), }, headers: Headers::default(), }; assert_eq!(expected, result); } // Requirements: output-duplicate-detection #[test] fn test_incomplete_wrapper_config() { let content: &[u8] = br#" schema: 4.2 intercept: mode: wrapper format: paths: directory: as-is file: as-is "#; let result = Loader::from_reader(content).unwrap(); let expected = Main { schema: String::from("4.2"), intercept: Intercept::Wrapper, compilers: vec![], sources: SourceFilter { directories: vec![], files: vec![] }, duplicates: DuplicateFilter { match_on: vec![OutputFields::Directory, OutputFields::File] }, format: Format { paths: PathFormat { directory: PathResolver::AsIs, file: PathResolver::AsIs }, entries: EntryFormat { use_array_format: true, include_output_field: true }, arguments: ArgumentsFormat::default(), }, headers: Headers::default(), }; assert_eq!(expected, result); } // Requirements: output-duplicate-detection #[test] fn test_incomplete_preload_config() { let content: &[u8] = br#" schema: 4.2 intercept: mode: preload format: paths: directory: absolute file: absolute "#; let result = Loader::from_reader(content).unwrap(); let expected = Main { schema: String::from("4.2"), intercept: Intercept::Preload, compilers: vec![], sources: SourceFilter { directories: vec![], files: vec![] }, duplicates: DuplicateFilter { match_on: vec![OutputFields::Directory, OutputFields::File] }, format: Format { paths: PathFormat { directory: PathResolver::Absolute, file: PathResolver::Absolute }, entries: EntryFormat { use_array_format: true, include_output_field: true }, arguments: ArgumentsFormat::default(), }, headers: Headers::default(), }; assert_eq!(expected, result); } // Requirements: output-header-entries #[test] fn test_header_entries_config_round_trips_through_loader() { let content: &[u8] = br#" schema: 4.2 intercept: mode: wrapper headers: enabled: true strategy: dependency-files "#; let result = Loader::from_reader(content).unwrap(); let expected = Main { schema: String::from("4.2"), intercept: Intercept::Wrapper, compilers: vec![], sources: SourceFilter::default(), duplicates: DuplicateFilter::default(), format: Format::default(), headers: Headers { enabled: true, strategy: HeaderStrategy::DependencyFiles }, }; assert_eq!(expected, result); } #[test] fn test_default_config() { let result = Main::default(); let expected = Main { schema: String::from("4.2"), intercept: Intercept::default(), compilers: vec![], sources: SourceFilter::default(), duplicates: DuplicateFilter::default(), format: Format::default(), headers: Headers::default(), }; assert_eq!(expected, result); } // Requirements: output-duplicate-detection #[test] fn test_default_duplicate_filter_matches_directory_and_file_only() { let sut = DuplicateFilter::default(); assert_eq!(sut.match_on, vec![OutputFields::Directory, OutputFields::File]); } #[test] fn test_invalid_schema_version() { let content: &[u8] = br#" schema: 3.0 intercept: mode: wrapper directory: /tmp "#; let result: Result = Loader::from_reader(content); assert!(result.is_err()); let message = result.unwrap_err().to_string(); assert!( message.contains("Unsupported schema version: 3.0. Expected: 4.2"), "unexpected error message: {message}" ); } #[test] fn test_failing_config() { let content: &[u8] = br#"{ "output": { "format": { "command_as_array": false }, "content": { "duplicates": "files" } } }"#; let result: Result = Loader::from_reader(content); assert!(result.is_err()); } #[test] fn test_validation_error_on_invalid_config() { use tempfile; let temp_dir = tempfile::tempdir().unwrap(); let config_file = temp_dir.path().join("bear.yml"); let invalid_config = r#" schema: "4.2" intercept: mode: wrapper path: /nonexistent/wrapper/path directory: /nonexistent/directory compilers: - path: /nonexistent/compiler as: "invalid_compiler_type" "#; fs::write(&config_file, invalid_config).unwrap(); // The `as:` spelling is not the schema layer's business: this config // fails because the compiler path does not exist. The unknown // spelling is caught later, in `Mode::configure` (see // `configure_rejects_an_unknown_compiler_as_spelling`). let result = Loader::from_file(&config_file); assert!(result.is_err()); match result.unwrap_err() { ConfigError::ValidationError { source, .. } => { let error_msg = source.to_string(); assert!(error_msg.contains("/nonexistent/compiler"), "message: {error_msg}"); } other => panic!("Expected ValidationError for the missing compiler path, got: {:?}", other), } } #[test] fn test_compiler_config_with_type_hints() { let temp_dir = tempfile::tempdir().unwrap(); let config_file = temp_dir.path().join("bear.yml"); // Create temporary compiler files for validation let gcc_wrapper = temp_dir.path().join("custom-gcc-wrapper"); let clang_wrapper = temp_dir.path().join("custom-clang"); let fortran_wrapper = temp_dir.path().join("my-fortran"); let intel_wrapper = temp_dir.path().join("ifort-wrapper"); let cray_wrapper = temp_dir.path().join("ftn-wrapper"); fs::write(&gcc_wrapper, "#!/bin/sh\necho gcc wrapper").unwrap(); fs::write(&clang_wrapper, "#!/bin/sh\necho clang wrapper").unwrap(); fs::write(&fortran_wrapper, "#!/bin/sh\necho fortran wrapper").unwrap(); fs::write(&intel_wrapper, "#!/bin/sh\necho intel wrapper").unwrap(); fs::write(&cray_wrapper, "#!/bin/sh\necho cray wrapper").unwrap(); // Create wrapper executable and directory for validation let wrapper_dir = temp_dir.path().join("wrapper"); std::fs::create_dir(&wrapper_dir).unwrap(); let wrapper_exe = wrapper_dir.join("wrapper"); fs::write(&wrapper_exe, "#!/bin/sh\necho wrapper").unwrap(); let config_with_hints = format!( r#" schema: "4.2" intercept: mode: wrapper path: {} directory: {} compilers: - path: {} as: "gcc" - path: {} as: "clang" - path: {} as: "flang" - path: {} as: "intel_fortran" - path: {} as: "cray_fortran" "#, wrapper_exe.display(), wrapper_dir.display(), gcc_wrapper.display(), clang_wrapper.display(), fortran_wrapper.display(), intel_wrapper.display(), cray_wrapper.display() ); fs::write(&config_file, config_with_hints).unwrap(); let result = Loader::from_file(&config_file); assert!(result.is_ok()); let config = result.unwrap(); assert_eq!(config.compilers.len(), 5); // Verify compiler type hints are kept verbatim assert_eq!(config.compilers[0].as_.as_deref(), Some("gcc")); assert_eq!(config.compilers[1].as_.as_deref(), Some("clang")); assert_eq!(config.compilers[2].as_.as_deref(), Some("flang")); assert_eq!(config.compilers[3].as_.as_deref(), Some("intel_fortran")); assert_eq!(config.compilers[4].as_.as_deref(), Some("cray_fortran")); } } rizsotto-Bear-0c30b27/crates/bear/src/config/mod.rs000066400000000000000000000034431527177720300221720ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! This module defines the configuration of the application. //! //! The configuration is either loaded from a file or used with default //! values, which are defined in the code. The configuration exposes the main //! logical steps that the application will follow. //! //! The configuration file syntax is based on the YAML format. //! The default configuration file name is `bear.yml`. //! //! The configuration file location is searched in the following order: //! 1. The current working directory //! 2. The local configuration directory of the user //! 3. The configuration directory of the user //! 4. The local configuration directory of the application //! 5. The configuration directory of the application //! //! ```yaml //! schema: 4.2 //! //! intercept: //! mode: wrapper //! //! compilers: //! - path: /usr/local/bin/cc //! as: gcc //! - path: /usr/bin/cc //! ignore: true //! - path: /usr/bin/clang++ //! //! sources: //! directories: //! - path: "/opt/project/sources" //! action: include //! - path: "/opt/project/tests" //! action: exclude //! files: //! - pattern: "moc_*.cpp" //! action: exclude //! //! duplicates: //! match_on: [file, directory] //! //! format: //! paths: //! directory: canonical //! file: canonical //! entries: //! use_array_format: true //! include_output_field: true //! ``` //! //! ```yaml //! schema: 4.2 //! //! intercept: //! mode: preload //! //! format: //! paths: //! directory: as-is //! file: as-is //! entries: //! use_array_format: true //! include_output_field: true //! ``` // Re-Export the types and the loader module content. pub use loader::{ConfigError, Loader}; pub use types::*; pub mod loader; mod types; mod validation; rizsotto-Bear-0c30b27/crates/bear/src/config/types.rs000066400000000000000000000273251527177720300225640ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later use serde::Deserialize; use std::fmt; use std::path::PathBuf; /// Represents the application configuration with flattened structure. #[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize)] pub struct Main { #[serde(deserialize_with = "validate_schema_version")] pub schema: String, #[serde(default)] pub intercept: Intercept, #[serde(default)] pub compilers: Vec, #[serde(default)] pub sources: SourceFilter, #[serde(default)] pub duplicates: DuplicateFilter, #[serde(default)] pub format: Format, #[serde(default)] pub headers: Headers, } impl Default for Main { fn default() -> Self { Self { schema: String::from(SUPPORTED_SCHEMA_VERSION), intercept: Intercept::default(), compilers: vec![], sources: SourceFilter::default(), duplicates: DuplicateFilter::default(), format: Format::default(), headers: Headers::default(), } } } impl fmt::Display for Main { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "Configuration:")?; match serde_saphyr::to_string(self) { Ok(yaml_string) => { for line in yaml_string.lines() { writeln!(f, "{}", line)?; } Ok(()) } Err(_) => Err(fmt::Error), } } } /// Simplified intercept configuration with mode. #[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(tag = "mode")] pub enum Intercept { #[serde(rename = "wrapper")] Wrapper, #[serde(rename = "preload")] Preload, } /// The default intercept mode is varying based on the target operating system. impl Default for Intercept { #[cfg(any(target_os = "macos", target_os = "ios", target_os = "windows"))] fn default() -> Self { Intercept::Wrapper } #[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "windows")))] fn default() -> Self { Intercept::Preload } } /// Represents compiler configuration matching the YAML format. /// /// `as_` holds the spelling the user wrote, verbatim. Resolving it onto a /// compiler family is semantic analysis, not schema validation, so it happens /// in the wiring layer (`modes`) against the semantic hint builder. #[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize)] pub struct Compiler { pub path: PathBuf, #[serde(rename = "as", skip_serializing_if = "Option::is_none")] pub as_: Option, #[serde(default)] pub ignore: bool, } /// Action to take for files matching a directory rule #[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "lowercase")] pub enum DirectoryAction { Include, Exclude, } /// A rule that specifies how to handle files within a directory #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct DirectoryRule { pub path: PathBuf, pub action: DirectoryAction, } /// A rule that specifies how to handle files matching a filename glob pattern. #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct FileRule { pub pattern: String, pub action: DirectoryAction, } /// Source filter configuration for controlling which files are included in the compilation database. /// /// Uses directory-based rules and filename-pattern rules with order-based evaluation semantics: /// /// 1. **Order-based evaluation**: For each source file, the *last* rule whose path prefix /// matches determines inclusion/exclusion. Filename-pattern rules are evaluated the same /// way, independently: the *last* rule whose glob matches determines inclusion/exclusion. /// 2. **Empty directories/files list**: Interpreted as "include everything" (no filtering) for /// that list. /// 3. **No-match behavior**: If no rule matches a file, the file is *included*. /// 4. **Path matching**: Simple prefix matching for directory rules, no normalization. /// 5. **Case sensitivity**: Always case-sensitive on all platforms. /// 6. **Path separators**: Platform-specific (`/` on Unix, `\` on Windows). /// 7. **Symlinks**: No symlink resolution — match literal paths only. /// 8. **Directory matching**: A rule matches both files directly in the directory and files in subdirectories. /// 9. **Empty path/pattern fields**: Invalid — validation must fail. /// 10. **Filename-pattern matching**: A pattern without a path separator matches the source /// file's basename; a pattern containing a separator matches the full source path as it /// appears in the entry. /// 11. **Composition**: An entry is emitted only when both the directory rules and the /// filename-pattern rules accept it (logical AND of the two independent verdicts). /// /// **Important**: For matching to work correctly, rule paths/patterns should use the same /// format as configured in `format.paths.file`. This consistency is the user's responsibility. #[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)] pub struct SourceFilter { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub directories: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub files: Vec, } /// Duplicate filter configuration matching the YAML format. #[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] pub struct DuplicateFilter { pub match_on: Vec, } impl Default for DuplicateFilter { fn default() -> Self { Self { match_on: vec![OutputFields::Directory, OutputFields::File] } } } /// Represent the fields of the JSON compilation database record. #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)] pub enum OutputFields { #[serde(rename = "directory")] Directory, #[serde(rename = "file")] File, #[serde(rename = "arguments")] Arguments, #[serde(rename = "command")] Command, #[serde(rename = "output")] Output, } /// Format configuration matching the YAML format. #[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)] pub struct Format { #[serde(default)] pub paths: PathFormat, #[serde(default)] pub entries: EntryFormat, #[serde(default)] pub arguments: ArgumentsFormat, } /// Controls how the `arguments` field of each entry is assembled during /// semantic analysis. #[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] pub struct ArgumentsFormat { /// Replace `@file` response-file references with their tokenized contents. /// Disabled by default: an `@file` argument is recorded verbatim. #[serde(default)] pub from_response_files: bool, /// Fold compiler environment variables (e.g. `CPATH`) into each entry's /// arguments as explicit flags. Enabled by default. #[serde(default = "default_enabled")] pub from_environment: bool, } impl Default for ArgumentsFormat { fn default() -> Self { Self { from_response_files: false, from_environment: true } } } /// Format configuration of paths in the JSON compilation database. #[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)] pub struct PathFormat { #[serde(default)] pub directory: PathResolver, #[serde(default)] pub file: PathResolver, } /// Path resolver options matching the YAML format. #[derive(Copy, Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)] pub enum PathResolver { /// Leave the path as is without any transformation. (Default) #[default] #[serde(rename = "as-is")] AsIs, /// The path will be resolved to the canonical path. #[serde(rename = "canonical")] Canonical, /// The path will be resolved to the relative path to the directory attribute. #[serde(rename = "relative")] Relative, /// The path will be resolved to an absolute path. #[serde(rename = "absolute")] Absolute, } /// Configuration for formatting output entries matching the YAML format. #[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] pub struct EntryFormat { #[serde(default = "default_enabled")] pub use_array_format: bool, #[serde(default = "default_enabled")] pub include_output_field: bool, } impl Default for EntryFormat { fn default() -> Self { Self { use_array_format: true, include_output_field: true } } } /// Configuration for synthesizing compilation database entries for header files. /// /// Off by default: with `enabled: false` the output is unchanged. When enabled, /// `strategy` selects how header files are discovered and which translation unit /// donates the compile flags. Which file extensions count as headers is fixed /// (a built-in C-family header set), not configurable here. #[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)] pub struct Headers { #[serde(default)] pub enabled: bool, #[serde(default)] pub strategy: HeaderStrategy, } /// Strategy for discovering header files and their donor translation unit. #[derive(Copy, Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "kebab-case")] pub enum HeaderStrategy { /// Clone from a compiled source in the same directory as the header. #[default] Siblings, /// Read the dependency files the build already emitted. DependencyFiles, } pub(crate) const SUPPORTED_SCHEMA_VERSION: &str = "4.2"; fn default_enabled() -> bool { true } // Custom deserialization function to validate the schema version fn validate_schema_version<'de, D>(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let schema: String = Deserialize::deserialize(deserializer)?; if schema != SUPPORTED_SCHEMA_VERSION { use serde::de::Error; Err(Error::custom(format!( "Unsupported schema version: {schema}. Expected: {SUPPORTED_SCHEMA_VERSION}" ))) } else { Ok(schema) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_header_strategy_default_is_siblings() { let sut = HeaderStrategy::default(); assert_eq!(sut, HeaderStrategy::Siblings); } #[test] fn test_headers_default_is_disabled_with_siblings_strategy() { let sut = Headers::default(); assert!(!sut.enabled, "headers should be disabled by default"); assert_eq!(sut.strategy, HeaderStrategy::Siblings); } #[test] fn test_header_strategy_deserializes_kebab_case_variants() { let cases = [("siblings", HeaderStrategy::Siblings), ("dependency-files", HeaderStrategy::DependencyFiles)]; for (input, expected) in cases { let yaml = format!("strategy: {input}"); let sut: Headers = serde_saphyr::from_str(&yaml).unwrap(); assert_eq!(sut.strategy, expected, "case: strategy: {input}"); } } #[test] fn test_header_strategy_rejects_unknown_variant() { let yaml = "strategy: unknown"; let sut: Result = serde_saphyr::from_str(yaml); assert!(sut.is_err(), "expected deserialization to fail for: {yaml}"); } #[test] fn compiler_as_spelling_is_kept_verbatim() { // The schema layer validates nothing about `as:`: it round-trips the // spelling the user wrote, whatever it is. let cases = ["gcc", "ccache", "not-a-compiler"]; for spelling in cases { let yaml = format!("path: /usr/bin/cc\nas: {spelling}"); let sut: Compiler = serde_saphyr::from_str(&yaml).unwrap(); assert_eq!(sut.as_.as_deref(), Some(spelling), "case: as: {spelling}"); } } } rizsotto-Bear-0c30b27/crates/bear/src/config/validation.rs000066400000000000000000000354561527177720300235560ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later use super::types::*; use thiserror::Error; /// Trait for validating configuration objects pub(super) trait Validator { type Error: std::error::Error; fn validate(config: &T) -> Result<(), Self::Error>; } /// Validation errors for configuration #[derive(Debug, Error)] pub enum ValidationError { #[error("Empty string value for field '{field}'")] EmptyString { field: String }, #[error("Path does not exist: '{path}'")] PathNotFound { path: String }, #[error("Duplicate {field} entry at: {idx}")] DuplicateEntry { field: &'static str, idx: usize }, #[error("Path format error: {message}")] PathFormatError { message: &'static str }, #[error("Invalid glob pattern for field '{field}': {message}")] InvalidPattern { field: String, message: String }, #[error("{}", errors.iter().map(ToString::to_string).collect::>().join("; "))] Multiple { errors: Vec }, } /// Collapse a list of errors into a single `Result`. /// /// Returns `Ok` for an empty list, the lone error directly for a singleton, /// and wraps two-or-more in `ValidationError::Multiple`. fn collapse(mut errors: Vec) -> Result<(), ValidationError> { match errors.len() { 0 => Ok(()), 1 => Err(errors.pop().expect("the match arm guarantees one error")), _ => Err(ValidationError::Multiple { errors }), } } /// Append a sub-validator's outcome to `target`, flattening `Multiple` so the /// final error list stays flat (avoids ugly nested `Multiple { [Multiple ...] }`). fn extend_with(target: &mut Vec, result: Result<(), ValidationError>) { match result { Ok(()) => {} Err(ValidationError::Multiple { errors }) => target.extend(errors), Err(single) => target.push(single), } } impl Validator
for Main { type Error = ValidationError; fn validate(config: &Main) -> Result<(), Self::Error> { let mut errors = Vec::new(); // Validate each compiler configuration for compiler in config.compilers.iter() { if let Err(e) = Compiler::validate(compiler) { errors.push(e); } } // Check for duplicate compiler paths let mut seen_paths = std::collections::HashSet::new(); for (idx, compiler) in config.compilers.iter().enumerate() { if !seen_paths.insert(&compiler.path) { errors.push(ValidationError::DuplicateEntry { field: "compiler", idx }); } } extend_with(&mut errors, SourceFilter::validate(&config.sources)); extend_with(&mut errors, DuplicateFilter::validate(&config.duplicates)); extend_with(&mut errors, PathFormat::validate(&config.format.paths)); collapse(errors) } } impl Validator for Compiler { type Error = ValidationError; fn validate(config: &Compiler) -> Result<(), Self::Error> { if config.path.exists() { Ok(()) } else { Err(ValidationError::PathNotFound { path: config.path.display().to_string() }) } } } impl Validator for SourceFilter { type Error = ValidationError; fn validate(config: &SourceFilter) -> Result<(), Self::Error> { let mut errors: Vec = config .directories .iter() .enumerate() .filter(|(_, rule)| rule.path.as_os_str().is_empty()) .map(|(idx, _)| ValidationError::EmptyString { field: format!("sources.directories[{}].path", idx), }) .collect(); for (idx, rule) in config.files.iter().enumerate() { if rule.pattern.is_empty() { errors .push(ValidationError::EmptyString { field: format!("sources.files[{}].pattern", idx) }); continue; } if let Err(source) = glob::Pattern::new(&rule.pattern) { errors.push(ValidationError::InvalidPattern { field: format!("sources.files[{}].pattern", idx), message: source.to_string(), }); } } collapse(errors) } } impl Validator for DuplicateFilter { type Error = ValidationError; fn validate(config: &DuplicateFilter) -> Result<(), Self::Error> { // The closure mutates `seen_fields`, which is intentional: `collect` drives // the iterator to completion in order, so each entry is visited exactly once. let mut seen_fields = std::collections::HashSet::new(); let errors = config .match_on .iter() .enumerate() .filter(|(_, field)| !seen_fields.insert(*field)) .map(|(idx, _)| ValidationError::DuplicateEntry { field: "duplicates.match_on", idx }) .collect(); collapse(errors) } } impl Validator for PathFormat { type Error = ValidationError; /// Validates the path format configuration according to the rules: /// - When directory is relative, file must be relative too /// - When directory is canonical, file can't be absolute /// - When directory is absolute, file can't be canonical fn validate(config: &PathFormat) -> Result<(), Self::Error> { use PathResolver::*; match (&config.directory, &config.file) { (Relative, Absolute | Canonical) => Err(ValidationError::PathFormatError { message: "When directory is relative, file must be relative too", }), (Canonical, Absolute) => Err(ValidationError::PathFormatError { message: "When directory is canonical, file can't be absolute", }), (Absolute, Canonical) => Err(ValidationError::PathFormatError { message: "When directory is absolute, file can't be canonical", }), (AsIs, Absolute | Relative | Canonical) => Err(ValidationError::PathFormatError { message: "When directory as-is, file should be the same", }), _ => Ok(()), } } } #[cfg(test)] mod tests { use super::*; use std::path::PathBuf; #[test] fn test_validate_compiler_invalid_path() { let config = Compiler { path: PathBuf::from("/nonexistent/compiler"), as_: Some("gcc".to_string()), ignore: false, }; let result = Compiler::validate(&config); assert!(result.is_err()); match result.unwrap_err() { ValidationError::PathNotFound { .. } => { // Expected - path doesn't exist } _ => panic!("Expected PathNotFound validation error"), } } // Requirements: output-source-directory-filter #[test] fn test_validate_source_filter_empty_paths() { let config = SourceFilter { directories: vec![ DirectoryRule { path: PathBuf::from("valid/path"), action: DirectoryAction::Include }, DirectoryRule { path: PathBuf::from(""), action: DirectoryAction::Exclude }, ], files: vec![], }; let result = SourceFilter::validate(&config); assert!(result.is_err()); match result.unwrap_err() { ValidationError::EmptyString { field } => { assert_eq!(field, "sources.directories[1].path"); } _ => panic!("Expected empty string validation error"), } } #[test] fn test_validate_source_filter_multiple_empty_paths() { let config = SourceFilter { directories: vec![ DirectoryRule { path: PathBuf::from(""), action: DirectoryAction::Include }, DirectoryRule { path: PathBuf::from("valid/path"), action: DirectoryAction::Exclude }, DirectoryRule { path: PathBuf::from(""), action: DirectoryAction::Include }, ], files: vec![], }; let result = SourceFilter::validate(&config); assert!(result.is_err()); match result.unwrap_err() { ValidationError::Multiple { errors } => { assert_eq!(errors.len(), 2); } _ => panic!("Expected multiple validation errors"), } } #[test] fn test_validate_source_filter_valid_config() { let config = SourceFilter { directories: vec![ DirectoryRule { path: PathBuf::from("/usr/include"), action: DirectoryAction::Exclude }, DirectoryRule { path: PathBuf::from("src"), action: DirectoryAction::Include }, ], files: vec![], }; let result = SourceFilter::validate(&config); assert!(result.is_ok()); } // Requirements: output-generated-file-filter #[test] fn test_validate_source_filter_valid_file_rules() { let config = SourceFilter { directories: vec![], files: vec![ FileRule { pattern: "moc_*.cpp".to_string(), action: DirectoryAction::Exclude }, FileRule { pattern: "generated/*.pb.cc".to_string(), action: DirectoryAction::Exclude }, ], }; let result = SourceFilter::validate(&config); assert!(result.is_ok()); } // Requirements: output-generated-file-filter #[test] fn test_validate_source_filter_empty_pattern() { let config = SourceFilter { directories: vec![], files: vec![FileRule { pattern: String::new(), action: DirectoryAction::Exclude }], }; let result = SourceFilter::validate(&config); assert!(result.is_err()); match result.unwrap_err() { ValidationError::EmptyString { field } => { assert_eq!(field, "sources.files[0].pattern"); } other => panic!("Expected EmptyString validation error, got: {:?}", other), } } // Requirements: output-generated-file-filter #[test] fn test_validate_source_filter_invalid_pattern() { let config = SourceFilter { directories: vec![], files: vec![FileRule { pattern: "moc_[.cpp".to_string(), action: DirectoryAction::Exclude }], }; let result = SourceFilter::validate(&config); assert!(result.is_err()); match result.unwrap_err() { ValidationError::InvalidPattern { field, .. } => { assert_eq!(field, "sources.files[0].pattern"); } other => panic!("Expected InvalidPattern validation error, got: {:?}", other), } } #[test] fn test_validate_source_filter_empty_directories() { let config = SourceFilter { directories: vec![], files: vec![] }; let result = SourceFilter::validate(&config); assert!(result.is_ok()); } #[test] fn test_validate_duplicate_filter_no_duplicates() { let config = DuplicateFilter { match_on: vec![OutputFields::File, OutputFields::Arguments, OutputFields::Directory], }; let result = DuplicateFilter::validate(&config); assert!(result.is_ok()); } // Requirements: output-duplicate-detection #[test] fn test_validate_duplicate_filter_with_duplicates() { let config = DuplicateFilter { match_on: vec![OutputFields::File, OutputFields::Arguments, OutputFields::File], }; let result = DuplicateFilter::validate(&config); assert!(result.is_err()); match result.unwrap_err() { ValidationError::DuplicateEntry { field, idx } => { assert_eq!(field, "duplicates.match_on"); assert_eq!(idx, 2); } _ => panic!("Expected DuplicateEntry validation error"), } } #[test] fn test_validate_duplicate_filter_multiple_duplicates() { let config = DuplicateFilter { match_on: vec![ OutputFields::File, OutputFields::Arguments, OutputFields::File, OutputFields::Directory, OutputFields::Arguments, ], }; let result = DuplicateFilter::validate(&config); assert!(result.is_err()); match result.unwrap_err() { ValidationError::Multiple { errors } => { assert_eq!(errors.len(), 2); } _ => panic!("Expected multiple validation errors"), } } #[test] fn test_validate_duplicate_filter_empty_match_on() { let config = DuplicateFilter { match_on: vec![] }; let result = DuplicateFilter::validate(&config); assert!(result.is_ok()); } #[test] fn test_validate_path_format_success() { let valid_configs = vec![ PathFormat { directory: PathResolver::AsIs, file: PathResolver::AsIs }, PathFormat { directory: PathResolver::Relative, file: PathResolver::Relative }, PathFormat { directory: PathResolver::Canonical, file: PathResolver::Relative }, PathFormat { directory: PathResolver::Absolute, file: PathResolver::Relative }, PathFormat { directory: PathResolver::Absolute, file: PathResolver::Absolute }, ]; for config in valid_configs { assert!(PathFormat::validate(&config).is_ok(), "Config should be valid: {:?}", config); } } #[test] fn test_validate_path_format_failures() { let invalid_configs = vec![ ( PathFormat { directory: PathResolver::Relative, file: PathResolver::Absolute }, "When directory is relative, file must be relative too", ), ( PathFormat { directory: PathResolver::Relative, file: PathResolver::Canonical }, "When directory is relative, file must be relative too", ), ( PathFormat { directory: PathResolver::Canonical, file: PathResolver::Absolute }, "When directory is canonical, file can't be absolute", ), ( PathFormat { directory: PathResolver::Absolute, file: PathResolver::Canonical }, "When directory is absolute, file can't be canonical", ), ( PathFormat { directory: PathResolver::AsIs, file: PathResolver::Canonical }, "When directory as-is, file should be the same", ), ]; for (config, expected_error) in invalid_configs { let result = PathFormat::validate(&config); assert!(result.is_err(), "Config should be invalid: {:?}", config); if let Err(ValidationError::PathFormatError { message }) = result { assert_eq!(message, expected_error); } else { panic!("Expected PathFormatError, got: {:?}", result); } } } } rizsotto-Bear-0c30b27/crates/bear/src/environment.rs000066400000000000000000000140371527177720300225130ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Config-to-primitive adapter for the driver-side build environment. //! //! The interception mechanism itself (wrapper directory, preload injection, //! masquerade resolution, supervised execution) lives in //! `intercept_supervisor::runner`, which works in terms of primitives only //! so it carries no `config`/`args`/`clap` dependency. This module is the thin //! `bear`-side adapter that maps Bear's `config` and `args` types onto those //! primitive calls. use crate::{args, config}; use intercept_supervisor::context; use intercept_supervisor::runner; use std::net::SocketAddr; use std::path::{Path, PathBuf}; use std::process::ExitStatus; pub use intercept_supervisor::ConfigurationError; /// Driver-side build environment: a thin wrapper over /// [`intercept_supervisor::BuildEnvironment`] that adapts Bear's `config` and /// `args` types to the supervisor's primitive API. pub struct BuildEnvironment { inner: runner::BuildEnvironment, } impl BuildEnvironment { /// Creates a new `BuildEnvironment` configured for the specified interception method. /// /// This method dispatches to the appropriate specialized creation method based on the /// configuration type (wrapper or preload mode). In both modes, the interceptor will /// report execution events via TCP sockets to the specified address. /// /// # Arguments /// /// * `intercept` - The interception configuration specifying the mode /// * `compilers` - The configured compilers (only the non-ignored ones are wrapped) /// * `address` - The socket address where the interceptor should report executions /// * `is_compiler` - Predicate used for PATH-based compiler discovery /// /// # Returns /// /// Returns a configured `BuildEnvironment` on success, or a `ConfigurationError` /// if the configuration is invalid or environment setup fails. pub fn create( context: &context::Context, intercept: &config::Intercept, compilers: &[config::Compiler], address: SocketAddr, is_compiler: impl Fn(&Path) -> bool, ) -> Result { let inner = match intercept { config::Intercept::Wrapper => { let executables = wrapped_executables(compilers); runner::BuildEnvironment::create_as_wrapper(context, &executables, address, is_compiler)? } config::Intercept::Preload => runner::BuildEnvironment::create_as_preload(context, address)?, }; Ok(Self { inner }) } /// Executes a build command within the configured interception environment. /// /// Adapts the clap `args::BuildCommand` into the primitive argument slice /// the supervisor's `run_build` expects. pub fn run_build( &self, build_command: args::BuildCommand, ) -> Result { self.inner.run_build(&build_command.arguments) } } /// The configured executables to wrap in wrapper mode. /// /// An entry the user excluded from the database gets no wrapper, and it does /// not count as a configured executable either: a configuration that only /// excludes compilers yields an empty list, which leaves the supervisor free /// to discover compilers on PATH. A non-empty list replaces that scan (see /// `interception-wrapper-mechanism`). fn wrapped_executables(compilers: &[config::Compiler]) -> Vec { compilers.iter().filter(|compiler| !compiler.ignore).map(|compiler| compiler.path.clone()).collect() } /// Creates an [`intercept::Execution`] from a build command with automatic /// environment trimming. /// /// The working directory is obtained from the current process, and environment /// variables are filtered to include only those relevant for compilation /// database generation. /// /// This lives in `bear` (not the supervisor crate) because it depends on /// [`args::BuildCommand`], which carries `clap` into the build graph; keeping it /// here lets the supervisor stay `clap`-free. pub fn execution_from_build_command(command: &args::BuildCommand) -> intercept::Execution { let working_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); let environment = std::env::vars().collect(); intercept::Execution { executable: PathBuf::from(&command.arguments[0]), arguments: command.arguments.clone(), working_dir, environment, } .trim() } #[cfg(test)] mod tests { use super::*; fn compiler(path: &str) -> config::Compiler { config::Compiler { path: PathBuf::from(path), as_: None, ignore: false } } fn ignored(path: &str) -> config::Compiler { config::Compiler { ignore: true, ..compiler(path) } } // Requirements: interception-wrapper-mechanism #[test] fn only_the_compilers_kept_in_the_database_are_wrapped() { let cases = [ ("nothing configured", vec![], vec![]), ("one compiler", vec![compiler("/usr/bin/mycc")], vec!["/usr/bin/mycc"]), ("excluded compiler", vec![ignored("/usr/bin/mycc")], vec![]), ("one of each", vec![compiler("/usr/bin/mycc"), ignored("/usr/bin/nope")], vec!["/usr/bin/mycc"]), ]; for (case, compilers, expected) in cases { let sut = wrapped_executables(&compilers); let expected: Vec = expected.into_iter().map(PathBuf::from).collect(); assert_eq!(sut, expected, "case: {case}"); } } /// A configuration that only excludes compilers must not switch /// interception over to the configured list, because that would silently /// stop the PATH scan and leave the real compilers unwrapped. // Requirements: interception-wrapper-mechanism #[test] fn excluding_every_compiler_leaves_path_discovery_enabled() { let compilers = vec![ignored("/usr/bin/mycc"), ignored("/usr/bin/nope")]; let sut = wrapped_executables(&compilers); assert!(sut.is_empty(), "an all-excluded configuration must not count as a configured list"); } } rizsotto-Bear-0c30b27/crates/bear/src/lib.rs000066400000000000000000000002221527177720300207040ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later pub mod args; pub mod config; pub mod environment; pub mod modes; pub mod output; pub mod parse_sh; rizsotto-Bear-0c30b27/crates/bear/src/modes/000077500000000000000000000000001527177720300207035ustar00rootroot00000000000000rizsotto-Bear-0c30b27/crates/bear/src/modes/execution.rs000066400000000000000000001032301527177720300232530ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later use crate::args::BuildCommand; use crate::environment::execution_from_build_command; use crate::output::AtomicFile; use intercept_supervisor::SuperviseError; use std::process::{ExitCode, ExitStatus}; use std::sync::Arc; use std::sync::mpsc::{Receiver, SyncSender, sync_channel}; use thiserror::Error; /// A type-erased error carried across the pipeline seam. /// /// Each `Producer`/`Consumer`/`Cancellable` implementation keeps its own /// precise error type internally and boxes it here at the trait boundary, so /// the traits do not force every implementation onto one shared error type. pub type DynError = Box; /// Backpressure here would slow the build: a full channel stops the /// collector from accepting reports. The room is large enough that only a /// consumer far behind the build ever blocks it. const INTERCEPT_CHANNEL_CAPACITY: usize = 1024; /// Replay reads from a file or a pipe, so backpressure costs nothing and keeps memory /// use low. const REPLAY_CHANNEL_CAPACITY: usize = 10; /// A trait for consuming events from a channel-based stream. /// /// # Thread Safety /// Implementers must be `Send` to allow usage across thread boundaries. #[cfg_attr(test, mockall::automock)] pub trait Consumer: Send { /// Consumes all items from the receiver until the channel is closed. /// /// This is a blocking operation that processes each received item. /// The method returns when the sender side of the channel is dropped /// or when an error occurs during processing. /// /// The consumer writes its output but does not publish it: only the /// caller knows whether the whole run succeeded, so it commits the /// returned file or drops it to roll it back. /// /// # Arguments /// * `receiver` - Channel receiver to consume items from /// /// # Returns /// * `Ok(AtomicFile)` - All items were written, the output waits for the commit /// * `Err(DynError)` - An error occurred during processing fn consume(self: Box, receiver: Receiver) -> Result; } /// A trait for producing events to a channel-based stream. /// /// # Thread Safety /// Implementers must be `Send + Sync` to allow usage across thread boundaries. #[cfg_attr(test, mockall::automock)] pub trait Producer: Send + Sync { /// Produces items by sending them through the provided sender. /// /// This is a blocking operation that continues until all items are produced /// or an error occurs. The producer should close the sender when finished /// to signal completion to consumers. /// /// # Arguments /// * `sender` - Channel sender to produce items to /// /// # Returns /// * `Ok(())` - All items were successfully produced /// * `Err(DynError)` - An error occurred during production fn produce(&self, sender: SyncSender) -> Result<(), DynError>; } /// A trait for cancelling ongoing operations. /// /// # Thread Safety /// Implementers must be `Send + Sync` to allow usage across thread boundaries. #[cfg_attr(test, mockall::automock)] pub trait Cancellable: Send + Sync { /// Cancels the ongoing operation. /// /// # Returns /// * `Ok(())` - Cancellation was successful /// * `Err(DynError)` - An error occurred during cancellation fn cancel(&self) -> Result<(), DynError>; } /// A trait for producers that support cancellation during operation. /// /// Combines `Producer` and `Cancellable` functionality for event production /// that can be cancelled mid-operation. pub trait CancellableProducer: Producer + Cancellable {} /// A trait for executing build commands. /// /// Executors are responsible for running the actual build process while /// allowing command interception to occur. They manage the lifecycle of /// the build command and report its exit status. /// /// May encounter `SuperviseError` during execution. #[cfg_attr(test, mockall::automock)] pub trait Executor { /// Executes the given build command. /// /// This is a blocking operation that runs the build command to completion. /// During execution, the command and its subprocesses may be intercepted /// by Bear's interception mechanisms. /// /// # Arguments /// * `command` - The build command to execute /// /// # Returns /// * `Ok(ExitStatus)` - The build completed with the given exit status /// * `Err(SuperviseError)` - An error occurred during execution fn run(&self, command: BuildCommand) -> Result; } /// Coordinates live command interception during build execution. /// /// `Interceptor` manages the simultaneous execution of: /// - Build command execution (via `Executor`) /// - Command interception (via `CancellableProducer`) /// - Event processing (via `Consumer`) /// /// The interceptor ensures proper coordination between these components, /// handling thread synchronization and error propagation. pub struct Interceptor { producer: Arc, consumer: Box, build: Box, } impl Interceptor { pub fn new( producer: Arc, consumer: Box, build: Box, ) -> Self { Self { producer, consumer, build } } /// Runs live command interception for the given build command. /// /// # Arguments /// * `command` - The build command to execute with interception /// /// # Returns /// * `Ok(ExitCode::SUCCESS)` - All operations completed successfully /// * `Err(RuntimeError)` - An error occurred in any component pub fn run(self, command: BuildCommand) -> Result { let (sender, receiver) = sync_channel::(INTERCEPT_CHANNEL_CAPACITY); // Inject the initial command as the first execution let _ = sender.send(execution_from_build_command(&command)); let producer_thread = { let producer = Arc::clone(&self.producer); std::thread::spawn(move || producer.produce(sender)) }; let consumer_thread = { let consumer = self.consumer; std::thread::spawn(move || consumer.consume(receiver)) }; let build_result = self.build.run(command); // Stop the interception whatever the build did, so both threads // finish and the output can be committed or rolled back. A failed // cancel may leave the producer running forever, so the threads are // not joined then, and the output is left as it is. if let Err(error) = self.producer.cancel() { // Report the build error first: it is the likelier cause. build_result?; return Err(RuntimeError::Producer(error)); } let producer_result = producer_thread.join().map_err(|_| RuntimeError::Thread("Producer thread panicked")); let consumer_result = consumer_thread.join().map_err(|_| RuntimeError::Thread("Consumer thread panicked")); // Publish only when nothing failed on Bear's side; every early return // drops the output, which rolls it back. A build that ran and failed // is not such a failure: its exit status is returned below. let exit_status = build_result?; producer_result?.map_err(RuntimeError::Producer)?; commit(consumer_result?.map_err(RuntimeError::Consumer)?)?; // The exit code is not always available. When the process is killed by a signal, // the exit code is not available. In this case, we return the `FAILURE` exit code. let exit_code = exit_status.code().map(|code| ExitCode::from(code as u8)).unwrap_or(ExitCode::FAILURE); Ok(exit_code) } } /// Drives a producer/consumer pair over existing input, without running a build. /// /// `Replayer` powers the modes that process input that already exists: /// - Re-analyzing previously captured intercept events (with different configurations) /// - Testing semantic analysis changes against archived event data /// - Interpreting shell command text into execution events (parse-sh) pub struct Replayer { producer: Box, consumer: Box, } impl Replayer { pub fn new(producer: Box, consumer: Box) -> Self { Self { producer, consumer } } /// Replays stored intercept events through the processing pipeline. /// /// # Returns /// * `Ok(ExitCode::SUCCESS)` - All events were successfully replayed /// * `Err(RuntimeError)` - An error occurred during replay (most likely IO error) pub fn run(self) -> Result { let (sender, receiver) = sync_channel::(REPLAY_CHANNEL_CAPACITY); let producer_thread = { let producer = self.producer; std::thread::spawn(move || producer.produce(sender)) }; let consumer_thread = { let consumer = self.consumer; std::thread::spawn(move || consumer.consume(receiver)) }; // Join both threads before propagating either error, then publish // only when both succeeded; every early return drops the output, // which rolls it back. The producer's error wins when both fail, // since the consumer only saw a truncated stream of it. let producer_result = producer_thread.join().map_err(|_| RuntimeError::Thread("Producer thread panicked")); let consumer_result = consumer_thread.join().map_err(|_| RuntimeError::Thread("Consumer thread panicked")); producer_result?.map_err(RuntimeError::Producer)?; commit(consumer_result?.map_err(RuntimeError::Consumer)?)?; Ok(ExitCode::SUCCESS) } } fn commit(output: AtomicFile) -> Result<(), RuntimeError> { output.commit().map_err(|err| RuntimeError::Consumer(Box::new(err))) } /// Errors that can occur during event processing or running the build. #[derive(Error, Debug)] pub enum RuntimeError { #[error("{0}")] Producer(DynError), #[error("{0}")] Consumer(DynError), #[error(transparent)] Executor(#[from] SuperviseError), #[error("Internal error: {0}")] Thread(&'static str), } #[cfg(test)] mod tests { use super::*; use crate::output::{SerializationError, WriterError}; use intercept::Execution; use std::collections::HashMap; use std::sync::{Arc, Mutex}; use std::time::Duration; // Helper functions for comparing ExitCode values that works with older Rust versions fn assert_is_success(code: ExitCode) { assert_eq!(format!("{code:?}"), format!("{:?}", ExitCode::SUCCESS)); } fn assert_is_failure(code: ExitCode) { // Use inequality with SUCCESS rather than equality with FAILURE // to avoid platform-specific differences in failure representation assert_ne!(format!("{code:?}"), format!("{:?}", ExitCode::SUCCESS)); } // Simple mock struct that implements both Producer and Cancellable struct MockCancellableProducer { executions: Vec, should_fail_produce: bool, should_fail_cancel: bool, cancel_count: Arc>, } impl MockCancellableProducer { fn new(executions: Vec) -> Self { Self { executions, should_fail_produce: false, should_fail_cancel: false, cancel_count: Arc::new(Mutex::new(0)), } } fn with_produce_failure(executions: Vec) -> Self { Self { executions, should_fail_produce: true, should_fail_cancel: false, cancel_count: Arc::new(Mutex::new(0)), } } fn with_cancel_failure(executions: Vec) -> Self { Self { executions, should_fail_produce: false, should_fail_cancel: true, cancel_count: Arc::new(Mutex::new(0)), } } fn cancel_call_count(&self) -> usize { *self.cancel_count.lock().expect("Failed to lock cancel_count mutex") } } impl Producer for MockCancellableProducer { fn produce(&self, sender: SyncSender) -> Result<(), DynError> { if self.should_fail_produce { return Err(Box::new(std::io::Error::new( std::io::ErrorKind::ConnectionRefused, "Test failure", ))); } for execution in &self.executions { // Mirror the real producers: a disconnected channel means the // consumer finished or failed, so stop quietly instead of // erroring (which would race the consumer's own error). if sender.send(execution.clone()).is_err() { break; } } Ok(()) } } impl Cancellable for MockCancellableProducer { fn cancel(&self) -> Result<(), DynError> { *self.cancel_count.lock().expect("Failed to lock cancel_count mutex") += 1; if self.should_fail_cancel { return Err(Box::new(std::io::Error::other("Cancel failure"))); } Ok(()) } } impl CancellableProducer for MockCancellableProducer {} fn create_test_execution(executable: &str) -> Execution { Execution::from_strings(executable, vec!["arg1", "arg2"], "/tmp", HashMap::new()) } fn create_test_command() -> BuildCommand { BuildCommand { arguments: vec!["make".to_string(), "all".to_string()] } } fn create_success_exit_status() -> ExitStatus { // According to the Rust documentation, ExitStatus::default() is equivalent to success. ExitStatus::default() } #[cfg(unix)] fn create_failure_exit_status() -> ExitStatus { use std::os::unix::process::ExitStatusExt; ExitStatusExt::from_raw(256) // On Unix, 256 represents exit code 1 (256 >> 8 = 1) } #[cfg(not(unix))] fn create_failure_exit_status() -> ExitStatus { use std::os::windows::process::ExitStatusExt; ExitStatusExt::from_raw(1) // On Windows, use raw value 1 to get exit code 1 } #[test] fn test_replayer_happy_path() { let events = vec![ create_test_execution("/usr/bin/gcc"), create_test_execution("/usr/bin/clang"), create_test_execution("/usr/bin/g++"), ]; let captured_events = Arc::new(Mutex::new(Vec::new())); let captured_events_clone = Arc::clone(&captured_events); let mut producer_mock = MockProducer::new(); producer_mock.expect_produce().times(1).returning(move |sender| { let test_events = vec![ create_test_execution("/usr/bin/gcc"), create_test_execution("/usr/bin/clang"), create_test_execution("/usr/bin/g++"), ]; for event in test_events { sender.send(event).expect("Failed to send test event"); } Ok(()) }); let mut consumer_mock = MockConsumer::new(); consumer_mock.expect_consume().times(1).returning(move |receiver| { for event in receiver { captured_events_clone.lock().expect("Failed to lock captured_events mutex").push(event); } Ok(AtomicFile::none()) }); let replayer = Replayer::new(Box::new(producer_mock), Box::new(consumer_mock)); let result = replayer.run(); assert!(result.is_ok()); assert_is_success(result.expect("Failed to get result in happy path test")); let consumed_events = captured_events.lock().expect("Failed to lock captured_events mutex"); assert_eq!(consumed_events.len(), 3); assert_eq!(*consumed_events, events); } #[test] fn test_replayer_producer_failure() { let mut producer_mock = MockProducer::new(); producer_mock.expect_produce().times(1).returning(|_| { Err(Box::new(std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "Test failure"))) }); let consumer_mock = MockConsumer::new(); let replayer = Replayer::new(Box::new(producer_mock), Box::new(consumer_mock)); let result = replayer.run(); assert!(result.is_err()); assert!(matches!(result.unwrap_err(), RuntimeError::Producer(_))); } #[test] fn test_replayer_consumer_failure() { let mut producer_mock = MockProducer::new(); producer_mock.expect_produce().times(1).returning(|sender| { // The consumer fails without receiving, so the send may observe // a dropped receiver; the producer must not turn that into its // own failure, or the asserted error variant becomes racy. let _ = sender.send(create_test_execution("/usr/bin/gcc")); Ok(()) }); let mut consumer_mock = MockConsumer::new(); consumer_mock.expect_consume().times(1).returning(|_| { Err(Box::new(WriterError::Io( std::path::PathBuf::new(), SerializationError::Io(std::io::Error::other("Test failure")), ))) }); let replayer = Replayer::new(Box::new(producer_mock), Box::new(consumer_mock)); let result = replayer.run(); assert!(result.is_err()); assert!(matches!(result.unwrap_err(), RuntimeError::Consumer(_))); } #[test] fn test_replayer_producer_stops_when_consumer_drops_receiver_mid_stream() { // More events than the channel holds, so the producer blocks in send // until the consumer drops its receiver. let mut producer_mock = MockProducer::new(); producer_mock.expect_produce().times(1).returning(|sender| { for _ in 0..(REPLAY_CHANNEL_CAPACITY * 5) { if sender.send(create_test_execution("/usr/bin/gcc")).is_err() { return Ok(()); } } panic!("the consumer dropped its receiver, so a send must fail"); }); let mut consumer_mock = MockConsumer::new(); consumer_mock.expect_consume().times(1).returning(|receiver| { receiver.recv().expect("first event"); Err(Box::new(WriterError::Io( std::path::PathBuf::new(), SerializationError::Io(std::io::Error::other("Test failure")), ))) }); let sut = Replayer::new(Box::new(producer_mock), Box::new(consumer_mock)).run(); assert!(matches!(sut, Err(RuntimeError::Consumer(_)))); } #[test] fn test_replayer_empty_events() { let captured_events = Arc::new(Mutex::new(Vec::new())); let captured_events_clone = Arc::clone(&captured_events); let mut producer_mock = MockProducer::new(); producer_mock.expect_produce().times(1).returning(|_| Ok(())); let mut consumer_mock = MockConsumer::new(); consumer_mock.expect_consume().times(1).returning(move |receiver| { for event in receiver { captured_events_clone.lock().expect("Failed to lock captured_events mutex").push(event); } Ok(AtomicFile::none()) }); let replayer = Replayer::new(Box::new(producer_mock), Box::new(consumer_mock)); let result = replayer.run(); assert!(result.is_ok()); assert_is_success(result.expect("Failed to get result in empty events test")); assert_eq!(captured_events.lock().expect("Failed to lock captured_events mutex").len(), 0); } #[test] fn test_interceptor_happy_path() { let events = vec![create_test_execution("/usr/bin/gcc"), create_test_execution("/usr/bin/clang")]; let captured_events = Arc::new(Mutex::new(Vec::new())); let captured_events_clone = Arc::clone(&captured_events); let producer_mock = Arc::new(MockCancellableProducer::new(events.clone())); let mut consumer_mock = MockConsumer::new(); consumer_mock.expect_consume().times(1).returning(move |receiver| { for event in receiver { captured_events_clone.lock().expect("Failed to lock captured_events mutex").push(event); } Ok(AtomicFile::none()) }); let mut executor_mock = MockExecutor::new(); executor_mock.expect_run().times(1).returning(|_| Ok(create_success_exit_status())); let interceptor = Interceptor::new(producer_mock.clone(), Box::new(consumer_mock), Box::new(executor_mock)); let result = interceptor.run(create_test_command()); assert!(result.is_ok()); assert_is_success(result.expect("Failed to get result in interceptor happy path test")); let consumed_events = captured_events.lock().expect("Failed to lock captured_events mutex"); assert_eq!(consumed_events.len(), 3); // Verify the first item is the initial command execution let initial = &consumed_events[0]; assert_eq!(initial.executable.to_str().unwrap(), "make"); assert_eq!(initial.arguments, vec!["make", "all"]); // Verify the remaining items match the mock producer executions assert_eq!(consumed_events[1..], events); assert_eq!(producer_mock.cancel_call_count(), 1); } #[test] fn test_interceptor_executor_failure() { let producer_mock = Arc::new(MockCancellableProducer::new(vec![])); let mut consumer_mock = MockConsumer::new(); consumer_mock.expect_consume().times(1).returning(|receiver| { for _ in receiver {} Ok(AtomicFile::none()) }); let mut executor_mock = MockExecutor::new(); executor_mock.expect_run().times(1).returning(|_| { Err(SuperviseError::ProcessSpawn { executable: "test_executable".into(), source: std::io::Error::new(std::io::ErrorKind::NotFound, "Test executor failure"), }) }); let interceptor = Interceptor::new(producer_mock, Box::new(consumer_mock), Box::new(executor_mock)); let result = interceptor.run(create_test_command()); assert!(result.is_err()); assert!(matches!(result.unwrap_err(), RuntimeError::Executor(_))); } #[test] fn test_interceptor_producer_failure() { let events = vec![create_test_execution("/usr/bin/gcc")]; let producer_mock = Arc::new(MockCancellableProducer::with_produce_failure(events)); let mut consumer_mock = MockConsumer::new(); consumer_mock.expect_consume().times(1).returning(|receiver| { // Actually consume all events from the channel to prevent race condition // where the channel is closed before the producer finishes sending while receiver.recv().is_ok() { // Consume events until channel is closed } Ok(AtomicFile::none()) }); let mut executor_mock = MockExecutor::new(); executor_mock.expect_run().times(1).returning(|_| Ok(create_success_exit_status())); let interceptor = Interceptor::new(producer_mock.clone(), Box::new(consumer_mock), Box::new(executor_mock)); let result = interceptor.run(create_test_command()); assert!(result.is_err()); assert!(matches!(result.unwrap_err(), RuntimeError::Producer(_))); assert_eq!(producer_mock.cancel_call_count(), 1); } #[test] fn test_interceptor_consumer_failure() { let events = vec![create_test_execution("/usr/bin/gcc")]; let producer_mock = Arc::new(MockCancellableProducer::new(events)); let mut consumer_mock = MockConsumer::new(); consumer_mock.expect_consume().times(1).returning(|_| { Err(Box::new(WriterError::Io( std::path::PathBuf::new(), SerializationError::Io(std::io::Error::other("Test failure")), ))) }); let mut executor_mock = MockExecutor::new(); executor_mock.expect_run().times(1).returning(|_| Ok(create_success_exit_status())); let interceptor = Interceptor::new(producer_mock.clone(), Box::new(consumer_mock), Box::new(executor_mock)); let result = interceptor.run(create_test_command()); assert!(result.is_err()); assert!(matches!(result.unwrap_err(), RuntimeError::Consumer(_))); assert_eq!(producer_mock.cancel_call_count(), 1); } #[test] fn test_interceptor_cancel_failure() { let events = vec![create_test_execution("/usr/bin/gcc")]; let producer_mock = Arc::new(MockCancellableProducer::with_cancel_failure(events)); let mut consumer_mock = MockConsumer::new(); consumer_mock.expect_consume().times(1).returning(|receiver| { // Actually consume all events from the channel to prevent race condition // where the channel is closed before the producer finishes sending while receiver.recv().is_ok() { // Consume events until channel is closed } Ok(AtomicFile::none()) }); let mut executor_mock = MockExecutor::new(); executor_mock.expect_run().times(1).returning(|_| Ok(create_success_exit_status())); let interceptor = Interceptor::new(producer_mock.clone(), Box::new(consumer_mock), Box::new(executor_mock)); let result = interceptor.run(create_test_command()); assert!(result.is_err()); assert!(matches!(result.unwrap_err(), RuntimeError::Producer(_))); assert_eq!(producer_mock.cancel_call_count(), 1); } #[test] fn test_interceptor_non_zero_exit_code() { let events = vec![create_test_execution("/usr/bin/gcc")]; let producer_mock = Arc::new(MockCancellableProducer::new(events)); let mut consumer_mock = MockConsumer::new(); consumer_mock.expect_consume().times(1).returning(|receiver| { // Actually consume all events from the channel to prevent race condition // where the channel is closed before the producer finishes sending while receiver.recv().is_ok() { // Consume events until channel is closed } Ok(AtomicFile::none()) }); let mut executor_mock = MockExecutor::new(); executor_mock.expect_run().times(1).returning(|_| Ok(create_failure_exit_status())); let interceptor = Interceptor::new(producer_mock, Box::new(consumer_mock), Box::new(executor_mock)); let result = interceptor.run(create_test_command()); assert!(result.is_ok()); assert_is_failure(result.expect("Failed to get result in non-zero exit code test")); } #[test] fn test_interceptor_coordination_timing() { let events = vec![ create_test_execution("/usr/bin/gcc"), create_test_execution("/usr/bin/clang"), create_test_execution("/usr/bin/g++"), ]; let captured_events = Arc::new(Mutex::new(Vec::new())); let captured_events_clone = Arc::clone(&captured_events); let producer_mock = Arc::new(MockCancellableProducer::new(events.clone())); let mut consumer_mock = MockConsumer::new(); consumer_mock.expect_consume().times(1).returning(move |receiver| { for event in receiver { std::thread::sleep(Duration::from_millis(5)); captured_events_clone.lock().expect("Failed to lock captured_events mutex").push(event); } Ok(AtomicFile::none()) }); let mut executor_mock = MockExecutor::new(); executor_mock.expect_run().times(1).returning(|_| { std::thread::sleep(Duration::from_millis(50)); Ok(create_success_exit_status()) }); let interceptor = Interceptor::new(producer_mock.clone(), Box::new(consumer_mock), Box::new(executor_mock)); let result = interceptor.run(create_test_command()); assert!(result.is_ok()); assert_is_success(result.expect("Failed to get result in coordination timing test")); let consumed_events = captured_events.lock().expect("Failed to lock captured_events mutex"); assert_eq!(consumed_events.len(), 4); // Verify the first item is the initial command execution let initial = &consumed_events[0]; assert_eq!(initial.executable.to_str().unwrap(), "make"); assert_eq!(initial.arguments, vec!["make", "all"]); // Verify the remaining items match the mock producer executions assert_eq!(consumed_events[1..], events); assert_eq!(producer_mock.cancel_call_count(), 1); } #[test] fn test_replayer_coordination_timing() { let events = vec![ create_test_execution("/usr/bin/gcc"), create_test_execution("/usr/bin/clang"), create_test_execution("/usr/bin/g++"), ]; let captured_events = Arc::new(Mutex::new(Vec::new())); let captured_events_clone = Arc::clone(&captured_events); let mut producer_mock = MockProducer::new(); producer_mock.expect_produce().times(1).returning(move |sender| { let test_events = vec![ create_test_execution("/usr/bin/gcc"), create_test_execution("/usr/bin/clang"), create_test_execution("/usr/bin/g++"), ]; for event in test_events { std::thread::sleep(Duration::from_millis(5)); sender.send(event).expect("Failed to send test event"); } Ok(()) }); let mut consumer_mock = MockConsumer::new(); consumer_mock.expect_consume().times(1).returning(move |receiver| { for event in receiver { std::thread::sleep(Duration::from_millis(10)); captured_events_clone.lock().expect("Failed to lock captured_events mutex").push(event); } Ok(AtomicFile::none()) }); let replayer = Replayer::new(Box::new(producer_mock), Box::new(consumer_mock)); let result = replayer.run(); assert!(result.is_ok()); assert_is_success(result.expect("Failed to get result in replayer coordination timing test")); let consumed_events = captured_events.lock().expect("Failed to lock captured_events mutex"); assert_eq!(consumed_events.len(), 3); assert_eq!(*consumed_events, events); } /// A consumer that drains the channel and hands back a pending file /// for `final_path`, as the real file-backed consumers do. fn consumer_with_pending_output(final_path: &std::path::Path) -> MockConsumer { let (output, file) = AtomicFile::create(final_path).expect("Failed to create the atomic file"); drop(file); let output = Mutex::new(Some(output)); let mut consumer_mock = MockConsumer::new(); consumer_mock.expect_consume().times(1).returning(move |receiver| { for _ in receiver {} Ok(output.lock().expect("Failed to lock output mutex").take().expect("consume runs once")) }); consumer_mock } // Requirements: output-atomic-write #[test] fn test_interceptor_rolls_back_when_build_cannot_start() { let dir = tempfile::tempdir().unwrap(); let final_path = dir.path().join("output.json"); let producer_mock = Arc::new(MockCancellableProducer::new(vec![])); let consumer_mock = consumer_with_pending_output(&final_path); let mut executor_mock = MockExecutor::new(); executor_mock.expect_run().times(1).returning(|_| { Err(SuperviseError::ProcessSpawn { executable: "test_executable".into(), source: std::io::Error::new(std::io::ErrorKind::NotFound, "Test executor failure"), }) }); let sut = Interceptor::new(producer_mock.clone(), Box::new(consumer_mock), Box::new(executor_mock)) .run(create_test_command()); assert!(matches!(sut, Err(RuntimeError::Executor(_)))); assert_eq!(producer_mock.cancel_call_count(), 1); assert_eq!(std::fs::read_dir(dir.path()).unwrap().count(), 0, "nothing may be left behind"); } // Requirements: output-atomic-write #[test] fn test_interceptor_commits_when_build_fails() { let dir = tempfile::tempdir().unwrap(); let final_path = dir.path().join("output.json"); let producer_mock = Arc::new(MockCancellableProducer::new(vec![])); let consumer_mock = consumer_with_pending_output(&final_path); let mut executor_mock = MockExecutor::new(); executor_mock.expect_run().times(1).returning(|_| Ok(create_failure_exit_status())); let sut = Interceptor::new(producer_mock, Box::new(consumer_mock), Box::new(executor_mock)) .run(create_test_command()); assert_is_failure(sut.expect("a failing build is not an error of Bear")); assert!(final_path.exists()); assert_eq!(std::fs::read_dir(dir.path()).unwrap().count(), 1, "only the output may remain"); } // Requirements: output-atomic-write #[test] fn test_replayer_rolls_back_when_producer_fails() { let dir = tempfile::tempdir().unwrap(); let final_path = dir.path().join("output.json"); let mut producer_mock = MockProducer::new(); producer_mock.expect_produce().times(1).returning(|_| { Err(Box::new(std::io::Error::new(std::io::ErrorKind::InvalidData, "Test failure"))) }); let consumer_mock = consumer_with_pending_output(&final_path); let sut = Replayer::new(Box::new(producer_mock), Box::new(consumer_mock)).run(); assert!(matches!(sut, Err(RuntimeError::Producer(_)))); assert_eq!(std::fs::read_dir(dir.path()).unwrap().count(), 0, "nothing may be left behind"); } } rizsotto-Bear-0c30b27/crates/bear/src/modes/mod.rs000066400000000000000000001007201527177720300220300ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! # Execution Modes //! //! This module provides the core execution patterns for Bear's operation modes. //! It defines traits and implementations for the producer-consumer pattern //! used throughout the application. mod execution; use crate::args::is_stdio; use crate::environment; use crate::{args, config, output}; use intercept_supervisor::CollectorOnTcp; use intercept_supervisor::context; use semantic::SpellingError; use semantic::{CompilerHints, CompilerRecognizer}; use std::process::ExitCode; use std::sync::Arc; /// Represents the application execution modes. /// /// Bear supports four user-facing modes: /// - **Intercept only**: Capture build commands and write them to a file for later analysis. /// - **Semantic only**: Read previously captured build commands from a file and analyze them. /// - **Combined**: Capture build commands and analyze them in real-time. /// - **Parse shell text**: Interpret shell command text and produce the compilation database. /// /// Internally, this enum distinguishes between: /// - `Intercept`: Modes that execute build commands while capturing events (intercept-only and combined) /// - `Replay`: Modes that process an existing input without running a build (semantic-only and parse-sh) /// /// The distinction between the input sources and output formats is handled by /// the producer and consumer configuration, not the mode itself. pub enum Mode { Intercept(execution::Interceptor, args::BuildCommand), Replay(execution::Replayer), /// Print the compilers Bear recognizes, then exit. Consults no config /// and reads no input -- handled like parse-sh, before config load. PrintCompilers, } impl Mode { /// Configure the application mode from the command line arguments. /// /// For the modes that consume it, this loads the configuration (from /// `--config`, a default-location file, or built-in defaults), checks that /// the argument/configuration combination is valid, and builds the matching /// mode instance -- returning a useful error otherwise. pub fn configure(context: context::Context, args: args::Arguments) -> Result { let args::Arguments { config: config_path, mode } = args; // print-compilers runs without configuration: it only lists the // built-in recognition tables, so it loads none. if let args::Mode::PrintCompilers = mode { log::debug!("Mode: print recognized compilers"); return Ok(Self::PrintCompilers); } let config = config::Loader::load(&context, &config_path) .map_err(|error| ConfigurationError::ConfigLoad(Box::new(error)))?; log::info!("{config}"); // Resolve every configured `as:` spelling once, here, so a typo fails // at startup in every mode that reads configuration -- before a build // starts. This is also the only place that sees both the config types // and the semantic types, so it owns the conversion between them. let hints = compiler_hints(&config.compilers)?; match mode { args::Mode::Intercept { input, output } => { log::debug!("Mode: intercept build and write events"); let (producer, address) = CollectorOnTcp::new().map_err(ConfigurationError::CollectorCreation)?; let recognizer = CompilerRecognizer::new_with_hints(hints); let build = environment::BuildEnvironment::create( &context, &config.intercept, &config.compilers, address, |path| recognizer.recognize(path).is_some(), ) .map_err(ConfigurationError::ExecutorCreation)?; let consumer = impls::RawEventWriter::create(&output.path) .map_err(ConfigurationError::ConsumerCreation)?; let intercept = execution::Interceptor::new( Arc::new(impls::TcpEventProducer::create(producer)), Box::new(consumer), Box::new(impls::BuildExecutor::create(build)), ); Ok(Self::Intercept(intercept, input)) } args::Mode::Semantic { input, output } => { log::debug!("Mode: replay events and semantic analysis"); let source = impls::RawEventReader::create(&input.path)?; let consumer = impls::SemanticEventWriter::create(output, &config, hints) .map_err(ConfigurationError::ConsumerCreation)?; let replayer = execution::Replayer::new(Box::new(source), Box::new(consumer)); Ok(Self::Replay(replayer)) } args::Mode::ParseSh { input, output } => { log::debug!("Mode: parse shell text and semantic analysis"); // When the caller left the directory override unset, fall // back to Bear's invocation directory so the producer always // has a concrete path to interpret from. let directory = input.directory.unwrap_or_else(|| context.current_directory.clone()); let producer = impls::ShellScriptReader::create(&input.path, directory, context.environment.clone())?; let consumer = impls::SemanticEventWriter::create(output, &config, hints) .map_err(ConfigurationError::ConsumerCreation)?; let replayer = execution::Replayer::new(Box::new(producer), Box::new(consumer)); Ok(Self::Replay(replayer)) } args::Mode::Combined { input, output } => { log::debug!("Mode: intercept build and semantic analysis"); if is_stdio(&output.path) { return Err(ConfigurationError::InvalidConfiguration( "cannot write the compilation database to stdout while running the build: \ it would mix with build output; write to a file, or split into \ `bear intercept` then `bear semantic`" .to_string(), )); } let (producer, address) = CollectorOnTcp::new().map_err(ConfigurationError::CollectorCreation)?; let recognizer = CompilerRecognizer::new_with_hints(hints.clone()); let build = environment::BuildEnvironment::create( &context, &config.intercept, &config.compilers, address, |path| recognizer.recognize(path).is_some(), ) .map_err(ConfigurationError::ExecutorCreation)?; let consumer = impls::SemanticEventWriter::create(output, &config, hints) .map_err(ConfigurationError::ConsumerCreation)?; let intercept = execution::Interceptor::new( Arc::new(impls::TcpEventProducer::create(producer)), Box::new(consumer), Box::new(impls::BuildExecutor::create(build)), ); Ok(Self::Intercept(intercept, input)) } // print-compilers is handled above, before configuration loads. args::Mode::PrintCompilers => unreachable!("print-compilers handled above"), } } /// Runs the application mode. /// /// This executes the build command in intercept mode, or reads the input /// source (event file or shell text) in replay mode. All errors returned /// are runtime errors that occur after valid arguments and configuration /// have been provided. pub fn run(self) -> ExitCode { let status = match self { Self::Intercept(interceptor, command) => interceptor.run(command), Self::Replay(replayer) => replayer.run(), Self::PrintCompilers => { print!( "Bear {} recognizes the following compilers:\n\n{}", env!("CARGO_PKG_VERSION"), semantic::print_compilers() ); return ExitCode::SUCCESS; } }; status.unwrap_or_else(|error| { log::error!("{error}"); ExitCode::FAILURE }) } } /// Converts the configured compilers into the semantic hint table. /// /// Every entry has its `as:` spelling resolved, including the ones marked /// `ignore: true`: they contribute no hint, but a typo in one still fails the /// run, the way it did when the configuration schema validated the spelling /// itself. An entry without `as:` is classified by the builder. fn compiler_hints(compilers: &[config::Compiler]) -> Result { let mut hints = CompilerHints::new(); for compiler in compilers { let spelling = compiler.as_.as_deref(); let resolved = if compiler.ignore { CompilerHints::check(spelling) } else { hints.add(&compiler.path, spelling) }; resolved.map_err(|source| ConfigurationError::CompilerSpelling { path: compiler.path.display().to_string(), source, })?; } Ok(hints) } #[derive(Debug, thiserror::Error)] pub enum ConfigurationError { #[error("cannot listen for intercepted commands: {0}")] CollectorCreation(std::io::Error), #[error(transparent)] ExecutorCreation(environment::ConfigurationError), #[error(transparent)] ConsumerCreation(output::WriterCreationError), #[error("cannot read {0}: {1}")] InputAccess(std::path::PathBuf, std::io::Error), #[error("{0}")] InvalidConfiguration(String), #[error(transparent)] ConfigLoad(Box), /// Names the offending entry: serde used to point at the file and line, /// and the path is the equivalent locator now that the schema layer no /// longer resolves the spelling. #[error("Invalid compiler configuration for '{path}': {source}")] CompilerSpelling { path: String, #[source] source: SpellingError, }, } mod impls { use super::ConfigurationError; use super::execution; use super::execution::DynError; use crate::args::BuildCommand; use crate::environment; use crate::output::{ AtomicFile, ExecutionEventDatabase, SerializationFormat, WriterCreationError, WriterError, }; use crate::{args, config, output, parse_sh}; use intercept_supervisor::CollectorOnTcp; use intercept_supervisor::SuperviseError; use semantic::CompilerHints; use std::collections::HashMap; use std::io::IsTerminal; use std::path::PathBuf; use std::process::ExitStatus; use std::sync::Arc; use std::sync::mpsc::{Receiver, SyncSender}; use std::{fs, io}; use thiserror::Error; pub(super) struct TcpEventProducer { source: CollectorOnTcp, } impl TcpEventProducer { pub(super) fn create(source: CollectorOnTcp) -> Self { Self { source } } } impl execution::Producer for TcpEventProducer { fn produce(&self, destination: SyncSender) -> Result<(), DynError> { for execution in self.source.executions() { match execution { Ok(execution) => { // A disconnected destination means the consumer finished // (normal shutdown) or failed (already logged upstream). // Break out quietly rather than spamming an error line // per remaining intercepted execution. if destination.send(execution).is_err() { log::debug!("Consumer channel closed; stopping execution forwarding"); break; } } Err(error) => { log::warn!("cannot receive an intercepted command: {error}"); } } } Ok(()) } } impl execution::Cancellable for TcpEventProducer { fn cancel(&self) -> Result<(), DynError> { self.source.shutdown().map_err(Into::into) } } impl execution::CancellableProducer for TcpEventProducer {} /// Fails before the run when a named input file cannot be read. fn check_input_file(path: &std::path::Path) -> Result<(), ConfigurationError> { let error = |err| ConfigurationError::InputAccess(path.to_path_buf(), err); let metadata = fs::metadata(path).map_err(error)?; if !metadata.is_file() { return Err(error(io::Error::other("not a regular file"))); } Ok(()) } /// Errors raised while reading and replaying a raw event file. #[derive(Debug, Error)] enum ReplayReadError { #[error("cannot read {0}: {1}")] Open(std::path::PathBuf, io::Error), #[error("every one of {0} event line(s) was rejected; no events to analyze")] AllRejected(usize), } /// Represents an event file reader to be event source. /// /// The event file is written by the interceptor mode and contains unprocessed /// events that can be later processed by the semantic analysis pipeline. pub(super) struct RawEventReader { path: std::path::PathBuf, } impl RawEventReader { /// Create a new raw event reader. /// /// This reader will read the intercepted events from a file in a raw format, /// or from standard input when the path is the `-` sentinel (the default: /// `semantic` is a filter and reads standard input unless a file is named). pub(super) fn create(path: &std::path::Path) -> Result { if super::is_stdio(path) { // A filter blocking on an interactive terminal is almost // always a missing producer in front of the pipe; say so // instead of waiting silently. if io::stdin().is_terminal() { log::warn!( "reading events from standard input (a terminal); \ pipe a producer in, or name an event file with --input" ); } } else { check_input_file(path)?; } Ok(Self { path: path.to_path_buf() }) } } impl execution::Producer for RawEventReader { /// Opens the event source (a file, or standard input when the path is /// `-`) and reads it through `ExecutionEventDatabase::read`, the /// single line-numbering, resilient JSON-Lines parser (see /// `docs/requirements/interception-events-format.md`): a malformed /// line does not stop parsing of the remaining lines, and its /// `Display` already carries its physical line number. /// /// Tallies non-blank lines seen (`nonempty`) against accepted ones; /// if every non-empty line was rejected, the run fails so the caller /// does not exit successfully having analyzed nothing. An entirely /// empty stream stays a success (an empty database is a valid /// output) but is reported on stderr, since it is almost always a /// plumbing mistake. fn produce(&self, destination: SyncSender) -> Result<(), DynError> { let source: Box = if super::is_stdio(&self.path) { Box::new(io::stdin().lock()) } else { Box::new( fs::File::open(&self.path) .map_err(|err| ReplayReadError::Open(self.path.clone(), err))?, ) }; let mut nonempty = 0usize; let mut accepted = 0usize; for result in ExecutionEventDatabase::read(source) { nonempty += 1; match result { Ok(execution) => { accepted += 1; if destination.send(execution).is_err() { log::debug!("Consumer channel closed; stopping execution forwarding"); break; } } Err(error) => { log::warn!("event stream: {error}"); } } } if nonempty == 0 { log::warn!("event stream: no events found in input"); } else if accepted == 0 { return Err(ReplayReadError::AllRejected(nonempty).into()); } Ok(()) } } /// Errors raised while reading and interpreting shell command text. #[derive(Debug, Error)] enum ShellScriptReadError { #[error("cannot read standard input: {0}")] ReadStdin(io::Error), #[error("cannot read {0}: {1}")] ReadFile(std::path::PathBuf, io::Error), #[error("every non-empty line was skipped; no commands parsed (see warnings above)")] AllSkipped, } /// Represents a shell text reader to be an execution source. /// /// Streams the `parse_sh` tokenizer/parser pipeline over shell command /// text (e.g. a `make -n` capture) and produces the recognized commands /// as executions, as if `bear intercept` had observed them; the semantic /// stage downstream turns them into the compilation database. pub(super) struct ShellScriptReader { path: std::path::PathBuf, working_dir: std::path::PathBuf, environment: HashMap, } impl ShellScriptReader { /// Create a new shell text reader. /// /// This reader will read the shell text from a file, or from standard /// input when the path is the `-` sentinel. The `working_dir` is the /// already-resolved initial working directory the parsed commands are /// interpreted from, and `environment` is the startup environment the /// parsed commands are interpreted against (captured once in `Context`, /// so `produce` performs no ambient environment I/O). pub(super) fn create( path: &std::path::Path, working_dir: std::path::PathBuf, environment: HashMap, ) -> Result { if !super::is_stdio(path) { check_input_file(path)?; } Ok(Self { path: path.to_path_buf(), working_dir, environment }) } } impl execution::Producer for ShellScriptReader { /// Streams the input (a file, or standard input when the path is /// `-`) through the parser and sends each recognized command down /// the channel as its line completes; memory stays bounded by the /// longest logical line. Skipped lines are logged as warnings; /// when every non-empty line was skipped, the run fails so the /// caller does not exit successfully having emitted nothing. fn produce(&self, destination: SyncSender) -> Result<(), DynError> { let input: Box = if super::is_stdio(&self.path) { Box::new(io::stdin().lock()) } else { let file = fs::File::open(&self.path) .map_err(|error| ShellScriptReadError::ReadFile(self.path.clone(), error))?; Box::new(io::BufReader::new(file)) }; let context = parse_sh::Context { working_dir: self.working_dir.clone(), environment: self.environment.clone(), }; let mut emitted = 0usize; let mut skipped = 0usize; for event in parse_sh::parse(input, &context) { match event { Err(error) => { return Err(if super::is_stdio(&self.path) { ShellScriptReadError::ReadStdin(error).into() } else { ShellScriptReadError::ReadFile(self.path.clone(), error).into() }); } Ok(parse_sh::Event::Skipped(line)) => { log::warn!("line {}: skipped ({})", line.line, line.reason); skipped += 1; } Ok(parse_sh::Event::Execution(execution)) => { emitted += 1; // Trim the environment to the build-relevant subset, // matching the shape `bear intercept` emits (see // `intercept::Execution::trim`). if destination.send(execution.trim()).is_err() { log::debug!("Consumer channel closed; stopping execution forwarding"); break; } } } } if skipped > 0 { log::warn!("parse-sh: {emitted} command(s) parsed, {skipped} line(s) skipped"); } else { log::info!("parse-sh: {emitted} command(s) parsed, {skipped} line(s) skipped"); } if emitted == 0 { if skipped > 0 { return Err(ShellScriptReadError::AllSkipped.into()); } log::warn!("parse-sh: no commands found in input"); } Ok(()) } } /// Represents a raw event writer to be used as a consumer. /// /// The raw event writer will write the intercepted events as they are observed /// without any transformation. This can be later replayed to analyze the build. pub(super) struct RawEventWriter { path: std::path::PathBuf, output: AtomicFile, destination: io::BufWriter, } impl RawEventWriter { /// Create a new raw event writer. /// /// This writer will write the intercepted events to a file in a raw format. /// Rejects the `-` sentinel: interception runs the build, and the /// build's own stdout shares that stream, so a non-atomic write here /// could split a JSON line and corrupt the output. pub(super) fn create(path: &std::path::Path) -> Result { if super::is_stdio(path) { return Err(WriterCreationError::Configuration( "cannot write events to stdout: interception shares stdout with the build \ and would corrupt the stream; write to a file instead" .to_string(), )); } Self::create_file(path) } fn create_file(path: &std::path::Path) -> Result { let (output, file) = AtomicFile::create(path)?; Ok(Self { path: path.to_path_buf(), output, destination: io::BufWriter::new(file) }) } } impl execution::Consumer for RawEventWriter { /// Using existing file format, write the intercepted executions to the output file. fn consume( self: Box, executions: Receiver, ) -> Result { let Self { path, output, destination } = *self; ExecutionEventDatabase::write(destination, executions.into_iter()) .map_err(|err| WriterError::Io(path, err))?; Ok(output) } } /// Represents a semantic event writer as a consumer. /// /// The output of this writer is a semantic analysis of the build commands /// that were intercepted. It uses the semantic interpreter to transform the /// intercepted events into semantic events and writes them to the output file /// in the specified format. pub(super) struct SemanticEventWriter { interpreter: Box, writer: output::OutputWriter, } impl SemanticEventWriter { /// Create a new semantic analysis pipeline based on the output configuration. /// /// The `output` argument contains the configuration for the output file location, /// while the `config` argument contains the configuration for the semantic analysis /// and clang compilation database formatting. The `hints` were resolved once in /// [`super::Mode::configure`]. pub(super) fn create( output: args::BuildSemantic, config: &config::Main, hints: CompilerHints, ) -> Result { let ignored: Vec = config .compilers .iter() .filter(|compiler| compiler.ignore) .map(|compiler| compiler.path.clone()) .collect(); let interpreter = semantic::interpreters::create( hints, ignored, config.format.arguments.from_response_files, config.format.arguments.from_environment, ); let writer = output::OutputWriter::try_from((&output, config))?; Ok(Self { interpreter: Box::new(interpreter), writer }) } } impl execution::Consumer for SemanticEventWriter { /// Consume the intercepted executions, transform them into semantic events, /// and write them into the target file (with the right format). fn consume( self: Box, executions: Receiver, ) -> Result { let stats = Arc::clone(self.writer.statistics()); let semantics = executions.into_iter().filter_map(|execution| match self.interpreter.recognize(execution) { semantic::RecognizeResult::Recognized(cmd) => { stats.semantic_commands_received.fetch_add(1, std::sync::atomic::Ordering::Relaxed); Some(cmd) } semantic::RecognizeResult::Ignored(_) => { stats.semantic_commands_received.fetch_add(1, std::sync::atomic::Ordering::Relaxed); None } semantic::RecognizeResult::NotRecognized(_) => None, }); Ok(self.writer.write(semantics)?) } } pub(super) struct BuildExecutor { environment: environment::BuildEnvironment, } impl BuildExecutor { /// Create a new build executor with the given environment. pub(super) fn create(environment: environment::BuildEnvironment) -> Self { Self { environment } } } impl execution::Executor for BuildExecutor { /// Execute the build command in the given environment. /// /// This will run the build command and return the exit code. fn run(&self, command: BuildCommand) -> Result { self.environment.run_build(command) } } } #[cfg(test)] mod tests { use super::*; use std::collections::HashMap; use std::path::{Path, PathBuf}; mod fixture { use super::*; use tempfile::TempDir; /// A configuration file on disk, plus the executable it names, so the /// compiler-path validation the loader performs passes and the run /// reaches the `as:` resolution. pub(super) struct ConfiguredRun { _root: TempDir, config_file: PathBuf, compiler: PathBuf, current_directory: PathBuf, } impl ConfiguredRun { /// Writes a config whose single compiler entry carries `spelling` /// and the given `ignore` flag. pub(super) fn with_compiler(spelling: &str, ignore: bool) -> Self { let root = tempfile::tempdir().expect("tempdir"); let compiler = root.path().join("my-compiler"); std::fs::write(&compiler, "#!/bin/sh\n").expect("write compiler"); let config_file = root.path().join("bear.yml"); std::fs::write( &config_file, format!( "schema: \"4.2\"\ncompilers:\n - path: {}\n as: \"{}\"\n ignore: {}\n", compiler.display(), spelling, ignore ), ) .expect("write config"); let current_directory = root.path().to_path_buf(); Self { _root: root, config_file, compiler, current_directory } } /// The configured compiler's path, as the diagnostic should name it. pub(super) fn compiler_path(&self) -> String { self.compiler.display().to_string() } pub(super) fn context(&self) -> context::Context { context::Context { current_executable: PathBuf::from("/usr/bin/bear"), current_directory: self.current_directory.clone(), environment: HashMap::new(), preload_supported: false, confstr_path: String::new(), } } /// Semantic mode reading standard input: the mode itself performs /// no work at configure time, so the outcome is the `as:` verdict. pub(super) fn arguments(&self) -> args::Arguments { args::Arguments { config: Some(self.config_file.display().to_string()), mode: args::Mode::Semantic { input: args::BuildEvents { path: PathBuf::from("-") }, output: args::BuildSemantic { path: PathBuf::from("-"), append: false }, }, } } } } /// A misspelled `as:` fails at startup, before any input is read. The /// message names the offending entry and still lists every accepted /// spelling. Ignored entries are held to the same standard: they /// contribute no hint, but a typo in one is still a configuration error. #[test] fn configure_rejects_an_unknown_compiler_as_spelling() { let cases = [("an active entry", false), ("an ignored entry", true)]; for (case, ignore) in cases { let fixture = fixture::ConfiguredRun::with_compiler("invalid_compiler_type", ignore); let sut = Mode::configure(fixture.context(), fixture.arguments()).err().map(|error| error.to_string()); let Some(message) = sut else { panic!("case: {case}, expected the configuration to be rejected"); }; assert!(message.contains(&fixture.compiler_path()), "case: {case}, message: {message}"); assert!(message.contains("unknown compiler id"), "case: {case}, message: {message}"); assert!(message.contains("invalid_compiler_type"), "case: {case}, message: {message}"); assert!(message.contains("wrapper"), "case: {case}, message: {message}"); assert!(message.contains("gcc"), "case: {case}, message: {message}"); } } #[test] fn configure_accepts_a_known_compiler_as_spelling() { let cases = [("an active entry", false), ("an ignored entry", true)]; for (case, ignore) in cases { let fixture = fixture::ConfiguredRun::with_compiler("gcc", ignore); let sut = Mode::configure(fixture.context(), fixture.arguments()); assert!(sut.is_ok(), "case: {case}, expected the configuration to be accepted"); } } /// An ignored entry resolves its spelling but contributes no hint, so /// recognition of that path falls back to the regex layer. Neither name /// matches any recognition pattern, so a verdict can only come from a hint. #[test] fn ignored_entries_contribute_no_hint() { let compilers = vec![ config::Compiler { path: PathBuf::from("/opt/toolchain/quiet-tool"), as_: Some("clang".to_string()), ignore: true, }, config::Compiler { path: PathBuf::from("/opt/toolchain/loud-tool"), as_: Some("clang".to_string()), ignore: false, }, ]; let sut = CompilerRecognizer::new_with_hints(compiler_hints(&compilers).unwrap()); assert_eq!(sut.recognize(Path::new("/opt/toolchain/quiet-tool")), None); assert!(sut.recognize(Path::new("/opt/toolchain/loud-tool")).is_some()); } } rizsotto-Bear-0c30b27/crates/bear/src/output/000077500000000000000000000000001527177720300211345ustar00rootroot00000000000000rizsotto-Bear-0c30b27/crates/bear/src/output/atomic.rs000066400000000000000000000304651527177720300227660ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later use super::formats::SerializationError; use super::{WriterCreationError, WriterError}; use std::hash::{BuildHasher, Hasher}; use std::{fs, io, path}; /// An output file that is published all at once, or not at all. /// /// Bear writes each output file of a run (the compilation database, the events file) as a /// transaction. The content goes to a temporary file. Only the coordinator of the run knows /// whether the run succeeded, so it alone decides: [`AtomicFile::commit`] publishes the file, /// dropping the value rolls it back. Rollback is the default, so every early return on an /// error leaves the previous output untouched and no temporary file behind. /// /// The temporary file is created up front, so a destination that cannot be written fails /// before the build runs. /// /// Publishing is a rename, so the previous file survives any failure before it, including /// signals that cannot be caught such as `SIGKILL`; the guarantee does not rely on signal /// handling (only the cleanup does). `rename(2)` is atomic only when source and destination /// share a filesystem, so the temporary file lives next to the output. Its name holds the /// process id and a random part, so it never collides with the output, nor with the temporary /// file of another Bear run, even one on another host or in a container that shares the /// directory. A temporary file left behind by a killed run is not cleaned up. On Windows the /// underlying `MoveFileEx`/`MOVEFILE_REPLACE_EXISTING` is not fully atomic under all /// conditions, a platform limitation Bear accepts. /// /// An output that exists and is not a regular file (`/dev/null`, a pipe) is written in place: /// a rename would replace it with a regular file. Such an output, like a stream, has nothing /// to commit. /// /// The file handle goes to the writer of the content, which is consumed by its write. So the /// handle is closed before the commit or the rollback, as Windows can not rename or remove an /// open file. pub struct AtomicFile(Option); struct Staged { temp_path: path::PathBuf, final_path: path::PathBuf, } impl AtomicFile { /// Creates the temporary file for `final_path` and returns it open for writing. pub(crate) fn create(final_path: &path::Path) -> Result<(Self, fs::File), WriterCreationError> { let error = |err| WriterCreationError::Io(final_path.to_path_buf(), err); // This is also where a directory as output fails: it can not be opened for writing. if fs::metadata(final_path).is_ok_and(|metadata| !metadata.is_file()) { let file = fs::OpenOptions::new().write(true).open(final_path).map_err(error)?; return Ok((Self::none(), file)); } let directory = match final_path.parent() { Some(parent) if !parent.as_os_str().is_empty() => parent, _ => path::Path::new("."), }; // `create_new` neither reuses nor follows a file that is already at the name, so a taken // name gets a new random part. The permissions and the error are the ones `File::create` // gives. let mut attempts = 0; loop { let mut name = std::ffi::OsString::from("."); name.push(final_path.file_name().unwrap_or_default()); name.push(format!(".{}.{:016x}.tmp", std::process::id(), random())); let temp_path = directory.join(name); match fs::OpenOptions::new().write(true).create_new(true).open(&temp_path) { Ok(file) => { let staged = Staged { temp_path, final_path: final_path.to_path_buf() }; return Ok((Self(Some(staged)), file)); } Err(err) if err.kind() == io::ErrorKind::AlreadyExists && attempts < CREATE_RETRIES => { attempts += 1; } Err(err) => return Err(error(err)), } } } /// An output with nothing to commit: a stream, or a file written in place. pub(crate) fn none() -> Self { Self(None) } /// Publishes the content by renaming the temporary file to the final path. /// /// When the rename fails, the error references the final path (the likely cause), and the /// temporary file is kept for debugging. pub(crate) fn commit(mut self) -> Result<(), WriterError> { let Some(Staged { temp_path, final_path }) = self.0.take() else { return Ok(()); }; fs::rename(&temp_path, &final_path).map_err(|err| { log::warn!("Kept the temporary file for debugging: {}", temp_path.display()); WriterError::Io(final_path, SerializationError::Io(err)) }) } } const CREATE_RETRIES: u32 = 4; /// 64 random bits from `std`: the keys of a `RandomState` are seeded by the OS, and differ for /// each instance. fn random() -> u64 { std::hash::RandomState::new().build_hasher().finish() } impl Drop for AtomicFile { fn drop(&mut self) { if let Some(staged) = self.0.take() { let _ = fs::remove_file(staged.temp_path); } } } #[cfg(test)] mod tests { use super::*; use std::io::Write; /// The names in `dir`, to spot a leftover temporary file. fn entries(dir: &path::Path) -> Vec { let mut names: Vec = fs::read_dir(dir) .unwrap() .map(|entry| entry.unwrap().file_name().to_string_lossy().into_owned()) .collect(); names.sort(); names } /// A path shaped like the temporary file of `final_path` in this process. fn other_temp_path(final_path: &path::Path) -> path::PathBuf { let name = final_path.file_name().unwrap().to_string_lossy(); final_path.with_file_name(format!(".{name}.{}.{:016x}.tmp", std::process::id(), 0)) } #[test] fn test_commit_publishes_the_content() { let dir = tempfile::tempdir().unwrap(); let final_path = dir.path().join("output.json"); let (sut, mut file) = AtomicFile::create(&final_path).unwrap(); file.write_all(b"content").unwrap(); drop(file); sut.commit().unwrap(); assert_eq!(fs::read_to_string(&final_path).unwrap(), "content"); assert_eq!(entries(dir.path()), vec!["output.json"]); } #[test] fn test_commit_replaces_the_previous_file() { let dir = tempfile::tempdir().unwrap(); let final_path = dir.path().join("output.json"); fs::write(&final_path, "previous").unwrap(); let (sut, mut file) = AtomicFile::create(&final_path).unwrap(); file.write_all(b"content").unwrap(); drop(file); sut.commit().unwrap(); assert_eq!(fs::read_to_string(&final_path).unwrap(), "content"); } #[cfg(unix)] #[test] fn test_commit_gives_the_permissions_of_a_created_file() { use std::os::unix::fs::PermissionsExt; let dir = tempfile::tempdir().unwrap(); let final_path = dir.path().join("output.json"); let expected_path = dir.path().join("expected.json"); fs::File::create(&expected_path).unwrap(); let (sut, file) = AtomicFile::create(&final_path).unwrap(); drop(file); sut.commit().unwrap(); let mode = |path: &path::Path| fs::metadata(path).unwrap().permissions().mode(); assert_eq!(mode(&final_path), mode(&expected_path)); } #[test] fn test_drop_rolls_back() { let dir = tempfile::tempdir().unwrap(); let final_path = dir.path().join("output.json"); fs::write(&final_path, "previous").unwrap(); let (sut, mut file) = AtomicFile::create(&final_path).unwrap(); file.write_all(b"content").unwrap(); drop(file); drop(sut); assert_eq!(fs::read_to_string(&final_path).unwrap(), "previous"); assert_eq!(entries(dir.path()), vec!["output.json"]); } #[test] fn test_temp_file_is_hidden_next_to_the_output() { let dir = tempfile::tempdir().unwrap(); let final_path = dir.path().join("output.json"); let (_sut, _file) = AtomicFile::create(&final_path).unwrap(); let names = entries(dir.path()); assert_eq!(names.len(), 1); let random = names[0] .strip_prefix(&format!(".output.json.{}.", std::process::id())) .and_then(|rest| rest.strip_suffix(".tmp")) .unwrap_or_else(|| panic!("unexpected name: {names:?}")); assert_eq!(random.len(), 16, "unexpected name: {names:?}"); assert!(random.chars().all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase())); } #[test] fn test_drop_removes_the_temp_file() { let dir = tempfile::tempdir().unwrap(); let final_path = dir.path().join("output.json"); let (sut, file) = AtomicFile::create(&final_path).unwrap(); drop(file); assert_eq!(entries(dir.path()).len(), 1); drop(sut); assert!(entries(dir.path()).is_empty()); } #[test] fn test_existing_temp_file_is_left_untouched() { let dir = tempfile::tempdir().unwrap(); let final_path = dir.path().join("output.json"); let other_path = other_temp_path(&final_path); fs::write(&other_path, "another run").unwrap(); let (sut, mut file) = AtomicFile::create(&final_path).unwrap(); file.write_all(b"content").unwrap(); drop(file); sut.commit().unwrap(); assert_eq!(fs::read_to_string(&other_path).unwrap(), "another run"); assert_eq!(fs::read_to_string(&final_path).unwrap(), "content"); assert_eq!(entries(dir.path()).len(), 2); } #[test] fn test_two_staged_files_for_one_output_do_not_collide() { let dir = tempfile::tempdir().unwrap(); let final_path = dir.path().join("output.json"); let (first, _first_file) = AtomicFile::create(&final_path).unwrap(); let (second, _second_file) = AtomicFile::create(&final_path).unwrap(); assert_eq!(entries(dir.path()).len(), 2); drop((first, second)); assert!(entries(dir.path()).is_empty()); } #[test] fn test_output_named_like_a_temp_file_survives_a_rollback() { let dir = tempfile::tempdir().unwrap(); let final_path = dir.path().join("output.tmp"); fs::write(&final_path, "previous").unwrap(); let (sut, file) = AtomicFile::create(&final_path).unwrap(); drop(file); drop(sut); assert_eq!(fs::read_to_string(&final_path).unwrap(), "previous"); } #[cfg(unix)] #[test] fn test_special_file_is_written_in_place() { // Only create and drop: a commit gone wrong would replace /dev/null when run as root. let final_path = path::Path::new("/dev/null"); let (sut, mut file) = AtomicFile::create(final_path).unwrap(); file.write_all(b"content").unwrap(); let staged = entries(path::Path::new("/dev")).into_iter().filter(|name| name.starts_with(".null.")).count(); drop(file); drop(sut); assert_eq!(staged, 0, "no temporary file may be staged next to /dev/null"); assert!(std::os::unix::fs::FileTypeExt::is_char_device( &fs::metadata(final_path).unwrap().file_type() )); } #[test] fn test_create_fails_on_missing_directory() { let final_path = path::Path::new("/nonexistent/directory/output.json"); let sut = AtomicFile::create(final_path); match sut { Err(WriterCreationError::Io(path, _)) => assert_eq!(path, final_path), _ => panic!("Expected WriterCreationError::Io"), } } #[test] fn test_failed_commit_keeps_the_temp_file() { let dir = tempfile::tempdir().unwrap(); let final_path = dir.path().join("output.json"); let (output, file) = AtomicFile::create(&final_path).unwrap(); let temp_path = dir.path().join(&entries(dir.path())[0]); drop(file); // Renaming a file over a non-empty directory fails even for root. fs::create_dir(&final_path).unwrap(); fs::write(final_path.join("sentinel"), "untouched").unwrap(); let sut = output.commit(); match sut { Err(WriterError::Io(path, _)) => assert_eq!(path, final_path), Ok(()) => panic!("Expected the commit to fail"), } assert!(temp_path.exists()); assert_eq!(fs::read_to_string(final_path.join("sentinel")).unwrap(), "untouched"); } } rizsotto-Bear-0c30b27/crates/bear/src/output/clang/000077500000000000000000000000001527177720300222205ustar00rootroot00000000000000rizsotto-Bear-0c30b27/crates/bear/src/output/clang/converter.rs000066400000000000000000001762001527177720300246030ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Command to compilation database entry conversion functionality. //! //! This module provides the [`CommandConverter`] which is responsible for converting //! semantic [`Command`] instances into clang compilation database [`Entry`] objects. //! The converter encapsulates format configuration and conversion logic, providing //! a clean separation between domain objects and output formatting. //! //! The conversion process handles: //! - Extracting source files from compiler command arguments //! - Building properly formatted command lines for each source file //! - Computing output files based on command arguments //! - Applying format configuration (array vs string commands, output field inclusion) //! - Filtering out commands that should not generate compilation database entries //! //! # Compilation Database Entry Generation Rules //! //! The converter applies specific rules to determine when compilation database entries //! should be generated: //! //! ## Cases that generate NO entries: //! 1. **Preprocessing-only commands**: Commands with `PassEffect::StopsAt(Preprocessing)` //! 2. **Info-only commands**: Commands with `PassEffect::InfoAndExit` (e.g., `--version`, `--help`) //! 3. **Linking-only commands**: Commands without compilation flags and no compilable source files //! 4. **Commands without source files**: Any command that has no source files to process //! //! ## Cases that generate entries: //! 1. **Compilation commands**: Commands with `PassEffect::StopsAt(Compiling)` or `PassEffect::StopsAt(Assembling)` //! 2. **Compile-and-link commands**: Commands that both compile and link in one step //! - Linking-specific flags (classified as `PassEffect::Configures(Linking)`) are filtered out from entries //! - Only compilation-relevant flags are included in the database //! //! The converter relies on semantic analysis performed by compiler interpreters to properly //! classify command-line arguments instead of checking raw flag strings. use super::Entry; use super::path_format::{ResolveFn, resolver_for}; use crate::config; use log::warn; use semantic::{Argument, ArgumentKind, Command, CompilerPass, PassEffect, SourceMode}; use std::borrow::Cow; use std::path::{Path, PathBuf}; /// A command whose working directory cannot be formatted, so none of its /// entries can be produced. #[derive(Debug)] pub struct UnresolvedDirectory { pub directory: PathBuf, pub files: Vec, pub reason: String, } /// Converts commands into compilation database entries. /// /// This converter takes format configuration during construction and uses it /// to convert commands into appropriately formatted entries. pub struct CommandConverter { format: config::EntryFormat, directory_resolver: ResolveFn, file_resolver: ResolveFn, } impl CommandConverter { /// Creates a new CommandConverter with the specified format configuration. pub fn new(format: config::Format) -> Self { Self::from_resolvers( format.entries, resolver_for(format.paths.directory), resolver_for(format.paths.file), ) } /// Constructs the converter from raw resolver functions. Production code /// goes through `new`; tests use this directly to inject deterministic /// resolvers without touching the filesystem. fn from_resolvers( format: config::EntryFormat, directory_resolver: ResolveFn, file_resolver: ResolveFn, ) -> Self { Self { format, directory_resolver, file_resolver } } /// Converts a compiler command into compilation database entries. /// /// Test helper: an unresolvable directory yields no entries. #[cfg(test)] pub fn convert(&self, cmd: &Command) -> Vec { self.try_convert(cmd).unwrap_or_default() } /// Converts a compiler command into compilation database entries. /// /// Fails when the working directory cannot be formatted; the error lists /// the source files whose entries are dropped because of it. pub fn try_convert(&self, cmd: &Command) -> Result, UnresolvedDirectory> { // Check if we should skip entry generation for this command if self.should_skip_entry_generation(cmd) { return Ok(vec![]); } let formatted_directory = match (self.directory_resolver)(&cmd.working_dir, &cmd.working_dir) { Ok(dir) => dir, Err(e) => { return Err(UnresolvedDirectory { directory: cmd.working_dir.clone(), files: Self::entry_source_files(cmd), reason: e.to_string(), }); } }; Ok(self.build_entries(cmd, &formatted_directory)) } /// The source files that would each produce an entry for `cmd`. fn entry_source_files(cmd: &Command) -> Vec { let path_updater: &dyn Fn(&Path) -> Cow = &|path: &Path| Cow::Borrowed(path); let sources = cmd .arguments .iter() .filter(|arg| matches!(arg.kind(), ArgumentKind::Source { binary: false })) .filter_map(|arg| arg.as_file(path_updater)); match cmd.source_mode { SourceMode::Combined => sources.take(1).collect(), SourceMode::PerSourceStripped | SourceMode::PerSourceFull => sources.collect(), } } fn build_entries(&self, cmd: &Command, formatted_directory: &Path) -> Vec { // Create output file if needed let output_file = self.create_output_file(formatted_directory, &cmd.arguments); match cmd.source_mode { SourceMode::PerSourceStripped => { // Create one entry per source argument (only non-binary source files), // each entry keeping only its own source (siblings stripped). cmd.arguments .iter() .enumerate() .filter(|(_, arg)| matches!(arg.kind(), ArgumentKind::Source { binary: false })) .filter_map(|(source_idx, source_arg)| { self.build_entry(cmd, source_arg, Some(source_idx), formatted_directory, &output_file) }) .collect() } SourceMode::PerSourceFull => { // Whole-module compiler (e.g. swiftc): one entry per compilable // source, but every entry keeps the complete invocation (no // sibling stripping), because each file's semantics depend on // every other source in the same invocation. cmd.arguments .iter() .filter(|arg| matches!(arg.kind(), ArgumentKind::Source { binary: false })) .filter_map(|source_arg| { self.build_entry(cmd, source_arg, None, formatted_directory, &output_file) }) .collect() } SourceMode::Combined => { // Single-translation-unit compiler (e.g. valac): the whole invocation // collapses to one entry whose `file` is the first compilable source. // All sources are kept in the arguments (no sibling stripping). let first_source = cmd .arguments .iter() .find(|arg| matches!(arg.kind(), ArgumentKind::Source { binary: false })); let Some(source_arg) = first_source else { // No compilable source; should_skip_entry_generation already // covers this, but keep the guard local to the branch. return vec![]; }; self.build_entry(cmd, source_arg, None, formatted_directory, &output_file) .into_iter() .collect() } } } /// Builds a single compilation database entry for `source_arg`. /// /// `selected` controls which sources are kept in the arguments: /// `Some(idx)` keeps only the source at that index /// (`SourceMode::PerSourceStripped`, one entry per source with siblings /// stripped). `None` keeps every source -- used both for /// `SourceMode::Combined` (one entry total) and `SourceMode::PerSourceFull` /// (one entry per source, but each keeps the full invocation). fn build_entry( &self, cmd: &Command, source_arg: &Argument, selected: Option, formatted_directory: &Path, output_file: &Option, ) -> Option { // Get and format source file let path_updater: &dyn Fn(&Path) -> Cow = &|path: &Path| Cow::Borrowed(path); let source_file_path = source_arg.as_file(path_updater)?; let formatted_source_file = self.format_source_file(formatted_directory, &source_file_path); let command_args = self.build_command_args(cmd, selected, formatted_directory); if self.format.use_array_format { Some(Entry::with_arguments( formatted_source_file, command_args, formatted_directory, output_file.as_ref(), )) } else { Some(Entry::with_command( formatted_source_file, command_args, formatted_directory, output_file.as_ref(), )) } } /// Creates the output file path if the format includes output fields. /// /// Returns `Some(output_path)` if output should be included and found, `None` otherwise. fn create_output_file(&self, formatted_directory: &Path, arguments: &[Argument]) -> Option { if !self.format.include_output_field { return None; } let path_updater: &dyn Fn(&Path) -> Cow = &|path: &Path| Cow::Borrowed(path); let output_path = arguments .iter() .filter(|arg| matches!(arg.kind(), ArgumentKind::Output)) .nth(0) .and_then(|arg| arg.as_file(path_updater))?; match (self.file_resolver)(formatted_directory, &output_path) { Ok(formatted_path) => Some(formatted_path), Err(e) => { warn!("Failed to format output file path {}: {}", output_path.display(), e); Some(output_path) } } } /// Formats a source file path. /// /// Returns the formatted path, falling back to the original path on error. fn format_source_file(&self, formatted_directory: &Path, source_file_path: &Path) -> PathBuf { match (self.file_resolver)(formatted_directory, source_file_path) { Ok(formatted_path) => formatted_path, Err(e) => { warn!("Failed to format source file path {}: {}", source_file_path.display(), e); source_file_path.to_path_buf() } } } /// Builds command arguments for an entry. /// /// This method constructs the command arguments list that includes the executable, /// all non-source arguments, and the selected source file(s). /// It ensures that source files are placed in the correct position relative to output arguments. /// /// `selected` is `Some(idx)` for `SourceMode::PerSourceStripped` (keep only /// the source at `idx`, strip the siblings) or `None` for /// `SourceMode::Combined`/`SourceMode::PerSourceFull` (keep every source). fn build_command_args( &self, cmd: &Command, selected: Option, formatted_directory: &Path, ) -> Vec { let mut command_args = vec![]; // Every path goes through the resolver, the same one the `output` field // uses, so an entry cannot disagree with itself about a path. let format_path: &dyn Fn(&Path) -> Cow = &|path: &Path| Cow::Owned(self.format_source_file(formatted_directory, path)); for (idx, arg) in cmd.arguments.iter().enumerate() { // For separable compilers, skip the other source arguments (keep only // the one we are building for). For combined compilers (`None`), keep all. if let Some(source_arg_idx) = selected && matches!(arg.kind(), ArgumentKind::Source { .. }) && idx != source_arg_idx { continue; } // Filter out linking-specific arguments for compilation database entries if matches!( arg.kind(), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking)) | ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Linking)) | ArgumentKind::Other(PassEffect::PassThrough) ) { continue; } // The compiler is recorded as the executable Bear observed, not as // the token the command line carried; everything else spells itself. match arg.kind() { ArgumentKind::Compiler => match cmd.executable.to_str() { Some(exe_str) => command_args.push(exe_str.to_string()), None => command_args.extend(arg.as_arguments(format_path)), }, _ => command_args.extend(arg.as_arguments(format_path)), } } command_args } /// Returns arguments of a specific kind from the command. /// /// This method filters arguments by their kind and returns their values as strings. /// For `ArgumentKind::Source`, this matches any source regardless of the `binary` flag. fn find_arguments_by_kind(cmd: &Command, kind: ArgumentKind) -> impl Iterator { cmd.arguments.iter().filter(move |arg| { match (arg.kind(), kind) { // For Source, match any source regardless of binary flag (ArgumentKind::Source { .. }, ArgumentKind::Source { .. }) => true, // For other kinds, use exact equality (a, b) => a == b, } }) } /// Determines if we should skip generating compilation database entries for a command. /// /// Returns true if the command should not generate entries for any of these reasons: /// 1. Preprocessing-only commands (`PassEffect::StopsAt(Preprocessing)`) /// 2. Info-only commands (`PassEffect::InfoAndExit`) /// 3. Commands without source files /// 4. Linking-only commands (no compilation flags and has source files) fn should_skip_entry_generation(&self, cmd: &Command) -> bool { // Check if this is an info-only command (e.g., --version, --help) if self.is_info_only(cmd) { return true; } // Check if this is a preprocessing-only command (e.g., -E) if self.is_preprocessing_only(cmd) { return true; } // Find all source arguments (using binary: false as a placeholder, find_arguments_by_kind matches any source) let source_arguments = Self::find_arguments_by_kind(cmd, ArgumentKind::Source { binary: false }) .collect::>(); // If no source files found, skip entry generation if source_arguments.is_empty() { return true; } // Check if this is a linking-only command if self.is_linking_only(cmd) { return true; } false } /// Determines if a compiler command is preprocessing-only. /// /// A command is considered preprocessing-only if it has a `PassEffect::StopsAt(Preprocessing)` flag. /// This is the `-E` flag which explicitly stops the compiler after preprocessing. fn is_preprocessing_only(&self, cmd: &Command) -> bool { cmd.arguments.iter().any(|arg| { matches!(arg.kind(), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing))) }) } /// Determines if a compiler command is info-only. /// /// A command is considered info-only if it contains arguments /// classified as `PassEffect::InfoAndExit` by the semantic analysis. /// These commands typically display information and don't perform compilation. fn is_info_only(&self, cmd: &Command) -> bool { cmd.arguments.iter().any(|arg| matches!(arg.kind(), ArgumentKind::Other(PassEffect::InfoAndExit))) } /// Determines if a compiler command is linking-only. /// /// A command is considered linking-only if: /// 1. It does NOT have a `PassEffect::StopsAt(Compiling)` or `PassEffect::StopsAt(Assembling)` flag /// 2. AND it has no compilable source files (only object files, libraries, etc.) /// /// This typically happens when linking pre-compiled object files or libraries. /// /// The `binary` flag on `ArgumentKind::Source` is set during semantic analysis /// by the interpreter, so we can simply check it here. fn is_linking_only(&self, cmd: &Command) -> bool { // Check if the command has a flag that stops before linking (-c or -S) let stops_before_linking = cmd.arguments.iter().any(|arg| { matches!( arg.kind(), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling)) | ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Assembling)) ) }); // If there's a -c or -S flag, it's not linking-only if stops_before_linking { return false; } // Check if there are any compilable source files (not binary files) // The binary flag is set by the interpreter during semantic analysis let has_compilable_sources = cmd.arguments.iter().any(|arg| matches!(arg.kind(), ArgumentKind::Source { binary: false })); // If no -c/-S flag and no compilable sources, it's linking-only !has_compilable_sources } } #[cfg(test)] mod tests { use super::super::path_format::FormatError; use super::*; use crate::config::{ArgumentsFormat, EntryFormat, Format, PathFormat, PathResolver}; use semantic::{ArgumentKind, Command, CompilerPass, PassEffect}; use std::ffi::OsStr; use std::io; fn resolver_identity(_base: &Path, path: &Path) -> Result { Ok(path.to_path_buf()) } fn resolver_always_fails(_base: &Path, _path: &Path) -> Result { Err(FormatError::PathCanonicalize(io::Error::other("test injected"))) } fn resolver_fail_for_object_files(_base: &Path, path: &Path) -> Result { if path.extension() == Some(OsStr::new("o")) { Err(FormatError::PathCanonicalize(io::Error::other("test injected"))) } else { Ok(path.to_path_buf()) } } #[test] fn test_compiler_command_to_entries_single_source() { let command = Command::from_strings( "/home/user", "/usr/bin/gcc", vec![ (ArgumentKind::Compiler, vec!["/usr/bin/gcc"]), (ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling)), vec!["-c"]), (ArgumentKind::Other(PassEffect::None), vec!["-Wall"]), (ArgumentKind::Source { binary: false }, vec!["main.c"]), (ArgumentKind::Output, vec!["-o", "main.o"]), ], ); let sut = { let format = Format { paths: PathFormat::default(), entries: EntryFormat::default(), arguments: ArgumentsFormat::default(), }; CommandConverter::new(format) }; let result = sut.convert(&command); let expected = vec![Entry::from_arguments_str( "main.c", vec!["/usr/bin/gcc", "-c", "-Wall", "main.c", "-o", "main.o"], "/home/user", Some("main.o"), )]; assert_eq!(result, expected); } #[test] fn test_compiler_command_to_entries_multiple_sources() { let command = Command::from_strings( "/home/user", "/usr/bin/g++", vec![ (ArgumentKind::Compiler, vec!["/usr/bin/g++"]), (ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling)), vec!["-c"]), (ArgumentKind::Source { binary: false }, vec!["file1.cpp"]), (ArgumentKind::Source { binary: false }, vec!["file2.cpp"]), ], ); let sut = { let format = Format { paths: PathFormat::default(), entries: EntryFormat::default(), arguments: ArgumentsFormat::default(), }; CommandConverter::new(format) }; let result = sut.convert(&command); let expected = vec![ Entry::from_arguments_str( "file1.cpp", vec!["/usr/bin/g++", "-c", "file1.cpp"], "/home/user", None, ), Entry::from_arguments_str( "file2.cpp", vec!["/usr/bin/g++", "-c", "file2.cpp"], "/home/user", None, ), ]; assert_eq!(result, expected); } #[test] fn test_compiler_command_to_entries_no_sources() { let command = Command::from_strings( "/home/user", "gcc", vec![(ArgumentKind::Other(PassEffect::InfoAndExit), vec!["--version"])], ); let sut = { let format = Format { paths: PathFormat::default(), entries: EntryFormat::default(), arguments: ArgumentsFormat::default(), }; CommandConverter::new(format) }; let result = sut.convert(&command); let expected: Vec = vec![]; assert_eq!(result, expected); } #[test] fn test_to_entries_command_field_as_string() { let command = Command::from_strings( "/home/user", "/usr/bin/gcc", vec![ (ArgumentKind::Compiler, vec!["/usr/bin/gcc"]), (ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling)), vec!["-c"]), (ArgumentKind::Source { binary: false }, vec!["main.c"]), (ArgumentKind::Output, vec!["-o", "main.o"]), ], ); let sut = { let format = Format { paths: PathFormat::default(), entries: EntryFormat { include_output_field: true, use_array_format: false }, arguments: ArgumentsFormat::default(), }; CommandConverter::new(format) }; let result = sut.convert(&command); let expected = vec![Entry::from_command_str( "main.c", "/usr/bin/gcc -c main.c -o main.o", "/home/user", Some("main.o"), )]; assert_eq!(result, expected); } #[test] fn test_to_entries_without_output_field() { let command = Command::from_strings( "/home/user", "/usr/bin/gcc", vec![ (ArgumentKind::Compiler, vec!["/usr/bin/gcc"]), (ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling)), vec!["-c"]), (ArgumentKind::Source { binary: false }, vec!["main.c"]), (ArgumentKind::Output, vec!["-o", "main.o"]), ], ); let sut = { let format = Format { paths: PathFormat::default(), entries: EntryFormat { use_array_format: true, include_output_field: false }, arguments: ArgumentsFormat::default(), }; CommandConverter::new(format) }; let result = sut.convert(&command); let expected = vec![Entry::from_arguments_str( "main.c", vec!["/usr/bin/gcc", "-c", "main.c", "-o", "main.o"], "/home/user", None, )]; assert_eq!(result, expected); } // Requirements: output-compilation-entries #[test] fn test_output_field_absent_when_invocation_has_no_output_flag() { // Output field enabled, but the invocation carries no -o flag: the // entry must omit the field rather than invent a value. let command = Command::from_strings( "/home/user", "/usr/bin/gcc", vec![ (ArgumentKind::Compiler, vec!["/usr/bin/gcc"]), (ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling)), vec!["-c"]), (ArgumentKind::Source { binary: false }, vec!["main.c"]), ], ); let sut = { let format = Format { paths: PathFormat::default(), entries: EntryFormat { use_array_format: true, include_output_field: true }, arguments: ArgumentsFormat::default(), }; CommandConverter::new(format) }; let result = sut.convert(&command); assert_eq!(result.len(), 1); assert_eq!(result[0].output, None, "entry must have no output field"); } #[test] fn test_command_converter_public_api() { // Test that CommandConverter can be used as a public API let sut = { let format = Format { paths: PathFormat::default(), entries: EntryFormat { use_array_format: true, include_output_field: false }, arguments: ArgumentsFormat::default(), }; CommandConverter::new(format) }; let command = Command::from_strings( "/home/user", "/usr/bin/gcc", vec![ (ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling)), vec!["-c"]), (ArgumentKind::Source { binary: false }, vec!["test.c"]), ], ); let result = sut.convert(&command); assert_eq!(result.len(), 1); // Verify the entry is valid using the public API assert!(result[0].validate().is_ok()); } #[test] fn test_directory_format_failure_yields_no_entries() { let sut = CommandConverter::from_resolvers( EntryFormat::default(), resolver_always_fails, resolver_identity, ); let cmd = Command::from_strings( "/home/user", "/usr/bin/gcc", vec![(ArgumentKind::Source { binary: false }, vec!["main.c"])], ); assert!(sut.convert(&cmd).is_empty()); } #[test] fn test_file_format_failure_falls_back_to_original_path() { let sut = CommandConverter::from_resolvers( EntryFormat::default(), resolver_identity, resolver_always_fails, ); let cmd = Command::from_strings( "/home/user", "/usr/bin/gcc", vec![(ArgumentKind::Source { binary: false }, vec!["nonexistent.c"])], ); let entries = sut.convert(&cmd); assert_eq!(entries.len(), 1); assert_eq!(entries[0].directory, PathBuf::from("/home/user")); assert_eq!(entries[0].file, PathBuf::from("nonexistent.c")); } #[test] fn test_output_format_failure_falls_back_only_for_output() { let sut = CommandConverter::from_resolvers( EntryFormat { include_output_field: true, use_array_format: true }, resolver_identity, resolver_fail_for_object_files, ); let cmd = Command::from_strings( "/home/user", "/usr/bin/gcc", vec![ (ArgumentKind::Source { binary: false }, vec!["main.c"]), (ArgumentKind::Output, vec!["-o", "main.o"]), ], ); let entries = sut.convert(&cmd); assert_eq!(entries.len(), 1); assert_eq!(entries[0].file, PathBuf::from("main.c")); assert_eq!(entries[0].output, Some(PathBuf::from("main.o"))); } // Requirements: output-compilation-entries #[test] fn test_preprocessing_only_command_no_entries() { let sut = { let format = Format::default(); CommandConverter::new(format) }; let command = Command::from_strings( "/home/user", "gcc", vec![ (ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing)), vec!["-E"]), (ArgumentKind::Source { binary: false }, vec!["main.c"]), ], ); let result = sut.convert(&command); assert_eq!(result.len(), 0); } /// Runs the production compiler recognition over an `executable` /// invocation and converts the result with the default format. The /// dependency-generation tests below must exercise the real flag /// classification (not a hand-written one), because entry suppression is /// decided from it. fn recognize_and_convert(executable: &str, args: Vec<&str>) -> Vec { use semantic::{CompilerHints, Interpreter, RecognizeResult}; let execution = intercept::Execution::from_strings( executable, args, "/home/user", std::collections::HashMap::new(), ); // The same wiring the driver builds: no hints, nothing ignored, and // the default argument handling. let interpreter = semantic::interpreters::create(CompilerHints::new(), vec![], false, true); let RecognizeResult::Recognized(command) = interpreter.recognize(execution) else { panic!("{executable} invocation must be recognized as a compiler command"); }; CommandConverter::new(Format::default()).convert(&command) } // Requirements: output-compilation-entries #[test] fn test_dependency_generation_only_command_no_entries() { // -M and -MM imply -E: the compiler only emits a dependency rule, // nothing is compiled, so no entry may be produced. for flag in ["-M", "-MM"] { let sut = recognize_and_convert("/usr/bin/gcc", vec!["gcc", flag, "main.c"]); assert!(sut.is_empty(), "gcc {flag} main.c must produce no entries, got: {sut:?}"); } } // Requirements: output-compilation-entries #[test] fn test_dependency_side_effect_compile_produces_entry() { // -MD emits the dependency file as a side effect of a real compile // step, so the invocation still produces its entry. let sut = recognize_and_convert("/usr/bin/gcc", vec!["gcc", "-MD", "-c", "main.c"]); assert_eq!(sut.len(), 1, "gcc -MD -c main.c must produce one entry, got: {sut:?}"); assert_eq!(sut[0].file, PathBuf::from("main.c")); } // Requirements: output-compilation-entries #[test] fn test_nasm_dependency_generation_only_command_no_entries() { // NASM's -M and -MG emit Makefile dependencies and stop: nothing is // assembled, so no entry may be produced. for flag in ["-M", "-MG"] { let sut = recognize_and_convert("/usr/bin/nasm", vec!["nasm", flag, "hello.asm"]); assert!(sut.is_empty(), "nasm {flag} hello.asm must produce no entries, got: {sut:?}"); } } // Requirements: output-compilation-entries #[test] fn test_nasm_dependency_side_effect_assembly_produces_entry() { // NASM's -MD generates the dependency file as a side effect of a // real assembly step, so the invocation still produces its entry. let sut = recognize_and_convert( "/usr/bin/nasm", vec!["nasm", "-MD", "hello.d", "-f", "elf64", "-o", "hello.o", "hello.asm"], ); assert_eq!(sut.len(), 1, "nasm -MD ... hello.asm must produce one entry, got: {sut:?}"); assert_eq!(sut[0].file, PathBuf::from("hello.asm")); } #[test] fn test_linking_only_command_no_entries() { let sut = { let format = Format::default(); CommandConverter::new(format) }; // Linking object files (no -c flag, object file inputs) let command = Command::from_strings( "/home/user", "gcc", vec![ (ArgumentKind::Source { binary: true }, vec!["main.o"]), (ArgumentKind::Source { binary: true }, vec!["lib.o"]), (ArgumentKind::Output, vec!["-o", "program"]), (ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking)), vec!["-L/usr/lib"]), ], ); let result = sut.convert(&command); assert_eq!(result.len(), 0); } #[test] fn test_compile_only_command_generates_entries() { let sut = { let format = Format::default(); CommandConverter::new(format) }; let command = Command::from_strings( "/home/user", "gcc", vec![ (ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling)), vec!["-c"]), (ArgumentKind::Source { binary: false }, vec!["main.c"]), (ArgumentKind::Output, vec!["-o", "main.o"]), ], ); let result = sut.convert(&command); assert_eq!(result.len(), 1); assert_eq!(result[0].file, PathBuf::from("main.c")); } #[test] fn test_compile_and_link_filters_linking_flags() { let sut = { let format = Format { paths: PathFormat::default(), entries: EntryFormat { use_array_format: true, include_output_field: false }, arguments: ArgumentsFormat::default(), }; CommandConverter::new(format) }; let command = Command::from_strings( "/home/user", "gcc", vec![ (ArgumentKind::Source { binary: false }, vec!["main.c"]), (ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking)), vec!["-L/usr/lib"]), (ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking)), vec!["-lmath"]), (ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling)), vec!["-Wall"]), ], ); let result = sut.convert(&command); assert_eq!(result.len(), 1); let entry = &result[0]; assert_eq!(entry.file, PathBuf::from("main.c")); // Check that linking flags are filtered out let args_str = entry.arguments.join(" "); assert!(args_str.contains("-Wall")); // Compile flag should be present assert!(!args_str.contains("-L/usr/lib")); // Link flag should be filtered assert!(!args_str.contains("-lmath")); // Link flag should be filtered } #[test] fn test_info_command_no_entries() { let sut = { let format = Format::default(); CommandConverter::new(format) }; let command = Command::from_strings( "/home/user", "gcc", vec![(ArgumentKind::Other(PassEffect::InfoAndExit), vec!["--version"])], ); let result = sut.convert(&command); assert_eq!(result.len(), 0); } #[test] fn test_realistic_source_file_detection() { let sut = { let format = Format::default(); CommandConverter::new(format) }; // Test compile-and-link with real source files (should generate entries) let command = Command::from_strings( "/home/user", "gcc", vec![ (ArgumentKind::Source { binary: false }, vec!["main.c"]), // Real source file (ArgumentKind::Source { binary: false }, vec!["utils.cpp"]), // Real source file (ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking)), vec!["-lm"]), ], ); let result = sut.convert(&command); assert_eq!(result.len(), 2); // Should generate entries for both source files // Test linking with object files only (should not generate entries) let command = Command::from_strings( "/home/user", "gcc", vec![ (ArgumentKind::Source { binary: true }, vec!["main.o"]), // Object file (ArgumentKind::Source { binary: true }, vec!["utils.a"]), // Static library (ArgumentKind::Output, vec!["-o", "program"]), ], ); let result = sut.convert(&command); assert_eq!(result.len(), 0); // Should not generate entries for object files } #[test] fn test_semantic_classification_vs_raw_flags() { let sut = { let format = Format::default(); CommandConverter::new(format) }; // Test that we rely on semantic classification, not raw flag strings // This tests a hypothetical case where a flag might look like "-E" but // is classified differently by semantic analysis // Test preprocessing flag properly classified let command = Command::from_strings( "/home/user", "gcc", vec![ ( ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing)), vec!["-E"], // Semantically classified as preprocessing ), (ArgumentKind::Source { binary: false }, vec!["main.c"]), ], ); let result = sut.convert(&command); assert_eq!(result.len(), 0); // Should skip preprocessing commands // Test compilation flag properly classified let command = Command::from_strings( "/home/user", "gcc", vec![ ( ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling)), vec!["-c"], // Semantically classified as compiling ), (ArgumentKind::Source { binary: false }, vec!["main.c"]), ], ); let result = sut.convert(&command); assert_eq!(result.len(), 1); // Should generate entry for compilation // Test info flag properly classified let command = Command::from_strings( "/home/user", "gcc", vec![( ArgumentKind::Other(PassEffect::InfoAndExit), vec!["--version"], // Semantically classified as info )], ); let result = sut.convert(&command); assert_eq!(result.len(), 0); // Should skip info commands // Test that linking flags are filtered out (not raw string matching) let command = Command::from_strings( "/home/user", "gcc", vec![ (ArgumentKind::Source { binary: false }, vec!["main.c"]), ( ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking)), vec!["-lmath"], // Semantically classified as linking ), ( ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling)), vec!["-O2"], // Compilation flag ), ], ); let result = sut.convert(&command); assert_eq!(result.len(), 1); // Verify linking flag is filtered out while compilation flag remains let args_str = result[0].arguments.join(" "); assert!(!args_str.contains("-lmath")); // Linking flag filtered assert!(args_str.contains("-O2")); // Compilation flag preserved } #[test] fn test_consistent_formatting_methods() { let sut = { let format = Format { paths: PathFormat::default(), entries: EntryFormat { use_array_format: true, include_output_field: true }, arguments: ArgumentsFormat::default(), }; CommandConverter::new(format) }; // Test that all three formatting methods work consistently let command = Command::from_strings( "/home/user", "gcc", vec![ (ArgumentKind::Compiler, vec!["gcc"]), (ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling)), vec!["-c"]), (ArgumentKind::Source { binary: false }, vec!["main.c"]), (ArgumentKind::Output, vec!["-o", "main.o"]), ], ); let result = sut.convert(&command); assert_eq!(result.len(), 1); let entry = &result[0]; // Verify all three formatting methods produced results: // 1. Working directory formatting assert_eq!(entry.directory, PathBuf::from("/home/user")); // 2. Source file formatting assert_eq!(entry.file, PathBuf::from("main.c")); // 3. Output file formatting assert_eq!(entry.output, Some(PathBuf::from("main.o"))); // Verify the command includes the formatted paths assert!(entry.arguments.contains(&"gcc".to_string())); assert!(entry.arguments.contains(&"-c".to_string())); assert!(entry.arguments.contains(&"main.c".to_string())); assert!(entry.arguments.contains(&"-o".to_string())); assert!(entry.arguments.contains(&"main.o".to_string())); } #[test] fn test_preprocessing_and_compilation_flags_generates_entries() { let sut = { let format = Format::default(); CommandConverter::new(format) }; // Test command with both preprocessing flags (-D) and compilation flags (-c) let command = Command::from_strings( "/home/user", "gcc", vec![ (ArgumentKind::Compiler, vec!["gcc"]), ( ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing)), vec!["-DWRAPPER_FLAG"], ), (ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling)), vec!["-c"]), (ArgumentKind::Source { binary: false }, vec!["test.c"]), ], ); let result = sut.convert(&command); // Should generate entries because it has compilation flags, not just preprocessing assert_eq!(result.len(), 1); let entry = &result[0]; assert_eq!(entry.file, PathBuf::from("test.c")); assert_eq!(entry.directory, PathBuf::from("/home/user")); // Verify the arguments include both preprocessing and compilation flags assert!(entry.arguments.contains(&"gcc".to_string())); assert!(entry.arguments.contains(&"-DWRAPPER_FLAG".to_string())); assert!(entry.arguments.contains(&"-c".to_string())); assert!(entry.arguments.contains(&"test.c".to_string())); } #[test] fn test_preprocessing_only_with_defines_no_entries() { let sut = { let format = Format::default(); CommandConverter::new(format) }; // Test command with only preprocessing flags (no -c flag) let command = Command::from_strings( "/home/user", "gcc", vec![ (ArgumentKind::Compiler, vec!["gcc"]), (ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing)), vec!["-E"]), ( ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing)), vec!["-DSOME_DEFINE"], ), (ArgumentKind::Source { binary: false }, vec!["test.c"]), ], ); let result = sut.convert(&command); // Should NOT generate entries because it's preprocessing-only (has -E flag) assert_eq!(result.len(), 0); } // --- Tests for non-trivial PathFormat configurations (end-to-end through CommandConverter::new) --- #[test] fn test_absolute_path_format_makes_paths_absolute() { let temp_dir = tempfile::tempdir().unwrap(); let temp_path = temp_dir.path().canonicalize().unwrap(); let working_dir = temp_path.to_str().unwrap(); // Create actual source file so canonicalize can work if needed std::fs::write(temp_path.join("main.c"), "").unwrap(); let sut = { let format = Format { paths: PathFormat { directory: PathResolver::Absolute, file: PathResolver::Absolute }, entries: EntryFormat::default(), arguments: ArgumentsFormat::default(), }; CommandConverter::new(format) }; let command = Command::from_strings( working_dir, "/usr/bin/gcc", vec![ (ArgumentKind::Compiler, vec!["/usr/bin/gcc"]), (ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling)), vec!["-c"]), (ArgumentKind::Source { binary: false }, vec!["main.c"]), (ArgumentKind::Output, vec!["-o", "main.o"]), ], ); let entries = sut.convert(&command); assert_eq!(entries.len(), 1); let entry = &entries[0]; // Directory should be absolute assert!(entry.directory.is_absolute(), "directory should be absolute: {:?}", entry.directory); // File should be absolute assert!(entry.file.is_absolute(), "file should be absolute: {:?}", entry.file); } #[test] fn test_relative_path_format_makes_paths_relative() { let temp_dir = tempfile::tempdir().unwrap(); let temp_path = temp_dir.path().canonicalize().unwrap(); let working_dir = temp_path.to_str().unwrap(); let source_file = temp_path.join("main.c"); std::fs::write(&source_file, "").unwrap(); let sut = { let format = Format { paths: PathFormat { directory: PathResolver::Relative, file: PathResolver::Relative }, entries: EntryFormat::default(), arguments: ArgumentsFormat::default(), }; CommandConverter::new(format) }; // Use a relative source file (relative to working dir) let command = Command::from_strings( working_dir, "/usr/bin/gcc", vec![ (ArgumentKind::Compiler, vec!["/usr/bin/gcc"]), (ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling)), vec!["-c"]), (ArgumentKind::Source { binary: false }, vec!["main.c"]), ], ); let entries = sut.convert(&command); assert_eq!(entries.len(), 1); let entry = &entries[0]; // Directory should be relative (the working dir resolved relative to itself is ".") assert!(!entry.directory.is_absolute(), "directory should be relative: {:?}", entry.directory); // File should be relative assert!(!entry.file.is_absolute(), "file should be relative: {:?}", entry.file); } #[test] fn test_canonical_path_format_resolves_dotdot() { let temp_dir = tempfile::tempdir().unwrap(); let temp_path = temp_dir.path().canonicalize().unwrap(); // Create subdirectory and file let sub_dir = temp_path.join("src"); std::fs::create_dir(&sub_dir).unwrap(); let source_file = sub_dir.join("main.c"); std::fs::write(&source_file, "").unwrap(); // Use a path with .. components let working_dir_with_dotdot = sub_dir.join("..").join("src"); let working_dir_str = working_dir_with_dotdot.to_str().unwrap(); let sut = { let format = Format { paths: PathFormat { directory: PathResolver::Canonical, file: PathResolver::Canonical }, entries: EntryFormat::default(), arguments: ArgumentsFormat::default(), }; CommandConverter::new(format) }; let command = Command::from_strings( working_dir_str, "/usr/bin/gcc", vec![ (ArgumentKind::Compiler, vec!["/usr/bin/gcc"]), (ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling)), vec!["-c"]), (ArgumentKind::Source { binary: false }, vec![source_file.to_str().unwrap()]), ], ); let entries = sut.convert(&command); assert_eq!(entries.len(), 1); let entry = &entries[0]; // Canonical paths should not contain ".." let dir_str = entry.directory.to_string_lossy(); assert!(!dir_str.contains(".."), "canonical directory should not contain '..': {}", dir_str); let file_str = entry.file.to_string_lossy(); assert!(!file_str.contains(".."), "canonical file should not contain '..': {}", file_str); } #[test] fn test_mixed_path_format_absolute_directory_relative_file() { let temp_dir = tempfile::tempdir().unwrap(); let temp_path = temp_dir.path().canonicalize().unwrap(); let working_dir = temp_path.to_str().unwrap(); std::fs::write(temp_path.join("main.c"), "").unwrap(); let sut = { let format = Format { paths: PathFormat { directory: PathResolver::Absolute, file: PathResolver::Relative }, entries: EntryFormat::default(), arguments: ArgumentsFormat::default(), }; CommandConverter::new(format) }; let command = Command::from_strings( working_dir, "/usr/bin/gcc", vec![ (ArgumentKind::Compiler, vec!["/usr/bin/gcc"]), (ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling)), vec!["-c"]), (ArgumentKind::Source { binary: false }, vec!["main.c"]), ], ); let entries = sut.convert(&command); assert_eq!(entries.len(), 1); let entry = &entries[0]; // Directory should be absolute assert!(entry.directory.is_absolute(), "directory should be absolute: {:?}", entry.directory); // File should be relative assert!(!entry.file.is_absolute(), "file should be relative: {:?}", entry.file); } /// Builds a `Command` with `source_mode` set to `Combined`, mirroring what /// the valac interpreter produces. `from_strings` always sets /// `PerSourceStripped`, so combined-path tests flip it here. fn combined(mut cmd: Command) -> Command { cmd.source_mode = semantic::SourceMode::Combined; cmd } /// Builds a `Command` with `source_mode` set to `PerSourceFull`, mirroring /// what the swiftc interpreter produces. `from_strings` always sets /// `PerSourceStripped`, so whole-module tests flip it here. fn per_source_full(mut cmd: Command) -> Command { cmd.source_mode = semantic::SourceMode::PerSourceFull; cmd } // Requirements: output-compilation-entries #[test] fn test_combined_sources_produce_single_entry() { let sut = { let format = Format { paths: PathFormat::default(), entries: EntryFormat { use_array_format: true, include_output_field: false }, arguments: ArgumentsFormat::default(), }; CommandConverter::new(format) }; // valac-style: two sources compiled as one translation unit, producing a // library/binding. `--library`/`--vapi` classify as `none`, not stripped. let command = combined(Command::from_strings( "/home/user", "valac", vec![ (ArgumentKind::Compiler, vec!["valac"]), (ArgumentKind::Other(PassEffect::None), vec!["--library", "foo"]), (ArgumentKind::Source { binary: false }, vec!["a.vala"]), (ArgumentKind::Source { binary: false }, vec!["b.vala"]), (ArgumentKind::Source { binary: false }, vec!["c.vala"]), ], )); let result = sut.convert(&command); // Exactly one entry, file == first source, all three sources retained. assert_eq!(result.len(), 1); let entry = &result[0]; assert_eq!(entry.file, PathBuf::from("a.vala")); let args_str = entry.arguments.join(" "); assert!(args_str.contains("a.vala"), "first source must be present: {}", args_str); assert!(args_str.contains("b.vala"), "sibling source must be retained: {}", args_str); assert!(args_str.contains("c.vala"), "sibling source must be retained: {}", args_str); assert!(args_str.contains("--library"), "non-link flag must be retained: {}", args_str); assert!(args_str.contains("foo"), "--library value must be retained: {}", args_str); } // Requirements: output-compilation-entries #[test] fn test_combined_sources_emit_command_string_form() { // vala-language-server reads only the command-string form, so guard that // a combined entry serializes its single command (with every source) when // use_array_format is false. let sut = { let format = Format { paths: PathFormat::default(), entries: EntryFormat { use_array_format: false, include_output_field: false }, arguments: ArgumentsFormat::default(), }; CommandConverter::new(format) }; let command = combined(Command::from_strings( "/home/user", "valac", vec![ (ArgumentKind::Compiler, vec!["valac"]), (ArgumentKind::Other(PassEffect::None), vec!["--library", "foo"]), (ArgumentKind::Source { binary: false }, vec!["a.vala"]), (ArgumentKind::Source { binary: false }, vec!["b.vala"]), ], )); let result = sut.convert(&command); // Exactly one entry in command-string form: `command` set, `arguments` empty. assert_eq!(result.len(), 1); let entry = &result[0]; assert_eq!(entry.file, PathBuf::from("a.vala")); assert!(entry.arguments.is_empty(), "command form must leave arguments empty"); assert!(entry.command.contains("a.vala"), "command must keep first source: {}", entry.command); assert!(entry.command.contains("b.vala"), "command must keep sibling source: {}", entry.command); assert!(entry.command.contains("--library"), "command must keep non-link flag: {}", entry.command); } // Requirements: output-compilation-entries #[test] fn test_combined_sources_strip_link_only_flags() { let sut = { let format = Format { paths: PathFormat::default(), entries: EntryFormat { use_array_format: true, include_output_field: false }, arguments: ArgumentsFormat::default(), }; CommandConverter::new(format) }; let command = combined(Command::from_strings( "/home/user", "valac", vec![ (ArgumentKind::Compiler, vec!["valac"]), (ArgumentKind::Source { binary: false }, vec!["a.vala"]), (ArgumentKind::Source { binary: false }, vec!["b.vala"]), (ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking)), vec!["-L/usr/lib"]), ], )); let result = sut.convert(&command); assert_eq!(result.len(), 1); let args_str = result[0].arguments.join(" "); assert!(args_str.contains("a.vala")); assert!(args_str.contains("b.vala")); assert!(!args_str.contains("-L/usr/lib"), "link-only flag must be stripped: {}", args_str); } // Requirements: output-compilation-entries #[test] fn test_separable_sources_remain_one_entry_per_source() { // Regression guard: the default separable path still fans out. let sut = { let format = Format::default(); CommandConverter::new(format) }; let command = Command::from_strings( "/home/user", "/usr/bin/g++", vec![ (ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling)), vec!["-c"]), (ArgumentKind::Source { binary: false }, vec!["file1.cpp"]), (ArgumentKind::Source { binary: false }, vec!["file2.cpp"]), ], ); let result = sut.convert(&command); assert_eq!(result.len(), 2); assert_eq!(result[0].file, PathBuf::from("file1.cpp")); assert_eq!(result[1].file, PathBuf::from("file2.cpp")); } // Requirements: output-compilation-entries #[test] fn test_combined_sources_skip_leading_binary_input() { let sut = { let format = Format::default(); CommandConverter::new(format) }; // A binary input appears before the first compilable source: the // representative `file` must be the source, and the binary input is not // emitted as its own entry. let command = combined(Command::from_strings( "/home/user", "valac", vec![ (ArgumentKind::Compiler, vec!["valac"]), (ArgumentKind::Source { binary: true }, vec!["prebuilt.o"]), (ArgumentKind::Source { binary: false }, vec!["a.vala"]), (ArgumentKind::Source { binary: false }, vec!["b.vala"]), ], )); let result = sut.convert(&command); // Exactly one entry: the binary input is not promoted to its own entry, // and the representative `file` is the first compilable source. assert_eq!(result.len(), 1); assert_eq!(result[0].file, PathBuf::from("a.vala")); // Combined mode keeps every input in the command, including the binary one // (no sibling stripping); only the entry count collapses to one. let args_str = result[0].arguments.join(" "); assert!(args_str.contains("prebuilt.o"), "binary input must remain in args: {}", args_str); assert!(args_str.contains("a.vala"), "first source must be present: {}", args_str); assert!(args_str.contains("b.vala"), "sibling source must be present: {}", args_str); } // Requirements: output-compilation-entries #[test] fn test_combined_sources_with_no_compilable_source_produce_no_entry() { let sut = { let format = Format::default(); CommandConverter::new(format) }; // Only binary inputs: a pure link step yields no entry, same as separable. let command = combined(Command::from_strings( "/home/user", "valac", vec![ (ArgumentKind::Compiler, vec!["valac"]), (ArgumentKind::Source { binary: true }, vec!["prebuilt.o"]), (ArgumentKind::Output, vec!["-o", "program"]), ], )); let result = sut.convert(&command); assert_eq!(result.len(), 0); } // Requirements: output-compilation-entries, recognition-compiler-names #[test] fn test_per_source_full_produces_one_entry_per_source_with_full_arguments() { let sut = { let format = Format { paths: PathFormat::default(), entries: EntryFormat { use_array_format: true, include_output_field: false }, arguments: ArgumentsFormat::default(), }; CommandConverter::new(format) }; // swiftc-style whole-module invocation: two sources compiled together, // each file's semantics depend on the whole module. let command = per_source_full(Command::from_strings( "/home/user", "swiftc", vec![ (ArgumentKind::Compiler, vec!["swiftc"]), (ArgumentKind::Other(PassEffect::None), vec!["-module-name", "App"]), (ArgumentKind::Source { binary: false }, vec!["a.swift"]), (ArgumentKind::Source { binary: false }, vec!["b.swift"]), ], )); let result = sut.convert(&command); // Two entries, one per source, each keeping the whole invocation. assert_eq!(result.len(), 2); assert_eq!(result[0].file, PathBuf::from("a.swift")); assert_eq!(result[1].file, PathBuf::from("b.swift")); for entry in &result { let args_str = entry.arguments.join(" "); assert!( args_str.contains("a.swift"), "entry for {:?} must keep a.swift: {}", entry.file, args_str ); assert!( args_str.contains("b.swift"), "entry for {:?} must keep b.swift: {}", entry.file, args_str ); assert!( args_str.contains("-module-name"), "entry for {:?} must keep non-source flags: {}", entry.file, args_str ); } } // Requirements: output-compilation-entries, recognition-compiler-names #[test] fn test_per_source_full_strips_link_only_flags() { let sut = { let format = Format { paths: PathFormat::default(), entries: EntryFormat { use_array_format: true, include_output_field: false }, arguments: ArgumentsFormat::default(), }; CommandConverter::new(format) }; let command = per_source_full(Command::from_strings( "/home/user", "swiftc", vec![ (ArgumentKind::Compiler, vec!["swiftc"]), (ArgumentKind::Source { binary: false }, vec!["a.swift"]), (ArgumentKind::Source { binary: false }, vec!["b.swift"]), (ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking)), vec!["-lm"]), ], )); let result = sut.convert(&command); assert_eq!(result.len(), 2); for entry in &result { let args_str = entry.arguments.join(" "); assert!(!args_str.contains("-lm"), "link-only flag must be stripped: {}", args_str); } } // Requirements: output-compilation-entries, recognition-compiler-names #[test] fn test_per_source_full_with_no_compilable_source_produces_no_entries() { let sut = { let format = Format::default(); CommandConverter::new(format) }; let command = per_source_full(Command::from_strings( "/home/user", "swiftc", vec![ (ArgumentKind::Compiler, vec!["swiftc"]), (ArgumentKind::Other(PassEffect::InfoAndExit), vec!["--version"]), ], )); let result = sut.convert(&command); assert_eq!(result.len(), 0); } #[test] fn test_driver_option_does_not_affect_entry_generation() { let sut = { let format = Format::default(); CommandConverter::new(format) }; // Test command with driver options like -pipe (should still generate entries) let command = Command::from_strings( "/home/user", "gcc", vec![ (ArgumentKind::Compiler, vec!["gcc"]), (ArgumentKind::Other(PassEffect::DriverOption), vec!["-pipe"]), (ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling)), vec!["-c"]), (ArgumentKind::Source { binary: false }, vec!["main.c"]), ], ); let result = sut.convert(&command); // Should generate entries - driver options don't stop compilation assert_eq!(result.len(), 1); assert_eq!(result[0].file, PathBuf::from("main.c")); // Verify -pipe is included in the command assert!(result[0].arguments.contains(&"-pipe".to_string())); } } rizsotto-Bear-0c30b27/crates/bear/src/output/clang/json.rs000066400000000000000000000213641527177720300235450ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! This module contains functions to serialize and deserialize JSON arrays. //! //! The main objective is to provide a way to serialize and deserialize //! entries from an iterator into a JSON array and vice versa. Iterators //! allow us to process large datasets without loading everything into //! memory at once. //! //! The format these methods produce is a JSON array of objects. //! It's *not* JSON lines format, which is a sequence of JSON objects //! separated by newlines. use std::io; use serde::Serializer; use serde::de::DeserializeOwned; use serde::ser::{Serialize, SerializeSeq}; /// Serialize entries from an iterator into a JSON array. /// /// The caller is responsible for ensuring entries are already valid for the /// target format. Returns a `serde_json::Error` if JSON emission itself /// fails (I/O or encoding errors). pub fn serialize_seq(writer: W, entries: impl Iterator) -> Result<(), serde_json::Error> where W: io::Write, T: Serialize, { let mut ser = serde_json::Serializer::pretty(writer); let mut seq = ser.serialize_seq(None)?; for entry in entries { seq.serialize_element(&entry)?; } seq.end()?; Ok(()) } /// Deserialize entries from a JSON array into an iterator. /// /// from https://github.com/serde-rs/json/issues/404#issuecomment-892957228 /// /// # Note /// Works well with self-delimiter-ed types, like string, objects, arrays. /// Does not work with types like simple integers, floats, etc. The problem /// is that the deserializer will not be able to distinguish between /// the end of the type, and it consumes the next byte which is relevant to /// the array parser. pub fn deserialize_seq(reader: R) -> impl Iterator> where T: DeserializeOwned, R: io::Read, { let mut reader = PeekableReader::new(reader); let mut state = State::AtStart; std::iter::from_fn(move || yield_next_obj(&mut reader, &mut state).transpose()) } // A wrapper around a reader that allows peeking at the next byte without consuming it struct PeekableReader { reader: R, peeked: Option, } impl PeekableReader { fn new(reader: R) -> Self { PeekableReader { reader, peeked: None } } fn peek(&mut self) -> Result { if self.peeked.is_none() { let mut byte = 0u8; self.reader.read_exact(std::slice::from_mut(&mut byte)).map_err(serde_json::Error::io)?; self.peeked = Some(byte); } Ok(self.peeked.expect("set by the read above when it was empty")) } fn consume(&mut self) -> Result { if let Some(byte) = self.peeked.take() { Ok(byte) } else { let mut byte = 0u8; self.reader.read_exact(std::slice::from_mut(&mut byte)).map_err(serde_json::Error::io)?; Ok(byte) } } fn peek_skipping_ws(&mut self) -> Result { loop { let byte = self.peek()?; if !byte.is_ascii_whitespace() { return Ok(byte); } self.consume()?; // Consume whitespace } } fn consume_skipping_ws(&mut self) -> Result { self.peek_skipping_ws()?; // Make sure peeked byte is not whitespace self.consume() } } // Implement Read directly for PeekableReader impl io::Read for PeekableReader { fn read(&mut self, buf: &mut [u8]) -> io::Result { if buf.is_empty() { return Ok(0); } // If we have a peeked byte, use it first if let Some(byte) = self.peeked.take() { buf[0] = byte; return Ok(1); } // Otherwise, read directly from the inner reader self.reader.read(buf) } } enum State { AtStart, AtMiddle, Finished, Failed, } fn yield_next_obj( reader: &mut PeekableReader, state: &mut State, ) -> Result, serde_json::Error> where T: DeserializeOwned, R: io::Read, { match state { State::AtStart => { let bracket = reader.consume_skipping_ws().inspect_err(|_| *state = State::Failed)?; if bracket != b'[' { *state = State::Failed; return Err(serde::de::Error::custom("expected `[`")); } // Check for empty array let next_byte = reader.peek_skipping_ws().inspect_err(|_| *state = State::Failed)?; if next_byte == b']' { reader.consume()?; // Consume the closing bracket *state = State::Finished; return Ok(None); } // Not an empty array, deserialize the first element *state = State::AtMiddle; deserialize_single(reader).map(Some) } State::AtMiddle => { // At this point we've consumed the previous value and need to check for delimiter let delimiter = reader.consume_skipping_ws().inspect_err(|_| *state = State::Failed)?; match delimiter { b',' => { reader.peek_skipping_ws().inspect_err(|_| *state = State::Failed)?; deserialize_single(reader).map(Some) } b']' => { *state = State::Finished; Ok(None) } _ => { *state = State::Failed; Err(serde::de::Error::custom("expected `,` or `]`")) } } } State::Finished | State::Failed => Ok(None), } } fn deserialize_single(reader: &mut R) -> Result where T: DeserializeOwned, R: io::Read, { let next_obj = serde_json::Deserializer::from_reader(reader).into_iter::().next(); match next_obj { Some(result) => result, None => Err(serde::de::Error::custom("premature EOF")), } } #[cfg(test)] mod tests { use super::*; use std::io::{Cursor, Seek, SeekFrom}; #[test] fn test_write_read_array() { let input: Vec = vec!["1".into(), "5".into(), "0".into(), "2".into(), "9".into()]; assert_write_and_read_results_equal(&input); } #[test] fn test_write_read_single_element_array() { let input: Vec = vec!["1".into()]; assert_write_and_read_results_equal(&input); } #[test] fn test_write_read_empty_array() { let input: Vec = vec![]; assert_write_and_read_results_equal(&input); } fn assert_write_and_read_results_equal(input: &[T]) where T: Serialize + DeserializeOwned + PartialEq + std::fmt::Debug + Clone, { // Create fake "file" let mut buffer = Cursor::new(Vec::new()); serialize_seq(&mut buffer, input.iter().cloned()).unwrap(); // Use the fake "file" as input buffer.seek(SeekFrom::Start(0)).unwrap(); let result: Vec = deserialize_seq(&mut buffer).collect::>().unwrap(); assert_eq!(result, input.to_vec()); } #[test] fn test_valid_json_reading() { let buffer = "[ 1 , 5 , 0 , 2 , 9 ]".as_bytes(); let mut cursor = Cursor::new(buffer); let result: Vec = deserialize_seq(&mut cursor).collect::>().unwrap(); assert_eq!(result, vec![1, 5, 0, 2, 9]); } #[test] fn test_invalid_json_reading() { let buffer = "[ \"key\": \"value\", ".as_bytes(); let mut cursor = Cursor::new(buffer); let mut it = deserialize_seq::>(&mut cursor); let first = it.next().unwrap(); assert!(first.is_ok()); let second = it.next().unwrap(); assert!(second.is_err()); } // Requirements: output-append #[test] fn test_truncated_json_reading_ends() { // (input, expected outcome per item: true for Ok, false for Err) let cases: [(&str, &[bool]); 8] = [ ("", &[false]), (" ", &[false]), ("[", &[false]), ("[ \"a\"", &[true, false]), ("[ \"a\" ,", &[true, false]), ("[ \"a\" , ", &[true, false]), ("[ \"a\" , \"b", &[true, false, false]), ("[ \"a", &[false, false]), ]; for (input, expected) in cases { // A bound, so a reader that never ends fails the test instead of hanging it. let sut: Vec = deserialize_seq::(input.as_bytes()) .take(expected.len() + 10) .map(|item| item.is_ok()) .collect(); assert_eq!(sut, expected, "input: {input:?}"); } } } rizsotto-Bear-0c30b27/crates/bear/src/output/clang/mod.rs000066400000000000000000000215111527177720300233450ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Clang JSON compilation database data model and conversion. //! //! This module defines the [`Entry`] type representing a single record in the //! [JSON compilation database](https://clang.llvm.org/docs/JSONCompilationDatabase.html), //! along with the [`CommandConverter`] for creating entries from semantic commands. pub mod converter; mod json; mod path_format; pub(crate) mod serialization; use shell_words; use std::path; use thiserror::Error; // Re-export types for easier access pub use converter::CommandConverter; /// Represents an entry of the compilation database. #[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)] pub struct Entry { /// The main translation unit source processed by this compilation step. /// This is used by tools as the key into the compilation database. /// There can be multiple command objects for the same file, for example if the same /// source file is compiled with different configurations. pub(crate) file: path::PathBuf, /// The compile command argv as list of strings. This should run the compilation step /// for the translation unit file. `arguments[0]` should be the executable name, such /// as `clang++`. Arguments should not be escaped, but ready to pass to `execvp()`. #[serde(skip_serializing_if = "Vec::is_empty")] #[serde(default)] pub(crate) arguments: Vec, /// The compile command as a single shell-escaped string. Arguments may be shell quoted /// and escaped following platform conventions, with ‘"’ and ‘\’ being the only special /// characters. Shell expansion is not supported. /// /// Either `arguments` or `command` is required. `arguments` is preferred, as shell /// (un)escaping is a possible source of errors. #[serde(skip_serializing_if = "String::is_empty")] #[serde(default)] pub(crate) command: String, /// The working directory of the compilation. All paths specified in the `command` or /// `file` fields must be either absolute or relative to this directory. pub(crate) directory: path::PathBuf, /// The name of the output created by this compilation step. This field is optional. /// It can be used to distinguish different processing modes of the same input file. #[serde(skip_serializing_if = "Option::is_none")] #[serde(default)] pub(crate) output: Option, } impl Entry { /// Create an Entry with the arguments field populated. pub fn with_arguments( file: impl Into, arguments: Vec, directory: impl Into, output: Option>, ) -> Self { Entry { file: file.into(), arguments, command: String::default(), directory: directory.into(), output: output.map(|o| o.into()), } } /// Create an Entry with the command field populated. pub fn with_command( file: impl Into, arguments: Vec, directory: impl Into, output: Option>, ) -> Self { Entry { file: file.into(), arguments: Vec::default(), command: shell_words::join(&arguments), directory: directory.into(), output: output.map(|o| o.into()), } } /// Semantic validation of the entry. Checking all fields for /// valid values and formats. pub fn validate(&self) -> Result<(), EntryError> { if self.file.to_string_lossy().is_empty() { return Err(EntryError::EmptyFileName); } if self.directory.to_string_lossy().is_empty() { return Err(EntryError::EmptyDirectory); } if self.command.is_empty() && self.arguments.is_empty() { return Err(EntryError::CommandOrArgumentsAreMissing); } if !self.command.is_empty() && !self.arguments.is_empty() { return Err(EntryError::CommandOrArgumentsArePresent); } if !self.command.is_empty() { shell_words::split(&self.command)?; } Ok(()) } /// Constructor method for testing purposes. #[cfg(test)] pub fn from_arguments_str( file: &str, arguments: Vec<&str>, directory: &str, output: Option<&str>, ) -> Self { Self { file: file.into(), command: String::default(), arguments: arguments.into_iter().map(String::from).collect(), directory: directory.into(), output: output.map(|o| o.into()), } } /// Constructor method for testing purposes. #[cfg(test)] pub fn from_command_str(file: &str, command: &str, directory: &str, output: Option<&str>) -> Self { Self { file: file.into(), arguments: Vec::default(), command: command.into(), directory: directory.into(), output: output.map(|o| o.into()), } } } /// Represents the possible errors that can occur when validating an entry. #[derive(Debug, Eq, PartialEq, Error)] pub enum EntryError { #[error("Entry has an empty file field")] EmptyFileName, #[error("Entry has an empty directory field")] EmptyDirectory, #[error("Both command and arguments fields are empty")] CommandOrArgumentsAreMissing, #[error("Both command and arguments fields are present")] CommandOrArgumentsArePresent, #[error("Entry has an invalid command field: {0}")] InvalidCommand(#[from] shell_words::ParseError), } #[cfg(test)] mod tests { use super::*; #[test] fn test_entry_validate_success_arguments() { let entry = Entry::from_arguments_str("main.cpp", vec!["clang", "-c"], "/tmp", None); assert!(entry.command.is_empty()); assert!(!entry.arguments.is_empty()); assert!(entry.validate().is_ok()); } #[test] fn test_entry_validate_success_command() { let entry = Entry::from_command_str("main.cpp", "clang -c", "/tmp", None); assert!(!entry.command.is_empty()); assert!(entry.arguments.is_empty()); assert!(entry.validate().is_ok()); } // Requirements: output-json-compilation-database #[test] fn test_entry_validate_errors() { let cases = vec![ (Entry::from_arguments_str("", vec!["clang", "-c"], "/tmp", None), EntryError::EmptyFileName), ( Entry::from_arguments_str("main.cpp", vec!["clang", "-c"], "", None), EntryError::EmptyDirectory, ), ( Entry { file: "main.cpp".into(), arguments: vec![], command: "".to_string(), directory: "/tmp".into(), output: None, }, EntryError::CommandOrArgumentsAreMissing, ), ( Entry { file: "main.cpp".into(), arguments: vec!["clang".to_string()], command: "clang".to_string(), directory: "/tmp".into(), output: None, }, EntryError::CommandOrArgumentsArePresent, ), ( Entry::from_command_str("main.cpp", "\"unterminated", "/tmp", None), EntryError::InvalidCommand(shell_words::ParseError), ), ]; for (entry, expected_error) in cases { let err = entry.validate().unwrap_err(); assert_eq!(err, expected_error); } } #[test] fn test_entry_with_arguments_constructor() { let entry = Entry::with_arguments( "main.cpp", vec!["clang".to_string(), "-c".to_string()], "/tmp", Some("main.o"), ); assert!(!entry.arguments.is_empty()); assert!(entry.command.is_empty()); assert_eq!(entry.file, std::path::PathBuf::from("main.cpp")); assert_eq!(entry.directory, std::path::PathBuf::from("/tmp")); assert_eq!(entry.output, Some(std::path::PathBuf::from("main.o"))); assert!(entry.validate().is_ok()); } #[test] fn test_entry_with_command_constructor() { let entry = Entry::with_command( "main.cpp", vec!["clang".to_string(), "-c".to_string()], "/tmp", Some("main.o"), ); assert!(entry.arguments.is_empty()); assert!(!entry.command.is_empty()); assert_eq!(entry.command, "clang -c"); assert_eq!(entry.file, std::path::PathBuf::from("main.cpp")); assert_eq!(entry.directory, std::path::PathBuf::from("/tmp")); assert_eq!(entry.output, Some(std::path::PathBuf::from("main.o"))); assert!(entry.validate().is_ok()); } } rizsotto-Bear-0c30b27/crates/bear/src/output/clang/path_format.rs000066400000000000000000000331411527177720300250740ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! This module is responsible for formatting paths in the compiler calls. //! The reason for this is to ensure that the paths are in a consistent format //! when it comes to the output. //! //! The JSON compilation database //! [format specification](https://clang.llvm.org/docs/JSONCompilationDatabase.html#format) //! allows the `directory` attribute to be absolute or relative to the current working //! directory. The `file`, `output` and `arguments` attributes are either absolute or //! relative to the `directory` attribute. //! //! Four resolution strategies are available, selected per field via the //! `format.paths` configuration (documented in `man/bear.1.md`): //! //! - `as-is`: return the path unchanged (no-op; the default). //! - `absolute`: join with the base directory when relative, then normalize //! via `std::path::absolute()`. Does not require the path to exist on disk. //! - `relative`: compute the path relative to the base directory. Returns //! `PathsCannotBeRelative` when the two paths share no common root (e.g. //! files on different Windows drive letters). //! - `canonical`: resolve symlinks and `.`/`..` via `Path::canonicalize()`, //! which requires every path component to exist on disk, then strip the //! Windows extended-length prefix (`\\?\`) that clangd rejects (issue #683). //! //! The `directory` field is formatted using itself as the base; the `file` and //! `output` fields are resolved against the already-formatted directory. A //! `directory` that fails to format drops the whole entry; a `file` or `output` //! that fails falls back to its original path (see `CommandConverter` in //! `converter.rs`). //! //! The source and output paths that reappear in the `arguments` attribute go //! through the `file` resolver too, so an entry cannot disagree with itself //! about a path. Paths embedded inside other flags (`-I../include` and its //! kind) are left untransformed: rewriting them would require a flag-aware //! path rewriter for every compiler, which is fragile and out of scope. use crate::config::PathResolver; use std::io; use std::path::{Path, PathBuf, absolute}; use thiserror::Error; #[derive(Debug, Error)] pub enum FormatError { #[error("Path canonicalize failed: {0}")] PathCanonicalize(#[from] io::Error), #[error("Path {0} can't be relative to {1}")] PathsCannotBeRelative(PathBuf, PathBuf), } /// Function pointer for resolving a path. Parameters are `(base, path)`. pub type ResolveFn = fn(&Path, &Path) -> Result; /// Returns the resolver function for the given strategy. pub fn resolver_for(strategy: PathResolver) -> ResolveFn { match strategy { PathResolver::AsIs => resolve_as_is, PathResolver::Canonical => resolve_canonical, PathResolver::Relative => resolve_relative, PathResolver::Absolute => absolute_to, } } fn resolve_as_is(_base: &Path, path: &Path) -> Result { Ok(path.to_path_buf()) } fn resolve_canonical(_base: &Path, path: &Path) -> Result { Ok(strip_windows_extended_length_prefix(path.canonicalize()?)) } fn resolve_relative(base: &Path, path: &Path) -> Result { let absolute = absolute_to(base, path)?; relative_to(base, &absolute) } /// Strip the Windows extended-length path prefix (`\\?\`) if present. /// /// On Windows, `Path::canonicalize()` returns paths with this prefix, /// which tools like clangd do not understand. See GitHub issue #683. fn strip_windows_extended_length_prefix(path: PathBuf) -> PathBuf { let s = path.to_string_lossy(); if let Some(stripped) = s.strip_prefix(r"\\?\") { PathBuf::from(stripped) } else { path } } /// Compute the absolute path from the root directory if the path is relative. fn absolute_to(root: &Path, path: &Path) -> Result { if path.is_absolute() { Ok(absolute(path)?) } else { Ok(absolute(root.join(path))?) } } /// Compute the relative path from the root directory. fn relative_to(root: &Path, path: &Path) -> Result { // Ensure both paths are absolute for consistent behavior let abs_root = absolute(root)?; let abs_path = absolute(path)?; let mut root_components = abs_root.components(); let mut path_components = abs_path.components(); let mut remaining_root_components = Vec::new(); let mut remaining_path_components = Vec::new(); // Find the common prefix loop { let root_comp = root_components.next(); let path_comp = path_components.next(); match (root_comp, path_comp) { (Some(root), Some(path)) if root != path => { remaining_root_components.push(root); remaining_root_components.extend(root_components); remaining_path_components.push(path); remaining_path_components.extend(path_components); break; } (Some(root), None) => { remaining_root_components.push(root); remaining_root_components.extend(root_components); break; } (None, Some(path)) => { remaining_path_components.push(path); remaining_path_components.extend(path_components); break; } (None, None) => break, _ => continue, } } // Count remaining components in the root to determine how many `..` are needed let mut result = PathBuf::new(); for _ in remaining_root_components { result.push(std::path::Component::ParentDir); } // Add the remaining components of the path for comp in remaining_path_components { // if comp is a Prefix or RootDir, signal error match comp { std::path::Component::Normal(_) | std::path::Component::ParentDir => { result.push(comp); } std::path::Component::CurDir => { // Ignore this (should not happen since we are working with absolute paths) } _ => { return Err(FormatError::PathsCannotBeRelative(abs_path, abs_root)); } } } // When root and path are identical, the component walk leaves `result` // empty. An empty path is not a valid JSON compilation database value // (it fails entry validation downstream), so emit the POSIX "same // directory" form instead. if result.as_os_str().is_empty() { result.push(std::path::Component::CurDir); } Ok(result) } #[cfg(test)] mod tests { use super::*; use crate::config::PathResolver; use std::fs; use tempfile::tempdir; #[test] fn test_relative_to_with_relative_paths() { // Test that relative_to works correctly with relative input paths let root = Path::new("./some/root"); let path = Path::new("./some/path/file.txt"); let result = relative_to(root, path); assert!(result.is_ok()); assert_eq!(result.unwrap(), PathBuf::from("../path/file.txt")); } #[test] fn test_path_resolver_as_is() { let resolve = resolver_for(PathResolver::AsIs); let base = PathBuf::from("/base"); let path = PathBuf::from("some/path"); let result = resolve(&base, &path).unwrap(); assert_eq!(result, path); } #[test] fn test_absolute_to() { // The test creates a temporary directory and a file in it. // Then it verifies that the absolute path of the file is correct. // // E.g., `/tmp/tmpdir/file.txt` is the absolute path of the file, // if `/tmp/tmpdir` is the root directory and `file.txt` is the file. let root_dir = tempdir().unwrap(); let root_dir_path = root_dir.path().canonicalize().unwrap(); let file_path = root_dir_path.join("file.txt"); fs::write(&file_path, "content").unwrap(); let file_relative_path = PathBuf::from("file.txt"); let result = absolute_to(&root_dir_path, &file_relative_path).unwrap(); assert_eq!(result, file_path); let result = absolute_to(&root_dir_path, &file_path).unwrap(); assert_eq!(result, file_path); } #[test] fn test_relative_to() { // The test creates two temporary directories and a file in the first one. // Then it verifies that the relative path from the second directory to the file // in the first directory is correct. // // E.g., `../tmpdir/file.txt` is the relative path to the file, // if `/tmp/tmpdir2` is the root directory and `/tmp/tmpdir/file.txt` is the file. let a_dir = tempdir().unwrap(); let a_dir_path = a_dir.path().canonicalize().unwrap(); let a_dir_name = a_dir_path.file_name().unwrap(); let file_path = a_dir_path.join("file.txt"); fs::write(&file_path, "content").unwrap(); let b_dir = tempdir().unwrap(); let b_dir_path = b_dir.path().canonicalize().unwrap(); let result = relative_to(&b_dir_path, &file_path).unwrap(); assert_eq!(result, PathBuf::from("..").join(a_dir_name).join("file.txt")); let result = relative_to(&a_dir_path, &file_path).unwrap(); assert_eq!(result, PathBuf::from("file.txt")); } #[test] fn test_path_resolver_absolute_with_temp_files() { let temp_dir = tempdir().unwrap(); let temp_path = temp_dir.path().canonicalize().unwrap(); // Create a test file let file_path = temp_path.join("test.txt"); fs::write(&file_path, "test content").unwrap(); let resolve = resolver_for(PathResolver::Absolute); let base = temp_path.clone(); let relative_file = PathBuf::from("test.txt"); let result = resolve(&base, &relative_file).unwrap(); assert_eq!(result, file_path); assert!(result.is_absolute()); } #[test] fn test_path_resolver_relative_with_temp_files() { let temp_dir = tempdir().unwrap(); let temp_path = temp_dir.path().canonicalize().unwrap(); // Create a test file let file_path = temp_path.join("test.txt"); fs::write(&file_path, "test content").unwrap(); let resolve = resolver_for(PathResolver::Relative); let result = resolve(&temp_path, &file_path).unwrap(); assert_eq!(result, PathBuf::from("test.txt")); } #[test] fn test_relative_to_same_path_returns_curdir() { // Regression test for GitHub issue #692: `directory: relative` calls // relative_to(working_dir, working_dir); the component walk produces // no remaining components and must yield "." rather than an empty // path (an empty directory field fails Entry::validate). let temp_dir = tempdir().unwrap(); let temp_path = temp_dir.path().canonicalize().unwrap(); let result = relative_to(&temp_path, &temp_path).unwrap(); assert_eq!(result, PathBuf::from(".")); let resolve = resolver_for(PathResolver::Relative); let via_resolver = resolve(&temp_path, &temp_path).unwrap(); assert_eq!(via_resolver, PathBuf::from(".")); } #[test] fn test_path_resolver_canonical_with_temp_files() { let temp_dir = tempdir().unwrap(); let temp_path = temp_dir.path().canonicalize().unwrap(); // Create a test file let file_path = temp_path.join("test.txt"); fs::write(&file_path, "test content").unwrap(); let resolve = resolver_for(PathResolver::Canonical); // Test with the full file path since canonicalize requires the file to exist let result = resolve(&temp_path, &file_path).unwrap(); // On Windows, canonicalize() adds \\?\ prefix to temp_path (and thus file_path), // but the resolver strips it. Compare against the stripped expected path. let expected = strip_windows_extended_length_prefix(file_path); assert_eq!(result, expected); assert!(result.is_absolute()); } #[test] fn test_path_resolver_canonical_no_extended_length_prefix() { // Regression test for GitHub issue #683: // On Windows, Path::canonicalize() returns extended-length paths // with "\\?\" prefix that clangd does not understand. let temp_dir = tempdir().unwrap(); let temp_path = temp_dir.path().canonicalize().unwrap(); let file_path = temp_path.join("source.c"); fs::write(&file_path, "int main() {}").unwrap(); let resolve = resolver_for(PathResolver::Canonical); let result = resolve(&temp_path, &file_path).unwrap(); let result_str = result.to_string_lossy(); assert!( !result_str.starts_with(r"\\?\"), "Canonical path should not have extended-length prefix '\\\\?\\': {}", result_str ); let dir_result = resolve(&temp_path, &temp_path).unwrap(); let dir_str = dir_result.to_string_lossy(); assert!( !dir_str.starts_with(r"\\?\"), "Canonical directory should not have extended-length prefix '\\\\?\\': {}", dir_str ); } #[test] fn test_strip_windows_extended_length_prefix() { // Unit test for the strip function directly let normal = PathBuf::from("/tmp/foo/bar"); assert_eq!(strip_windows_extended_length_prefix(normal.clone()), normal); let with_prefix = PathBuf::from(r"\\?\C:\Users\foo\bar"); let stripped = strip_windows_extended_length_prefix(with_prefix); assert_eq!(stripped, PathBuf::from(r"C:\Users\foo\bar")); let unc_path = PathBuf::from(r"\\server\share\file"); assert_eq!(strip_windows_extended_length_prefix(unc_path.clone()), unc_path); } } rizsotto-Bear-0c30b27/crates/bear/src/output/clang/serialization.rs000066400000000000000000000265241527177720300254540ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! JSON compilation database serialization format. //! //! The format is a JSON array format, which is a sequence of JSON objects //! enclosed in square brackets. Each object represents a compilation command. //! //! The format itself is defined in the LLVM project documentation. //! https://clang.llvm.org/docs/JSONCompilationDatabase.html use super::Entry; use super::json; use crate::output::{SerializationError, SerializationFormat}; /// The type represents a JSON compilation database format. pub struct JsonCompilationDatabase; impl SerializationFormat for JsonCompilationDatabase { /// Serialize entries as a JSON compilation database. /// /// Entries are expected to already be validated (see /// `ValidatingOutputWriter` in the output pipeline). This function /// performs no further semantic validation; it reports only I/O and /// JSON-encoding errors. fn write( mut writer: impl std::io::Write, entries: impl Iterator, ) -> Result<(), SerializationError> { json::serialize_seq(&mut writer, entries).map_err(SerializationError::Syntax)?; // Without this, a buffering writer (stdout, BufWriter) flushes on // drop and discards the error: a tail write failure (EPIPE, // disk-full) would exit zero with a truncated database. writer.flush().map_err(SerializationError::Io)?; Ok(()) } fn read(reader: impl std::io::Read) -> impl Iterator> { json::deserialize_seq(reader).map(|res| { res.map_err(SerializationError::Syntax) // Ensure only valid entries are returned. .and_then(|entry: Entry| match entry.validate() { Ok(_) => Ok(entry), Err(err) => Err(SerializationError::Semantic(err)), }) }) } } #[cfg(test)] mod test { use super::JsonCompilationDatabase as Sut; use super::{Entry, SerializationError, SerializationFormat}; use serde_json::error::Category; use serde_json::json; use std::io::{Cursor, Seek, SeekFrom}; macro_rules! assert_json_error { ($x:expr) => { match $x { Some(Err(SerializationError::Syntax(error))) => { assert_eq!(error.classify(), Category::Data) } _ => assert!(false, "shout be JSON error"), } }; } macro_rules! assert_format_error { ($x:expr) => { assert!(matches!($x, Some(Err(SerializationError::Semantic(_)))), "should be format error"); }; } #[test] fn load_non_json_content() { let content = r#"this is not json"#; let mut result = Sut::read(content.as_bytes()); assert_json_error!(result.next()); assert!(result.next().is_none()); } #[test] fn load_not_expected_json_content() { let content = json!({ "file": "string" }).to_string(); let mut result = Sut::read(content.as_bytes()); assert_json_error!(result.next()); assert!(result.next().is_none()); } #[test] fn load_on_bad_value() { let content = json!([ { "directory": " ", "file": "./file_a.c", "command": "cc -Dvalue=\"this" } ]) .to_string(); let mut result = Sut::read(content.as_bytes()); assert_format_error!(result.next()); assert!(result.next().is_none()); } #[test] fn load_on_multiple_commands() { let content = json!([ { "directory": " ", "file": "./file_a.c", "command": "cc source.c", "arguments": ["cc", "source.c"], } ]) .to_string(); let mut result = Sut::read(content.as_bytes()); assert_format_error!(result.next()); assert!(result.next().is_none()); } #[test] fn load_empty_array() { let content = json!([]).to_string(); let mut result = Sut::read(content.as_bytes()); assert!(result.next().is_none()); } // Serializer trusts its input; the upstream `ValidatingOutputWriter` // filters invalid entries before they reach `write`. See // `bear/src/output/writers/validating.rs` for the validation tests. fn expected_values_with_arguments() -> Vec { vec![ Entry::from_arguments_str( "./file_a.c", vec!["cc", "-c", "./file_a.c", "-o", "./file_a.o"], "/home/user", None, ), Entry::from_arguments_str( "./file_b.c", vec!["cc", "-c", "./file_b.c", "-o", "./file_b.o"], "/home/user", Some("./file_b.o"), ), ] } fn expected_values_with_command() -> Vec { vec![ Entry::from_command_str("./file_a.c", "cc -c ./file_a.c -o ./file_a.o", "/home/user", None), Entry::from_command_str( "./file_b.c", "cc -c ./file_b.c -o ./file_b.o", "/home/user", Some("./file_b.o"), ), ] } fn expected_with_array_syntax() -> serde_json::Value { json!([ { "directory": "/home/user", "file": "./file_a.c", "arguments": ["cc", "-c", "./file_a.c", "-o", "./file_a.o"] }, { "directory": "/home/user", "file": "./file_b.c", "output": "./file_b.o", "arguments": ["cc", "-c", "./file_b.c", "-o", "./file_b.o"] } ]) } fn expected_with_string_syntax() -> serde_json::Value { json!([ { "directory": "/home/user", "file": "./file_a.c", "command": "cc -c ./file_a.c -o ./file_a.o" }, { "directory": "/home/user", "file": "./file_b.c", "output": "./file_b.o", "command": "cc -c ./file_b.c -o ./file_b.o" } ]) } #[test] fn load_content_with_string_command_syntax() { let content = expected_with_string_syntax().to_string(); let result = Sut::read(content.as_bytes()); let entries: Vec = result.map(|e| e.unwrap()).collect(); assert_eq!(expected_values_with_command(), entries); } #[test] fn load_content_with_array_command_syntax() { let content = expected_with_array_syntax().to_string(); let result = Sut::read(content.as_bytes()); let entries: Vec = result.map(|e| e.unwrap()).collect(); assert_eq!(expected_values_with_arguments(), entries); } #[test] fn save_with_array_command_syntax() -> Result<(), SerializationError> { let input = expected_values_with_arguments(); // Create fake "file" let mut buffer = Cursor::new(Vec::new()); let result = Sut::write(&mut buffer, input.into_iter()); assert!(result.is_ok()); // Use the fake "file" as input buffer.seek(SeekFrom::Start(0))?; let content: serde_json::Value = serde_json::from_reader(&mut buffer)?; assert_eq!(expected_with_array_syntax(), content); Ok(()) } /// A writer that accepts every write but fails the final flush, the /// shape of a pipe closed after the last write or a disk that fills /// at flush time. struct FlushFails; impl std::io::Write for FlushFails { fn write(&mut self, buf: &[u8]) -> std::io::Result { Ok(buf.len()) } fn flush(&mut self) -> std::io::Result<()> { Err(std::io::Error::other("flush failed")) } } #[test] fn write_surfaces_a_tail_flush_error() { let input = expected_values_with_arguments(); let sut = Sut::write(FlushFails, input.into_iter()); assert!(matches!(sut, Err(SerializationError::Io(_))), "flush errors must not be swallowed"); } fn expected_quoted_values_with_argument() -> Vec { vec![ Entry::from_arguments_str( "./file_a.c", vec!["cc", "-c", "-D", r#"name=\"me\""#, "./file_a.c", "-o", "./file_a.o"], "/home/user", None, ), Entry::from_arguments_str( "./file_b.c", vec!["cc", "-c", "-D", r#"name="me""#, "./file_b.c", "-o", "./file_b.o"], "/home/user", None, ), ] } fn expected_quoted_values_with_command() -> Vec { vec![ Entry::from_command_str( "./file_a.c", r#"cc -c -D 'name=\"me\"' ./file_a.c -o ./file_a.o"#, "/home/user", None, ), Entry::from_command_str( "./file_b.c", r#"cc -c -D 'name="me"' ./file_b.c -o ./file_b.o"#, "/home/user", None, ), ] } fn expected_quoted_with_array_syntax() -> serde_json::Value { json!([ { "directory": "/home/user", "file": "./file_a.c", "arguments": ["cc", "-c", "-D", r#"name=\"me\""#, "./file_a.c", "-o", "./file_a.o"] }, { "directory": "/home/user", "file": "./file_b.c", "arguments": ["cc", "-c", "-D", r#"name="me""#, "./file_b.c", "-o", "./file_b.o"] } ]) } fn expected_quoted_with_string_syntax() -> serde_json::Value { json!([ { "directory": "/home/user", "file": "./file_a.c", "command": r#"cc -c -D 'name=\"me\"' ./file_a.c -o ./file_a.o"# }, { "directory": "/home/user", "file": "./file_b.c", "command": r#"cc -c -D 'name="me"' ./file_b.c -o ./file_b.o"# } ]) } #[test] fn load_quoted_content_with_array_command_syntax() { let content = expected_quoted_with_array_syntax().to_string(); let result = Sut::read(content.as_bytes()); let entries: Vec = result.map(|e| e.unwrap()).collect(); assert_eq!(expected_quoted_values_with_argument(), entries); } #[test] fn load_quoted_content_with_string_command_syntax() { let content = expected_quoted_with_string_syntax().to_string(); let result = Sut::read(content.as_bytes()); let entries: Vec = result.map(|e| e.unwrap()).collect(); assert_eq!(expected_quoted_values_with_command(), entries); } #[test] fn save_quoted_with_array_command_syntax() -> Result<(), SerializationError> { let input = expected_quoted_values_with_argument(); // Create fake "file" let mut buffer = Cursor::new(Vec::new()); let result = Sut::write(&mut buffer, input.into_iter()); assert!(result.is_ok()); // Use the fake "file" as input buffer.seek(SeekFrom::Start(0))?; let content: serde_json::Value = serde_json::from_reader(&mut buffer)?; assert_eq!(expected_quoted_with_array_syntax(), content); Ok(()) } } rizsotto-Bear-0c30b27/crates/bear/src/output/formats.rs000066400000000000000000000032001527177720300231500ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Shared serialization traits and error types for file formats. use super::clang; use thiserror::Error; /// Represents errors that can occur while working with file formats. #[derive(Debug, Error)] pub enum SerializationError { #[error(transparent)] Io(#[from] std::io::Error), #[error(transparent)] Syntax(#[from] serde_json::Error), #[error("invalid entry: {0}")] Semantic(#[from] clang::EntryError), #[error("line {line}: {source}")] AtLine { line: usize, source: serde_json::Error }, } /// A trait representing a file format that can be written to and read from. /// /// File formats in this project are usually sequences of values. This trait /// provides a type-independent abstraction over file formats. pub trait SerializationFormat { /// Writes an iterator of items to the specified writer. fn write(writer: impl std::io::Write, items: impl Iterator) -> Result<(), SerializationError>; /// Reads items from the specified reader, returning an iterator of results. fn read(reader: impl std::io::Read) -> impl Iterator>; /// Reads entries from the file and ignores any errors. /// /// This is not always feasible when the file format is strict. fn read_and_ignore(reader: impl std::io::Read, message_writer: impl Fn(&str)) -> impl Iterator { Self::read(reader).filter_map(move |result| match result { Ok(value) => Some(value), Err(error) => { message_writer(&error.to_string()); None } }) } } rizsotto-Bear-0c30b27/crates/bear/src/output/intercept.rs000066400000000000000000000207321527177720300235030ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Serialization format for execution events. //! //! The format is a [JSON line format](https://jsonlines.org/), which is a sequence //! of JSON objects separated by newlines. //! //! # Note //! The output format is not stable and may change in future versions. use super::{SerializationError, SerializationFormat}; use std::io::BufRead; /// The type represents a database format for execution events. pub struct ExecutionEventDatabase; impl SerializationFormat for ExecutionEventDatabase { fn write( writer: impl std::io::Write, executions: impl Iterator, ) -> Result<(), SerializationError> { let mut writer = writer; for execution in executions { serde_json::to_writer(&mut writer, &execution).map_err(SerializationError::Syntax)?; writer.write_all(b"\n").map_err(SerializationError::Io)?; } writer.flush().map_err(SerializationError::Io)?; Ok(()) } /// Reads line-delimited JSON events, one `Result` per non-blank line. /// /// Unlike a single `StreamDeserializer` over the whole reader, this /// parses each line independently: a malformed line yields `Err` for /// that line only, and parsing continues with subsequent lines. This /// is required by `docs/requirements/interception-events-format.md` /// (a non-conforming line must not silently drop every line after it). /// Blank/whitespace-only lines are skipped rather than yielded as /// errors or empty results. /// /// This is the single production parser for the event stream: it is /// what `RawEventReader::produce` (`crates/bear/src/modes/mod.rs`) /// consumes directly. `enumerate()` runs before the blank-line /// `filter_map`, so a malformed line's `SerializationError::AtLine` /// carries its true 1-based physical line number even when earlier /// blank lines were skipped. fn read( reader: impl std::io::Read, ) -> impl Iterator> { let buffered = std::io::BufReader::new(reader); buffered.lines().enumerate().filter_map(|(index, line)| { let line_number = index + 1; match line { Ok(line) if line.trim().is_empty() => None, Ok(line) => Some( serde_json::from_str::(&line) .map_err(|source| SerializationError::AtLine { line: line_number, source }), ), Err(error) => Some(Err(SerializationError::Io(error))), } }) } } #[cfg(test)] mod tests { use super::ExecutionEventDatabase as Sut; use super::SerializationError; use super::SerializationFormat; use intercept::Execution; use serde_json::json; use std::collections::HashMap; use std::io::{Cursor, Seek, SeekFrom}; #[test] fn read_write() { let executions = expected_values(); let mut buffer = Cursor::new(Vec::new()); Sut::write(&mut buffer, executions.iter().cloned()).unwrap(); buffer.seek(SeekFrom::Start(0)).unwrap(); let read_back: Vec<_> = Sut::read(&mut buffer).collect::>().unwrap(); assert_eq!(executions, read_back); } #[test] fn read_write_empty() { let executions = Vec::::new(); let mut buffer = Cursor::new(Vec::new()); Sut::write(&mut buffer, executions.iter().cloned()).unwrap(); buffer.seek(SeekFrom::Start(0)).unwrap(); let read_back: Vec<_> = Sut::read(&mut buffer).collect::>().unwrap(); assert_eq!(executions, read_back); } #[test] fn write_propagates_flush_errors() { // A sink whose writes succeed but whose flush fails, standing in // for a full pipe or disk discovered only at the tail flush. struct FailingFlushWriter; impl std::io::Write for FailingFlushWriter { fn write(&mut self, buf: &[u8]) -> std::io::Result { Ok(buf.len()) } fn flush(&mut self) -> std::io::Result<()> { Err(std::io::Error::other("flush failure")) } } let executions = expected_values(); let sut = Sut::write(FailingFlushWriter, executions.into_iter()); assert!(matches!(sut, Err(SerializationError::Io(_))), "the tail flush error must propagate"); } #[test] fn read_continues_past_errors() { let line1 = json!({ "executable": "/usr/bin/clang", "arguments": ["clang", "-c", "main.c"], "working_dir": "/home/user", "environment": { "PATH": "/usr/bin", "HOME": "/home/user" } }); let line2 = json!({"executable": 42 }); let line3 = json!({ "executable": "/usr/bin/clang", "arguments": ["clang", "-c", "output.c"], "working_dir": "/home/user", "environment": {} }); let content = format!("{line1}\n{line2}\n{line3}\n"); let mut cursor = Cursor::new(content); let warnings = std::cell::RefCell::new(Vec::new()); let read_back: Vec<_> = Sut::read_and_ignore(&mut cursor, |error| { warnings.borrow_mut().push(format!("Warning: {error:?}")); }) .collect(); // Both valid executions are read; only the malformed middle line is dropped. assert_eq!(expected_values()[0..2], read_back); assert_eq!(warnings.borrow().len(), 1); } #[test] fn read_all_malformed_yields_no_executions_and_one_warning_per_line() { let content = "{\"executable\": 42}\nnot json at all\n{\"executable\": true}\n".to_string(); let mut cursor = Cursor::new(content); let warnings = std::cell::RefCell::new(Vec::new()); let read_back: Vec<_> = Sut::read_and_ignore(&mut cursor, |error| { warnings.borrow_mut().push(format!("Warning: {error:?}")); }) .collect(); assert_eq!(Vec::::new(), read_back); assert_eq!(warnings.borrow().len(), 3); } #[test] fn read_reports_physical_line_number_of_malformed_line() { let line1 = json!({ "executable": "/usr/bin/clang", "arguments": ["clang", "-c", "main.c"], "working_dir": "/home/user", "environment": {} }); let line3 = json!({ "executable": "/usr/bin/clang", "arguments": ["clang", "-c", "output.c"], "working_dir": "/home/user", "environment": {} }); let content = format!("{line1}\nnot json at all\n{line3}\n"); let mut cursor = Cursor::new(content); let results: Vec<_> = Sut::read(&mut cursor).collect(); assert_eq!(results.len(), 3); assert!(results[0].is_ok()); let error = results[1].as_ref().unwrap_err(); assert!(error.to_string().contains("line 2"), "expected 'line 2' in: {error}"); assert!(results[2].is_ok()); } #[test] fn read_reports_true_physical_line_number_past_a_blank_line() { let line1 = json!({ "executable": "/usr/bin/clang", "arguments": ["clang", "-c", "main.c"], "working_dir": "/home/user", "environment": {} }); // A blank line between the valid and malformed lines must not shift // the reported line number: the malformed line is physical line 3 // (valid, blank, malformed), not record 2 counting only data lines. let content = format!("{line1}\n\nnot json at all\n"); let mut cursor = Cursor::new(content); let results: Vec<_> = Sut::read(&mut cursor).collect(); assert_eq!(results.len(), 2); assert!(results[0].is_ok()); let error = results[1].as_ref().unwrap_err(); assert!(error.to_string().contains("line 3"), "expected 'line 3' in: {error}"); } fn expected_values() -> Vec { vec![ Execution::from_strings( "/usr/bin/clang", vec!["clang", "-c", "main.c"], "/home/user", HashMap::from([("PATH", "/usr/bin"), ("HOME", "/home/user")]), ), Execution::from_strings( "/usr/bin/clang", vec!["clang", "-c", "output.c"], "/home/user", HashMap::from([]), ), ] } } rizsotto-Bear-0c30b27/crates/bear/src/output/mod.rs000066400000000000000000000124411527177720300222630ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! This module is responsible for writing the output of semantic analysis. //! //! The output is written as JSON compilation databases. //! The module provides functionality to write these outputs to files, //! handle duplicates, and format the output as needed. //! //! The `OutputWriter` struct represents the main entry point for writing output. //! The input to the `OutputWriter` is a stream of `semantic::Command` instances. mod atomic; pub mod clang; mod formats; mod intercept; mod statistics; mod writers; use crate::{args, config}; use std::sync::Arc; use thiserror::Error; // Re-export types for convenience. pub use atomic::AtomicFile; pub use formats::{SerializationError, SerializationFormat}; pub use intercept::ExecutionEventDatabase; pub use statistics::OutputStatistics; /// Represents the output writer for JSON compilation databases. /// /// The writer handles writing semantic analysis results as JSON compilation databases, /// with support for deduplication, atomic writes, and appending to existing files. pub struct OutputWriter { writer: writers::SemanticCommandWriter, stats: Arc, } impl TryFrom<(&args::BuildSemantic, &config::Main)> for OutputWriter { type Error = WriterCreationError; fn try_from(value: (&args::BuildSemantic, &config::Main)) -> Result { let (args, config) = value; let stats = OutputStatistics::new(); let writer = writers::create_pipeline(args, config, Arc::clone(&stats))?; Ok(Self { writer, stats }) } } impl OutputWriter { /// Writes semantic commands using the configured output writer. /// /// # Arguments /// * `semantics` - An iterator of semantic commands to write /// /// # Returns /// The written output, still to be committed, or a `WriterError` if /// writing fails. pub fn write( self, semantics: impl Iterator, ) -> Result { let result = self.writer.write(semantics); // Log pipeline statistics log::info!("{}", self.stats); // If every candidate entry was dropped as invalid, surface that // at ERROR level so the empty compilation database is never silent. // The per-entry WARN logs explain individual drops; this one // explains the shape of the whole output. let written = self.stats.entries_written.load(std::sync::atomic::Ordering::Relaxed); let dropped = self.stats.entries_dropped_invalid.load(std::sync::atomic::Ordering::Relaxed); if written == 0 && dropped > 0 { log::error!( "Compilation database is empty: all {dropped} candidate entries were dropped as invalid; see WARN log lines above for per-entry reasons", ); } result } /// Returns the statistics collected during the output pipeline execution. pub fn statistics(&self) -> &Arc { &self.stats } } /// Represents errors that can occur while creating an output writer. #[derive(Error, Debug)] pub enum WriterCreationError { #[error("cannot create {0}: {1}")] Io(std::path::PathBuf, std::io::Error), #[error("cannot read {0}: {1}")] Read(std::path::PathBuf, std::io::Error), #[error("{0}")] Configuration(String), } /// Represents errors that can occur while writing output. #[derive(Error, Debug)] pub enum WriterError { #[error("cannot write {0}: {1}")] Io(std::path::PathBuf, SerializationError), } #[cfg(test)] mod tests { use super::*; use semantic::{ArgumentKind, Command, CompilerPass, PassEffect}; use std::sync::atomic::Ordering; fn make_compile_command(file: &str) -> Command { Command::from_strings( "/home/user", "/usr/bin/gcc", vec![ (ArgumentKind::Compiler, vec!["/usr/bin/gcc"]), (ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling)), vec!["-c"]), (ArgumentKind::Source { binary: false }, vec![file]), ], ) } #[test] fn test_output_writer_try_from_and_write() { let dir = tempfile::tempdir().unwrap(); let output_path = dir.path().join("compile_commands.json"); let args = args::BuildSemantic { path: output_path.clone(), append: false }; let config = config::Main::default(); let writer = OutputWriter::try_from((&args, &config)).unwrap(); let stats = writer.statistics().clone(); let commands = vec![make_compile_command("main.c"), make_compile_command("util.c")]; writer.write(commands.into_iter()).unwrap().commit().unwrap(); assert!(output_path.exists()); let content = std::fs::read_to_string(&output_path).unwrap(); assert!(content.contains("main.c")); assert!(content.contains("util.c")); assert_eq!(stats.entries_written.load(Ordering::Relaxed), 2); } #[test] fn test_output_writer_creation_failure() { let args = args::BuildSemantic { path: std::path::PathBuf::from("/nonexistent/dir/output.json"), append: false, }; let config = config::Main::default(); let result = OutputWriter::try_from((&args, &config)); assert!(result.is_err()); } } rizsotto-Bear-0c30b27/crates/bear/src/output/statistics.rs000066400000000000000000000135141527177720300237000ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Statistics collection for the output pipeline. //! //! This module provides a centralized statistics structure that tracks metrics //! across all stages of the output pipeline. Each writer in the chain updates //! its specific field(s) as entries flow through, enabling comprehensive //! visibility into the pipeline's behavior. //! //! # Usage //! //! Create a shared `OutputStatistics` instance and pass it to each writer //! during pipeline construction. After processing completes, the statistics //! can be logged to provide insight into how entries were transformed, //! filtered, or deduplicated at each stage. use std::fmt; use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; /// Statistics collected during the output pipeline execution. /// /// Each writer in the chain updates its specific field(s) using atomic /// operations, allowing for lock-free updates as entries flow through. /// /// # Fields by Writer /// /// - **ConverterClangOutputWriter**: `semantic_commands_received`, `compilation_entries_produced`, /// `entries_dropped_invalid` (unresolvable directory) /// - **AppendClangOutputWriter**: `entries_read_from_existing` /// - **UniqueOutputWriter**: `duplicates_detected` /// - **SourceFilterOutputWriter**: `entries_filtered_by_source` /// - **HeaderEntrySynthesizer**: entries_synthesized /// - **ValidatingOutputWriter**: `entries_dropped_invalid` /// - **ClangOutputWriter**: `entries_written` #[derive(Debug, Default)] pub struct OutputStatistics { /// Number of semantic commands received by the converter. pub semantic_commands_received: AtomicUsize, /// Number of compilation database entries produced by the converter. pub compilation_entries_produced: AtomicUsize, /// Number of entries read from an existing compilation database (append mode). pub entries_read_from_existing: AtomicUsize, /// Number of duplicate entries detected and removed. pub duplicates_detected: AtomicUsize, /// Number of entries filtered out based on source file location rules. pub entries_filtered_by_source: AtomicUsize, /// Number of header entries synthesized from a donor translation unit. pub entries_synthesized: AtomicUsize, /// Number of entries dropped as invalid: failed validation, or a working /// directory that could not be resolved. pub entries_dropped_invalid: AtomicUsize, /// Total number of entries written to the final output file. pub entries_written: AtomicUsize, } impl OutputStatistics { /// Creates a new `OutputStatistics` instance wrapped in an `Arc` for sharing. pub fn new() -> Arc { Arc::new(Self::default()) } } impl fmt::Display for OutputStatistics { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let semantic = self.semantic_commands_received.load(Ordering::Relaxed); let produced = self.compilation_entries_produced.load(Ordering::Relaxed); let from_existing = self.entries_read_from_existing.load(Ordering::Relaxed); let duplicates = self.duplicates_detected.load(Ordering::Relaxed); let filtered = self.entries_filtered_by_source.load(Ordering::Relaxed); let synthesized = self.entries_synthesized.load(Ordering::Relaxed); let dropped_invalid = self.entries_dropped_invalid.load(Ordering::Relaxed); let written = self.entries_written.load(Ordering::Relaxed); writeln!(f, "Output pipeline:")?; writeln!(f, " semantic events: {}", semantic)?; writeln!(f, " current entries: {}", produced)?; writeln!(f, " previous entries: {}", from_existing)?; writeln!(f, " filtered entries by duplicate: {}", duplicates)?; writeln!(f, " filtered entries by source: {}", filtered)?; writeln!(f, " synthesized header entries: {}", synthesized)?; writeln!(f, " dropped entries (invalid): {}", dropped_invalid)?; write!(f, " total entries written: {}", written) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_new_creates_arc() { let stats = OutputStatistics::new(); assert_eq!(stats.semantic_commands_received.load(Ordering::Relaxed), 0); assert_eq!(stats.entries_written.load(Ordering::Relaxed), 0); } #[test] fn test_display_format() { let stats = OutputStatistics::new(); stats.semantic_commands_received.store(20, Ordering::Relaxed); stats.compilation_entries_produced.store(15, Ordering::Relaxed); stats.entries_read_from_existing.store(5, Ordering::Relaxed); stats.duplicates_detected.store(3, Ordering::Relaxed); stats.entries_filtered_by_source.store(2, Ordering::Relaxed); stats.entries_synthesized.store(4, Ordering::Relaxed); stats.entries_dropped_invalid.store(1, Ordering::Relaxed); stats.entries_written.store(10, Ordering::Relaxed); let output = format!("{}", stats); assert!(output.contains("Output pipeline:")); assert!(output.contains("semantic events: 20")); assert!(output.contains("current entries: 15")); assert!(output.contains("previous entries: 5")); assert!(output.contains("filtered entries by duplicate: 3")); assert!(output.contains("filtered entries by source: 2")); assert!(output.contains("synthesized header entries: 4")); assert!(output.contains("dropped entries (invalid): 1")); assert!(output.contains("total entries written: 10")); } #[test] fn test_atomic_updates_from_multiple_references() { let stats = OutputStatistics::new(); let stats_clone = Arc::clone(&stats); stats.semantic_commands_received.fetch_add(5, Ordering::Relaxed); stats_clone.semantic_commands_received.fetch_add(3, Ordering::Relaxed); assert_eq!(stats.semantic_commands_received.load(Ordering::Relaxed), 8); } } rizsotto-Bear-0c30b27/crates/bear/src/output/writers/000077500000000000000000000000001527177720300226335ustar00rootroot00000000000000rizsotto-Bear-0c30b27/crates/bear/src/output/writers/append.rs000066400000000000000000000256171527177720300244630ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later use super::IteratorWriter; use crate::output::WriterCreationError; use crate::output::clang; use crate::output::clang::serialization::JsonCompilationDatabase; use crate::output::formats::{SerializationError, SerializationFormat}; use crate::output::statistics::OutputStatistics; use std::sync::Arc; use std::sync::atomic::Ordering; use std::{fs, io, path}; /// The type represents a writer that handles appending entries to an existing Clang output file. /// /// This writer supports reading existing entries from a compilation database file, /// combining them with new entries, and writing the result back to the file. /// If the file does not exist and the append option is enabled, it logs a warning /// and writes only the new entries. /// /// The existing file is opened once when the writer is created, so a file /// that cannot be opened fails before the build runs. It is read after the /// build, as it is then. Invalid entries in it are skipped with a warning. pub(crate) struct AppendClangOutputWriter> { writer: T, path: Option, stats: Arc, } impl> AppendClangOutputWriter { pub(crate) fn new( writer: T, input_path: &path::Path, append: bool, stats: Arc, ) -> Result { let path = if input_path.exists() && append { fs::File::open(input_path) .map_err(|err| WriterCreationError::Read(input_path.to_path_buf(), err))?; Some(input_path.to_path_buf()) } else { if append && !input_path.exists() { log::warn!( "cannot read previous entries from {}: the file does not exist", input_path.display() ); } None }; Ok(Self { writer, path, stats }) } } impl> IteratorWriter for AppendClangOutputWriter { fn write(self, entries: impl Iterator) -> Result<(), SerializationError> { if let Some(path) = self.path { let stats = Arc::clone(&self.stats); // Opened before the build already; a failure now means the file // changed during the build. let file = fs::File::open(&path)?; let entries_from_db = JsonCompilationDatabase::read_and_ignore(io::BufReader::new(file), move |error| { log::warn!("cannot read previous entries from {}: {error}", path.display()); }); // Count entries read from existing database let counted_existing = entries_from_db.inspect(move |_| { stats.entries_read_from_existing.fetch_add(1, Ordering::Relaxed); }); let final_entries = entries.chain(counted_existing); self.writer.write(final_entries) } else { self.writer.write(entries) } } } #[cfg(test)] mod tests { use super::*; use crate::output::writers::file::ClangOutputWriter; #[test] fn test_append_clang_output_writer_no_original_file() { let dir = tempfile::tempdir().unwrap(); let input_path = dir.path().join("file_to_append.json"); let result_path = dir.path().join("result_file.json"); let stats = OutputStatistics::new(); let entries_to_write = vec![ clang::Entry::from_arguments_str("file1.cpp", vec!["clang", "-c"], "/path/to/dir", None), clang::Entry::from_arguments_str("file2.cpp", vec!["clang", "-c"], "/path/to/dir", None), ]; let writer = ClangOutputWriter::new(fs::File::create(&result_path).unwrap(), Arc::clone(&stats)); let sut = AppendClangOutputWriter::new(writer, &input_path, false, Arc::clone(&stats)).unwrap(); sut.write(entries_to_write.into_iter()).unwrap(); // Verify the result file contains the written entries assert!(result_path.exists()); let content = fs::read_to_string(&result_path).unwrap(); assert!(content.contains("file1.cpp")); assert!(content.contains("file2.cpp")); } // Requirements: output-append #[test] fn test_append_clang_output_writer_with_original_file() { let dir = tempfile::tempdir().unwrap(); let input_path = dir.path().join("file_to_append.json"); let result_path = dir.path().join("result_file.json"); let stats = OutputStatistics::new(); // Create the original file with some entries let original_entries = vec![ clang::Entry::from_arguments_str("file3.cpp", vec!["clang", "-c"], "/path/to/dir", None), clang::Entry::from_arguments_str("file4.cpp", vec!["clang", "-c"], "/path/to/dir", None), ]; let writer = ClangOutputWriter::new(fs::File::create(&input_path).unwrap(), Arc::clone(&stats)); writer.write(original_entries.into_iter()).unwrap(); let new_entries = vec![ clang::Entry::from_arguments_str("file1.cpp", vec!["clang", "-c"], "/path/to/dir", None), clang::Entry::from_arguments_str("file2.cpp", vec!["clang", "-c"], "/path/to/dir", None), ]; let writer = ClangOutputWriter::new(fs::File::create(&result_path).unwrap(), Arc::clone(&stats)); let sut = AppendClangOutputWriter::new(writer, &input_path, true, Arc::clone(&stats)).unwrap(); sut.write(new_entries.into_iter()).unwrap(); // Verify the result file contains both original and new entries assert!(result_path.exists()); let content = fs::read_to_string(&result_path).unwrap(); assert!(content.contains("file1.cpp")); assert!(content.contains("file2.cpp")); assert!(content.contains("file3.cpp")); assert!(content.contains("file4.cpp")); // New entries must be emitted before the existing ones. let pos_new1 = content.find("file1.cpp").unwrap(); let pos_new2 = content.find("file2.cpp").unwrap(); let pos_existing3 = content.find("file3.cpp").unwrap(); let pos_existing4 = content.find("file4.cpp").unwrap(); assert!( pos_new1 < pos_existing3 && pos_new2 < pos_existing3, "new entries must come before existing entries in the merged output" ); assert!( pos_new1 < pos_existing4 && pos_new2 < pos_existing4, "new entries must come before existing entries in the merged output" ); } #[test] fn test_append_with_corrupted_database_file() { let dir = tempfile::tempdir().unwrap(); let input_path = dir.path().join("corrupted.json"); let result_path = dir.path().join("result_file.json"); let stats = OutputStatistics::new(); // Write invalid JSON to the input file fs::write(&input_path, "this is not valid json at all!!!").unwrap(); let new_entries = vec![clang::Entry::from_arguments_str("new_file.cpp", vec!["clang", "-c"], "/path/to/dir", None)]; let writer = ClangOutputWriter::new(fs::File::create(&result_path).unwrap(), Arc::clone(&stats)); let sut = AppendClangOutputWriter::new(writer, &input_path, true, Arc::clone(&stats)).unwrap(); // The invalid content is skipped with a warning, so only the new entry is written. sut.write(new_entries.into_iter()).unwrap(); let content = fs::read_to_string(&result_path).unwrap(); assert!(content.contains("new_file.cpp")); assert_eq!(stats.entries_read_from_existing.load(std::sync::atomic::Ordering::Relaxed), 0); } #[test] fn test_append_with_partially_valid_database_file() { let dir = tempfile::tempdir().unwrap(); let input_path = dir.path().join("partial.json"); let result_path = dir.path().join("result_file.json"); let stats = OutputStatistics::new(); // Write a JSON array with one valid and one invalid entry // The valid entry has both command and arguments (which is invalid per validation) let partial_content = r#"[ {"directory": "/path/to/dir", "file": "valid.cpp", "arguments": ["clang", "-c"]}, {"directory": "", "file": "invalid.cpp", "arguments": ["clang", "-c"]} ]"#; fs::write(&input_path, partial_content).unwrap(); let new_entries = vec![clang::Entry::from_arguments_str("new_file.cpp", vec!["clang", "-c"], "/path/to/dir", None)]; let writer = ClangOutputWriter::new(fs::File::create(&result_path).unwrap(), Arc::clone(&stats)); let sut = AppendClangOutputWriter::new(writer, &input_path, true, Arc::clone(&stats)).unwrap(); sut.write(new_entries.into_iter()).unwrap(); let content = fs::read_to_string(&result_path).unwrap(); // Valid existing entry should be included assert!(content.contains("valid.cpp")); // New entry should be included assert!(content.contains("new_file.cpp")); // Invalid entry should be skipped (read_and_ignore filters it out) assert!(!content.contains("invalid.cpp")); // Only the valid existing entry was counted assert_eq!(stats.entries_read_from_existing.load(std::sync::atomic::Ordering::Relaxed), 1); } #[test] fn test_append_clang_output_writer_overwrite_existing_file() { let dir = tempfile::tempdir().unwrap(); let input_path = dir.path().join("file_to_overwrite.json"); let result_path = dir.path().join("result_file.json"); let stats = OutputStatistics::new(); // Create the original file with some entries let original_entries = vec![ clang::Entry::from_arguments_str("old_file1.cpp", vec!["clang", "-c"], "/path/to/dir", None), clang::Entry::from_arguments_str("old_file2.cpp", vec!["clang", "-c"], "/path/to/dir", None), ]; let writer = ClangOutputWriter::new(fs::File::create(&input_path).unwrap(), Arc::clone(&stats)); writer.write(original_entries.into_iter()).unwrap(); let new_entries = vec![ clang::Entry::from_arguments_str("new_file1.cpp", vec!["clang", "-c"], "/path/to/dir", None), clang::Entry::from_arguments_str("new_file2.cpp", vec!["clang", "-c"], "/path/to/dir", None), ]; let writer = ClangOutputWriter::new(fs::File::create(&result_path).unwrap(), Arc::clone(&stats)); let sut = AppendClangOutputWriter::new(writer, &input_path, false, Arc::clone(&stats)).unwrap(); sut.write(new_entries.into_iter()).unwrap(); // Verify the result file contains only new entries (no original entries) assert!(result_path.exists()); let content = fs::read_to_string(&result_path).unwrap(); assert!(content.contains("new_file1.cpp")); assert!(content.contains("new_file2.cpp")); assert!(!content.contains("old_file1.cpp")); assert!(!content.contains("old_file2.cpp")); } } rizsotto-Bear-0c30b27/crates/bear/src/output/writers/converter.rs000066400000000000000000000153231527177720300252140ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later use super::IteratorWriter; use crate::config; use crate::output::SerializationError; use crate::output::clang; use crate::output::statistics::OutputStatistics; use std::sync::Arc; use std::sync::atomic::Ordering; /// Converts `semantic::Command` instances into compilation database `Entry` objects, /// tracking pipeline statistics. pub(crate) struct ConverterClangOutputWriter> { converter: clang::CommandConverter, writer: T, stats: Arc, } impl> ConverterClangOutputWriter { pub(crate) fn new(writer: T, format: &config::Format, stats: Arc) -> Self { Self { converter: clang::CommandConverter::new(format.clone()), writer, stats } } } impl> IteratorWriter for ConverterClangOutputWriter { fn write(self, semantics: impl Iterator) -> Result<(), SerializationError> { let stats = Arc::clone(&self.stats); let dropped_stats = Arc::clone(&self.stats); let entries = semantics.flat_map(move |cmd| match self.converter.try_convert(&cmd) { Ok(entries) => entries, Err(unresolved) => { // Counted like a validation drop, so the run summary and the // all-dropped error cover it too. let dropped = unresolved.files.len(); dropped_stats.compilation_entries_produced.fetch_add(dropped, Ordering::Relaxed); dropped_stats.entries_dropped_invalid.fetch_add(dropped, Ordering::Relaxed); for file in &unresolved.files { log::warn!( "Dropping compilation database entry for file {:?}: cannot resolve directory {:?}: {}", file, unresolved.directory, unresolved.reason ); } vec![] } }); let counted_entries = entries.inspect(move |_| { stats.compilation_entries_produced.fetch_add(1, Ordering::Relaxed); }); self.writer.write(counted_entries) } } #[cfg(test)] mod tests { use super::*; use crate::output::statistics::OutputStatistics; use crate::output::writers::fixtures::CollectingWriter; use semantic::{ArgumentKind, Command, CompilerPass, PassEffect}; use std::path::PathBuf; use std::sync::atomic::Ordering; fn make_compile_command(file: &str) -> Command { Command::from_strings( "/home/user", "/usr/bin/gcc", vec![ (ArgumentKind::Compiler, vec!["/usr/bin/gcc"]), (ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling)), vec!["-c"]), (ArgumentKind::Source { binary: false }, vec![file]), ], ) } fn make_preprocessing_command() -> Command { Command::from_strings( "/home/user", "gcc", vec![ (ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Preprocessing)), vec!["-E"]), (ArgumentKind::Source { binary: false }, vec!["main.c"]), ], ) } fn make_link_only_command() -> Command { Command::from_strings( "/home/user", "gcc", vec![ (ArgumentKind::Source { binary: true }, vec!["main.o"]), (ArgumentKind::Output, vec!["-o", "program"]), ], ) } #[test] fn test_statistics_counting_compile_commands() { let stats = OutputStatistics::new(); let (writer, collected) = CollectingWriter::new(); let format = config::Format::default(); let sut = ConverterClangOutputWriter::new(writer, &format, Arc::clone(&stats)); let commands = vec![ make_compile_command("file1.c"), make_compile_command("file2.c"), make_compile_command("file3.c"), ]; sut.write(commands.into_iter()).unwrap(); assert_eq!(stats.compilation_entries_produced.load(Ordering::Relaxed), 3); let entries = collected.lock().unwrap(); assert_eq!(entries.len(), 3); assert_eq!(entries[0].file, PathBuf::from("file1.c")); assert_eq!(entries[1].file, PathBuf::from("file2.c")); assert_eq!(entries[2].file, PathBuf::from("file3.c")); } #[test] fn test_statistics_counting_preprocessing_and_link_only() { let stats = OutputStatistics::new(); let (writer, _collected) = CollectingWriter::new(); let format = config::Format::default(); let sut = ConverterClangOutputWriter::new(writer, &format, Arc::clone(&stats)); let commands = vec![make_preprocessing_command(), make_link_only_command()]; sut.write(commands.into_iter()).unwrap(); assert_eq!(stats.compilation_entries_produced.load(Ordering::Relaxed), 0); } // Requirements: output-path-format, output-json-compilation-database #[test] fn test_unresolvable_directory_counts_each_entry_as_dropped() { let stats = OutputStatistics::new(); let (writer, collected) = CollectingWriter::new(); let format = config::Format { paths: config::PathFormat { directory: config::PathResolver::Canonical, file: config::PathResolver::AsIs, }, ..config::Format::default() }; let sut = ConverterClangOutputWriter::new(writer, &format, Arc::clone(&stats)); let command = Command::from_strings( "/nonexistent/bear/working/dir", "/usr/bin/gcc", vec![ (ArgumentKind::Compiler, vec!["/usr/bin/gcc"]), (ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling)), vec!["-c"]), (ArgumentKind::Source { binary: false }, vec!["a.c"]), (ArgumentKind::Source { binary: false }, vec!["b.c"]), ], ); sut.write(std::iter::once(command)).unwrap(); assert!(collected.lock().unwrap().is_empty()); assert_eq!(stats.entries_dropped_invalid.load(Ordering::Relaxed), 2); assert_eq!(stats.compilation_entries_produced.load(Ordering::Relaxed), 2); } #[test] fn test_statistics_counting_empty_input() { let stats = OutputStatistics::new(); let (writer, _collected) = CollectingWriter::new(); let format = config::Format::default(); let sut = ConverterClangOutputWriter::new(writer, &format, Arc::clone(&stats)); sut.write(std::iter::empty()).unwrap(); assert_eq!(stats.compilation_entries_produced.load(Ordering::Relaxed), 0); } } rizsotto-Bear-0c30b27/crates/bear/src/output/writers/file.rs000066400000000000000000000071171527177720300241260ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later use super::IteratorWriter; use crate::output::SerializationError; use crate::output::clang; use crate::output::clang::serialization::JsonCompilationDatabase; use crate::output::formats::SerializationFormat; use crate::output::statistics::OutputStatistics; use std::sync::Arc; use std::sync::atomic::Ordering; use std::{fs, io}; /// The type represents a writer that writes JSON compilation database files from given entries. /// /// # Features /// - Writes the entries to a file. /// - Formats the entries to the configured shape. pub(crate) struct ClangOutputWriter { output: io::BufWriter, stats: Arc, } impl ClangOutputWriter { pub(crate) fn new(file: fs::File, stats: Arc) -> Self { Self { output: io::BufWriter::new(file), stats } } } impl IteratorWriter for ClangOutputWriter { fn write(self, entries: impl Iterator) -> Result<(), SerializationError> { write_entries(self.output, entries, self.stats) } } /// Serializes `entries` as a JSON compilation database to `output`, counting /// each entry via `stats`. The caller adds the path to an error. /// /// Shared by the file-backed [`ClangOutputWriter`] and the stdout-backed /// `ClangStdoutOutputWriter` (see `writers/stdout.rs`) so both stream through /// the identical serialization instead of duplicating it. pub(super) fn write_entries( output: impl io::Write, entries: impl Iterator, stats: Arc, ) -> Result<(), SerializationError> { // Count entries as they are written let counted_entries = entries.inspect(move |_| { stats.entries_written.fetch_add(1, Ordering::Relaxed); }); JsonCompilationDatabase::write(output, counted_entries) } #[cfg(test)] mod tests { use super::*; use crate::output::statistics::OutputStatistics; use std::sync::atomic::Ordering; #[test] fn test_create_and_write_entries() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("output.json"); let stats = OutputStatistics::new(); let entries = vec![ clang::Entry::from_arguments_str("file1.c", vec!["gcc", "-c"], "/project", None), clang::Entry::from_arguments_str("file2.c", vec!["gcc", "-c"], "/project", None), clang::Entry::from_arguments_str("file3.c", vec!["gcc", "-c"], "/project", Some("file3.o")), ]; let writer = ClangOutputWriter::new(fs::File::create(&path).unwrap(), Arc::clone(&stats)); writer.write(entries.into_iter()).unwrap(); assert_eq!(stats.entries_written.load(Ordering::Relaxed), 3); // Read back and verify let content = fs::read_to_string(&path).unwrap(); assert!(content.contains("file1.c")); assert!(content.contains("file2.c")); assert!(content.contains("file3.c")); assert!(content.contains("file3.o")); } #[test] fn test_entries_written_counter_matches_actual() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("output.json"); let stats = OutputStatistics::new(); let writer = ClangOutputWriter::new(fs::File::create(&path).unwrap(), Arc::clone(&stats)); writer.write(std::iter::empty()).unwrap(); assert_eq!(stats.entries_written.load(Ordering::Relaxed), 0); let content = fs::read_to_string(&path).unwrap(); // Should be an empty JSON array assert!(content.contains("[]") || content.trim() == "[\n]" || content.trim().starts_with('[')); } } rizsotto-Bear-0c30b27/crates/bear/src/output/writers/filtering/000077500000000000000000000000001527177720300246165ustar00rootroot00000000000000rizsotto-Bear-0c30b27/crates/bear/src/output/writers/filtering/mod.rs000066400000000000000000000142521527177720300257470ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Entry filtering for the output pipeline. //! //! This module provides the [`EntryFilter`] trait and a generic //! [`FilteredOutputWriter`] that removes entries from the stream based on //! pluggable filter implementations (source-path rules, duplicate detection). pub(crate) mod source; pub(crate) mod unique; use super::IteratorWriter; use crate::output::SerializationError; use crate::output::clang::Entry; use crate::output::statistics::OutputStatistics; use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; pub(crate) use source::SourceEntryFilter; pub(crate) use unique::DuplicateEntryFilter; /// A trait for filtering compilation database entries. /// /// Implementations decide whether an entry should be kept or removed from the /// output pipeline. Uses `&mut self` to support stateful filters (e.g. dedup). pub(crate) trait EntryFilter { fn accept(&mut self, entry: &Entry) -> bool; } /// A generic pipeline writer that filters entries using an [`EntryFilter`] /// and delegates to an inner writer. pub(crate) struct FilteredOutputWriter, F: EntryFilter> { writer: T, filter: F, stats: Arc, rejected_counter: fn(&OutputStatistics) -> &AtomicUsize, } impl, F: EntryFilter> FilteredOutputWriter { pub(crate) fn new( writer: T, filter: F, stats: Arc, rejected_counter: fn(&OutputStatistics) -> &AtomicUsize, ) -> Self { Self { writer, filter, stats, rejected_counter } } } impl, F: EntryFilter> IteratorWriter for FilteredOutputWriter { fn write(self, entries: impl Iterator) -> Result<(), SerializationError> { let mut filter = self.filter; let stats = Arc::clone(&self.stats); let rejected_counter = self.rejected_counter; let filtered_entries = entries.filter(move |entry| { let accepted = filter.accept(entry); if !accepted { rejected_counter(&stats).fetch_add(1, Ordering::Relaxed); } accepted }); self.writer.write(filtered_entries) } } #[cfg(test)] mod tests { use super::*; use crate::config; use crate::output::statistics::OutputStatistics; use crate::output::writers::fixtures::CollectingWriter; use std::sync::atomic::Ordering; #[test] fn test_filtered_writer_deduplicates_entries() { let stats = OutputStatistics::new(); let (writer, collected) = CollectingWriter::new(); let filter = DuplicateEntryFilter::try_from(config::DuplicateFilter { match_on: vec![config::OutputFields::File, config::OutputFields::Directory], }) .unwrap(); let sut = FilteredOutputWriter::new(writer, filter, Arc::clone(&stats), |s| &s.duplicates_detected); let entries = vec![ Entry::from_arguments_str("file1.c", vec!["gcc", "-c"], "/project", None), Entry::from_arguments_str("file1.c", vec!["gcc", "-c", "-Wall"], "/project", None), Entry::from_arguments_str("file2.c", vec!["gcc", "-c"], "/project", None), Entry::from_arguments_str("file1.c", vec!["gcc", "-c", "-O2"], "/project", None), ]; sut.write(entries.into_iter()).unwrap(); let result = collected.lock().unwrap(); assert_eq!(result.len(), 2); assert_eq!(result[0].file, std::path::PathBuf::from("file1.c")); assert_eq!(result[1].file, std::path::PathBuf::from("file2.c")); assert_eq!(stats.duplicates_detected.load(Ordering::Relaxed), 2); } #[test] fn test_filtered_writer_filters_by_source() { let stats = OutputStatistics::new(); let (writer, collected) = CollectingWriter::new(); let filter = SourceEntryFilter::from(config::SourceFilter { directories: vec![ config::DirectoryRule { path: std::path::PathBuf::from("src"), action: config::DirectoryAction::Include, }, config::DirectoryRule { path: std::path::PathBuf::from("/usr/include"), action: config::DirectoryAction::Exclude, }, ], files: vec![], }); let sut = FilteredOutputWriter::new(writer, filter, Arc::clone(&stats), |s| &s.entries_filtered_by_source); let entries = vec![ Entry::from_arguments_str("src/main.c", vec!["gcc", "-c"], "/project", None), Entry::from_arguments_str("/usr/include/stdio.h", vec!["gcc", "-c"], "/project", None), Entry::from_arguments_str("lib/utils.c", vec!["gcc", "-c"], "/project", None), ]; sut.write(entries.into_iter()).unwrap(); let result = collected.lock().unwrap(); assert_eq!(result.len(), 2); assert_eq!(result[0].file, std::path::PathBuf::from("src/main.c")); assert_eq!(result[1].file, std::path::PathBuf::from("lib/utils.c")); assert_eq!(stats.entries_filtered_by_source.load(Ordering::Relaxed), 1); } #[test] fn test_filtered_writer_preserves_order() { let stats = OutputStatistics::new(); let (writer, collected) = CollectingWriter::new(); let filter = DuplicateEntryFilter::try_from(config::DuplicateFilter { match_on: vec![config::OutputFields::File], }) .unwrap(); let sut = FilteredOutputWriter::new(writer, filter, Arc::clone(&stats), |s| &s.duplicates_detected); let entries = vec![ Entry::from_arguments_str("c.c", vec!["gcc", "-c"], "/project", None), Entry::from_arguments_str("a.c", vec!["gcc", "-c"], "/project", None), Entry::from_arguments_str("b.c", vec!["gcc", "-c"], "/project", None), ]; sut.write(entries.into_iter()).unwrap(); let result = collected.lock().unwrap(); assert_eq!(result.len(), 3); assert_eq!(result[0].file, std::path::PathBuf::from("c.c")); assert_eq!(result[1].file, std::path::PathBuf::from("a.c")); assert_eq!(result[2].file, std::path::PathBuf::from("b.c")); } } rizsotto-Bear-0c30b27/crates/bear/src/output/writers/filtering/source.rs000066400000000000000000000463511527177720300264750ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Source filtering for the output pipeline. //! //! This module provides both the source file filtering logic and the pipeline writer //! that uses it. The filtering system allows fine-grained control over which source //! files appear in the generated `compile_commands.json` while maintaining predictable //! and explicit behavior. //! //! ## Evaluation Strategy //! //! The source filter uses the following evaluation strategy: //! //! 1. **Order-based evaluation**: Rules are processed in the order they appear in the //! configuration. For each source file, the filter iterates through all rules and //! applies the **last** matching rule's action. //! //! 2. **Empty directories list**: If no directory rules are configured, all files are //! included (no filtering is applied). //! //! 3. **No-match behavior**: If no rule matches a file, the file is **included** by default. //! //! 4. **Path matching**: Uses simple prefix matching with `Path::starts_with()`. A rule //! matches a file if the file's path starts with the rule's path. //! //! 5. **Case sensitivity**: Path matching is always case-sensitive on all platforms. //! //! 6. **No normalization**: Performs no path normalization or canonicalization during //! matching. Paths are compared as literal strings. //! //! 7. **Directory vs. file matching**: A directory rule matches both files directly in //! that directory and files in any subdirectory (recursive matching). //! //! ## Filename-pattern rules //! //! Alongside directory rules, the source filter supports filename-glob rules that target //! machine-generated sources (Qt `moc` output, protobuf stubs, and similar): //! //! 1. **Basename vs. full-path matching**: A pattern with no path separator (`/`, or `\` on //! Windows) matches the source file's basename; a pattern containing a separator matches //! the full source path as it appears in the entry. //! 2. **Order-based evaluation**: Evaluated independently of the directory rules, using the //! same last-match-wins semantics: the last rule whose glob matches determines the verdict. //! 3. **Empty files list**: Interpreted as "include everything" (no filtering). //! 4. **No-match behavior**: If no pattern matches a file, the file is **included**. //! 5. **Composition**: An entry is emitted only when both the directory rules and the //! filename-pattern rules accept it (a logical AND of the two independent verdicts). //! 6. **Compilation**: Each `glob::Pattern` is compiled once, when the filter is built from //! configuration, not per entry. Configuration validation rejects patterns that fail to //! compile before a filter is ever built from them. use crate::config::{DirectoryAction, SourceFilter}; use crate::output::clang::Entry; use std::path::Path; // --- Source entry filter --- /// A filename-glob rule compiled once, at filter construction. #[derive(Debug)] struct CompiledFileRule { /// The compiled glob pattern. pattern: glob::Pattern, /// The action to apply when the pattern matches. action: DirectoryAction, /// Whether the pattern matches the full source path (`true`) or just the basename /// (`false`), decided once from whether the original pattern text contains a path /// separator. match_full_path: bool, } /// A filter that determines which compilation database entries should be included /// based on source file paths, directory-based rules, and filename-glob rules. #[derive(Debug)] pub(crate) struct SourceEntryFilter { /// The source filter configuration containing directory rules. config: SourceFilter, /// Filename-glob rules, compiled once from `config.files`. file_rules: Vec, } impl SourceEntryFilter { /// Determines whether a compilation database entry should be included. fn should_include(&self, entry: &Entry) -> bool { self.should_include_path(&entry.file) } /// Determines whether a file path should be included based on the configured rules. /// /// An entry is included only when both the directory rules and the filename-pattern /// rules accept it. fn should_include_path(&self, file_path: &Path) -> bool { self.directories_accept(file_path) && self.files_accept(file_path) } /// Evaluates the directory rules for a file path. fn directories_accept(&self, file_path: &Path) -> bool { // Empty directories list means include everything if self.config.directories.is_empty() { return true; } let mut result = true; // Default: include if no rule matches // Order-based evaluation: last matching rule wins for rule in &self.config.directories { if file_path.starts_with(&rule.path) { result = match rule.action { DirectoryAction::Include => true, DirectoryAction::Exclude => false, }; } } result } /// Evaluates the filename-glob rules for a file path. fn files_accept(&self, file_path: &Path) -> bool { // Empty files list means include everything if self.file_rules.is_empty() { return true; } // Computed once, reused across the rule list below. let full_path = file_path.to_string_lossy(); let basename = file_path.file_name().map(|name| name.to_string_lossy()); let mut result = true; // Default: include if no rule matches // Order-based evaluation: last matching rule wins for rule in &self.file_rules { let candidate: &str = if rule.match_full_path { &full_path } else { match &basename { Some(name) => name, // No basename (e.g. a path ending in a separator): a basename rule // has nothing to compare against, so it cannot match. None => continue, } }; if rule.pattern.matches(candidate) { result = match rule.action { DirectoryAction::Include => true, DirectoryAction::Exclude => false, }; } } result } } impl super::EntryFilter for SourceEntryFilter { fn accept(&mut self, entry: &Entry) -> bool { self.should_include(entry) } } impl From for SourceEntryFilter { fn from(config: SourceFilter) -> Self { let file_rules = config .files .iter() .map(|rule| { let match_full_path = rule.pattern.contains('/') || rule.pattern.contains(std::path::MAIN_SEPARATOR); let pattern = glob::Pattern::new(&rule.pattern) .expect("config validation rejects patterns that do not compile"); CompiledFileRule { pattern, action: rule.action, match_full_path } }) .collect(); SourceEntryFilter { config, file_rules } } } // Requirements: output-source-directory-filter #[cfg(test)] mod tests { use super::*; use crate::config::{DirectoryAction, DirectoryRule, FileRule, SourceFilter}; use crate::output::writers::filtering::EntryFilter; use std::path::PathBuf; fn create_test_entry(file_path: &str, directory: &str) -> Entry { Entry::from_arguments_str(file_path, vec!["gcc", "-c"], directory, None) } #[test] fn test_empty_directories_accepts_all() { let config = SourceFilter { directories: vec![], files: vec![] }; let mut filter = SourceEntryFilter::from(config); assert!(filter.accept(&create_test_entry("any/path.c", "/project"))); assert!(filter.accept(&create_test_entry("/absolute/path.cpp", "/project"))); assert!(filter.accept(&create_test_entry("src/main.rs", "/project"))); } #[test] fn test_order_based_evaluation() { let config = SourceFilter { directories: vec![ DirectoryRule { path: PathBuf::from("src"), action: DirectoryAction::Include }, DirectoryRule { path: PathBuf::from("src/test"), action: DirectoryAction::Exclude }, DirectoryRule { path: PathBuf::from("src/test/integration"), action: DirectoryAction::Include, }, ], files: vec![], }; let mut filter = SourceEntryFilter::from(config); assert!(filter.accept(&create_test_entry("src/main.c", "/project"))); assert!(filter.accept(&create_test_entry("src/lib/utils.c", "/project"))); assert!(!filter.accept(&create_test_entry("src/test/unit.c", "/project"))); assert!(!filter.accept(&create_test_entry("src/test/mock.c", "/project"))); assert!(filter.accept(&create_test_entry("src/test/integration/api.c", "/project"))); assert!(filter.accept(&create_test_entry("src/test/integration/db.c", "/project"))); } #[test] fn test_no_match_accepts() { let config = SourceFilter { directories: vec![ DirectoryRule { path: PathBuf::from("src"), action: DirectoryAction::Include }, DirectoryRule { path: PathBuf::from("/usr/include"), action: DirectoryAction::Exclude }, ], files: vec![], }; let mut filter = SourceEntryFilter::from(config); assert!(filter.accept(&create_test_entry("lib/external.c", "/project"))); assert!(filter.accept(&create_test_entry("vendor/third_party.cpp", "/project"))); assert!(filter.accept(&create_test_entry("/opt/custom/tool.c", "/project"))); } #[test] fn test_exact_path_matching() { let config = SourceFilter { directories: vec![DirectoryRule { path: PathBuf::from("src/main.c"), action: DirectoryAction::Exclude, }], files: vec![], }; let mut filter = SourceEntryFilter::from(config); assert!(!filter.accept(&create_test_entry("src/main.c", "/project"))); assert!(filter.accept(&create_test_entry("src/main.cpp", "/project"))); assert!(filter.accept(&create_test_entry("src/main.c.backup", "/project"))); assert!(filter.accept(&create_test_entry("src/main_test.c", "/project"))); } #[test] fn test_prefix_matching() { let config = SourceFilter { directories: vec![DirectoryRule { path: PathBuf::from("src"), action: DirectoryAction::Include }], files: vec![], }; let mut filter = SourceEntryFilter::from(config); assert!(filter.accept(&create_test_entry("src/main.c", "/project"))); assert!(filter.accept(&create_test_entry("src/lib/utils.c", "/project"))); assert!(filter.accept(&create_test_entry("src/deeply/nested/file.c", "/project"))); assert!(filter.accept(&create_test_entry("not_src/main.c", "/project"))); assert!(filter.accept(&create_test_entry("prefix_src/main.c", "/project"))); } #[cfg(unix)] #[test] fn test_case_sensitivity_unix() { let config = SourceFilter { directories: vec![DirectoryRule { path: PathBuf::from("src"), action: DirectoryAction::Exclude }], files: vec![], }; let mut filter = SourceEntryFilter::from(config); assert!(!filter.accept(&create_test_entry("src/main.c", "/project"))); assert!(filter.accept(&create_test_entry("Src/main.c", "/project"))); assert!(filter.accept(&create_test_entry("SRC/main.c", "/project"))); } #[cfg(windows)] #[test] fn test_case_sensitivity_windows() { let config = SourceFilter { directories: vec![DirectoryRule { path: PathBuf::from("src"), action: DirectoryAction::Exclude }], files: vec![], }; let mut filter = SourceEntryFilter::from(config); assert!(!filter.accept(&create_test_entry("src\\main.c", "/project"))); assert!(filter.accept(&create_test_entry("Src\\main.c", "/project"))); assert!(filter.accept(&create_test_entry("SRC\\main.c", "/project"))); } #[test] fn test_platform_separators() { #[cfg(unix)] let config = SourceFilter { directories: vec![DirectoryRule { path: PathBuf::from("src/lib"), action: DirectoryAction::Exclude, }], files: vec![], }; #[cfg(windows)] let config = SourceFilter { directories: vec![DirectoryRule { path: PathBuf::from("src\\lib"), action: DirectoryAction::Exclude, }], files: vec![], }; let mut filter = SourceEntryFilter::from(config); #[cfg(unix)] { assert!(!filter.accept(&create_test_entry("src/lib/utils.c", "/project"))); assert!(filter.accept(&create_test_entry("src\\lib\\utils.c", "/project"))); } #[cfg(windows)] { assert!(!filter.accept(&create_test_entry("src\\lib\\utils.c", "/project"))); assert!(!filter.accept(&create_test_entry("src/lib/utils.c", "/project"))); assert!(filter.accept(&create_test_entry("other/path/utils.c", "/project"))); } } #[test] fn test_complex_scenario() { let config = SourceFilter { directories: vec![ DirectoryRule { path: PathBuf::from("."), action: DirectoryAction::Include }, DirectoryRule { path: PathBuf::from("/usr/include"), action: DirectoryAction::Exclude }, DirectoryRule { path: PathBuf::from("/usr/local/include"), action: DirectoryAction::Exclude }, DirectoryRule { path: PathBuf::from("build"), action: DirectoryAction::Exclude }, DirectoryRule { path: PathBuf::from("target"), action: DirectoryAction::Exclude }, DirectoryRule { path: PathBuf::from("build/config"), action: DirectoryAction::Include }, ], files: vec![], }; let mut filter = SourceEntryFilter::from(config); assert!(filter.accept(&create_test_entry("./src/main.c", "/project"))); assert!(filter.accept(&create_test_entry("./lib/utils.c", "/project"))); assert!(!filter.accept(&create_test_entry("/usr/include/stdio.h", "/project"))); assert!(!filter.accept(&create_test_entry("/usr/local/include/boost/algorithm.hpp", "/project"))); assert!(!filter.accept(&create_test_entry("build/main.o", "/project"))); assert!(!filter.accept(&create_test_entry("target/release/app", "/project"))); assert!(filter.accept(&create_test_entry("build/config/settings.h", "/project"))); assert!(filter.accept(&create_test_entry("build/config/generated/defs.h", "/project"))); } // Requirements: output-generated-file-filter #[test] fn test_file_pattern_rules() { struct Case { name: &'static str, files: Vec, path: &'static str, expected: bool, } fn rule(pattern: &str, action: DirectoryAction) -> FileRule { FileRule { pattern: pattern.to_string(), action } } let cases = vec![ Case { name: "basename exclude drops a matching generated file", files: vec![rule("moc_*.cpp", DirectoryAction::Exclude)], path: "src/moc_window.cpp", expected: false, }, Case { name: "basename exclude leaves a non-matching file untouched", files: vec![rule("moc_*.cpp", DirectoryAction::Exclude)], path: "src/main.cpp", expected: true, }, Case { name: "last-match-wins re-includes after a broader exclude", files: vec![ rule("*.cpp", DirectoryAction::Exclude), rule("main.cpp", DirectoryAction::Include), ], path: "src/main.cpp", expected: true, }, Case { name: "a pattern with a separator matches the full path", files: vec![rule("generated/*.cpp", DirectoryAction::Exclude)], path: "generated/moc_window.cpp", expected: false, }, Case { name: "a pattern with a separator does not match by basename alone", files: vec![rule("generated/*.cpp", DirectoryAction::Exclude)], path: "src/moc_window.cpp", expected: true, }, Case { name: "a file matched by no pattern rule is included", files: vec![rule("moc_*.cpp", DirectoryAction::Exclude)], path: "src/util.cpp", expected: true, }, ]; for case in cases { let config = SourceFilter { directories: vec![], files: case.files }; let mut sut = SourceEntryFilter::from(config); let actual = sut.accept(&create_test_entry(case.path, "/project")); assert_eq!(actual, case.expected, "case: {}", case.name); } } // Requirements: output-generated-file-filter #[test] fn test_directory_and_file_rules_compose() { // Accepted by the directory rules, excluded by a file-pattern rule: dropped. let config = SourceFilter { directories: vec![DirectoryRule { path: PathBuf::from("src"), action: DirectoryAction::Include }], files: vec![FileRule { pattern: "moc_*.cpp".to_string(), action: DirectoryAction::Exclude }], }; let mut sut = SourceEntryFilter::from(config); assert!(!sut.accept(&create_test_entry("src/moc_window.cpp", "/project"))); // Excluded by the directory rules, accepted by the file-pattern rules: still dropped. let config = SourceFilter { directories: vec![DirectoryRule { path: PathBuf::from("build"), action: DirectoryAction::Exclude, }], files: vec![FileRule { pattern: "*.cpp".to_string(), action: DirectoryAction::Include }], }; let mut sut = SourceEntryFilter::from(config); assert!(!sut.accept(&create_test_entry("build/main.cpp", "/project"))); // Accepted by both rule sets: included. let config = SourceFilter { directories: vec![DirectoryRule { path: PathBuf::from("src"), action: DirectoryAction::Include }], files: vec![FileRule { pattern: "moc_*.cpp".to_string(), action: DirectoryAction::Exclude }], }; let mut sut = SourceEntryFilter::from(config); assert!(sut.accept(&create_test_entry("src/main.cpp", "/project"))); } #[cfg(windows)] // Requirements: output-generated-file-filter #[test] fn test_file_pattern_windows_separator_matches_full_path() { let config = SourceFilter { directories: vec![], files: vec![FileRule { pattern: "generated\\moc_window.cpp".to_string(), action: DirectoryAction::Exclude, }], }; let mut sut = SourceEntryFilter::from(config); assert!(!sut.accept(&create_test_entry("generated\\moc_window.cpp", "/project"))); assert!(sut.accept(&create_test_entry("other\\moc_window.cpp", "/project"))); } } rizsotto-Bear-0c30b27/crates/bear/src/output/writers/filtering/unique.rs000066400000000000000000000165141527177720300265010ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Duplicate entry filtering for the output pipeline. //! //! This module provides both the duplicate detection logic and the pipeline writer //! that uses it. The filter can be configured to use different fields of the //! compilation database entries to determine if they are duplicates. //! //! Detection compares 64-bit hashes (`DefaultHasher`/SipHash) of the configured //! fields rather than the entries themselves, so memory grows O(n) in unique //! entries while the stream is processed one entry at a time. A hash collision //! would silently drop a non-duplicate entry (a false positive); with a 64-bit //! hash this is negligible for typical databases, and trading that vanishing //! risk for the simplicity and speed of hashing is intentional. use crate::config; use crate::output::clang::Entry; use std::collections::HashSet; use std::hash::{DefaultHasher, Hash, Hasher}; use thiserror::Error; // --- Duplicate entry filter --- #[derive(Debug)] pub(crate) struct DuplicateEntryFilter { /// The fields to use for filtering duplicate entries. fields: Vec, /// The cache of seen hashes. hash_values: HashSet, } // SAFETY: the struct owns only a Vec of plain config enum values and a // HashSet. It holds no Rc, raw pointer or thread-affine state, so moving // it to another thread is sound. unsafe impl Send for DuplicateEntryFilter {} impl DuplicateEntryFilter { fn unique(&mut self, entry: &Entry) -> bool { let hash = self.hash_function(entry); if self.hash_values.contains(&hash) { return false; } self.hash_values.insert(hash); true } fn hash_function(&self, entry: &Entry) -> u64 { let mut hasher = DefaultHasher::new(); for field in &self.fields { match field { config::OutputFields::Directory => entry.directory.hash(&mut hasher), config::OutputFields::File => entry.file.hash(&mut hasher), config::OutputFields::Arguments => entry.arguments.hash(&mut hasher), config::OutputFields::Command => entry.command.hash(&mut hasher), config::OutputFields::Output => entry.output.hash(&mut hasher), } } hasher.finish() } } impl super::EntryFilter for DuplicateEntryFilter { fn accept(&mut self, entry: &Entry) -> bool { self.unique(entry) } } #[derive(Error, Debug)] pub(crate) enum ConfigurationError { #[error("Duplicate field: {0:?}")] DuplicateField(config::OutputFields), #[error("Command and Arguments cannot be both specified")] CommandAndArgumentsBothSpecified, #[error("Empty field list")] EmptyFieldList, } impl TryFrom for DuplicateEntryFilter { type Error = ConfigurationError; fn try_from(config: config::DuplicateFilter) -> Result { if config.match_on.is_empty() { return Err(ConfigurationError::EmptyFieldList); } let mut already_seen = HashSet::new(); for field in &config.match_on { if !already_seen.insert(field) { return Err(ConfigurationError::DuplicateField(*field)); } } if already_seen.contains(&config::OutputFields::Command) && already_seen.contains(&config::OutputFields::Arguments) { return Err(ConfigurationError::CommandAndArgumentsBothSpecified); } Ok(DuplicateEntryFilter { fields: config.match_on, hash_values: HashSet::new() }) } } #[cfg(test)] mod tests { use super::*; use crate::output::writers::filtering::EntryFilter; #[test] fn test_try_from_success() { let config = config::DuplicateFilter { match_on: vec![config::OutputFields::File, config::OutputFields::Directory], }; let result = DuplicateEntryFilter::try_from(config); assert!(result.is_ok()); } #[test] fn test_try_from_failure_empty_fields() { let config = config::DuplicateFilter { match_on: vec![] }; let result = DuplicateEntryFilter::try_from(config); assert!(matches!(result, Err(ConfigurationError::EmptyFieldList))); } // Requirements: output-duplicate-detection #[test] fn test_try_from_failure_duplicate_fields() { let config = config::DuplicateFilter { match_on: vec![config::OutputFields::File, config::OutputFields::File], }; let result = DuplicateEntryFilter::try_from(config); assert!(matches!(result, Err(ConfigurationError::DuplicateField(config::OutputFields::File)))); } #[test] fn test_try_from_failure_command_and_arguments() { let config = config::DuplicateFilter { match_on: vec![config::OutputFields::Command, config::OutputFields::Arguments], }; let result = DuplicateEntryFilter::try_from(config); assert!(matches!(result, Err(ConfigurationError::CommandAndArgumentsBothSpecified))); } #[test] fn test_accept_rejects_duplicates_by_file_and_directory() { let config = config::DuplicateFilter { match_on: vec![config::OutputFields::File, config::OutputFields::Directory], }; let mut sut = DuplicateEntryFilter::try_from(config).unwrap(); let entry1 = Entry::from_arguments_str( "/home/user/project/source.c", vec!["cc", "-c", "source.c"], "/home/user/project", Some("/home/user/project/source.o"), ); let entry2 = Entry::from_arguments_str( "/home/user/project/source.c", vec!["cc", "-c", "-Wall", "source.c"], "/home/user/project", Some("/home/user/project/source.o"), ); assert!(sut.accept(&entry1)); assert!(!sut.accept(&entry2)); } #[test] fn test_accept_distinguishes_by_output() { let config = config::DuplicateFilter { match_on: vec![config::OutputFields::Output] }; let mut sut = DuplicateEntryFilter::try_from(config).unwrap(); let entry1 = Entry::from_arguments_str( "/home/user/project/source.c", vec!["cc", "-c", "source.c"], "/home/user/project", Some("/home/user/project/source.o"), ); let entry2 = Entry::from_arguments_str( "/home/user/project/source.c", vec!["cc", "-c", "source.c", "-o", "test.o"], "/home/user/project", Some("/home/user/project/test.o"), ); assert!(sut.accept(&entry1)); assert!(sut.accept(&entry2)); } #[test] fn test_accept_distinguishes_by_arguments() { let config = config::DuplicateFilter { match_on: vec![config::OutputFields::Arguments] }; let mut sut = DuplicateEntryFilter::try_from(config).unwrap(); let entry1 = Entry::from_arguments_str( "/home/user/project/source.c", vec!["cc", "-c", "source.c"], "/home/user/project", Some("/home/user/project/source.o"), ); let entry2 = Entry::from_arguments_str( "/home/user/project/source.c", vec!["cc", "-c", "-Wall", "source.c"], "/home/user/project", Some("/home/user/project/source.o"), ); assert!(sut.accept(&entry1)); assert!(sut.accept(&entry2)); } } rizsotto-Bear-0c30b27/crates/bear/src/output/writers/mod.rs000066400000000000000000000330461527177720300237660ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Output writer pipeline for compilation databases. //! //! This module provides a composable pipeline of writers that transform, //! filter, and serialize compilation database entries. Each writer //! implements the `IteratorWriter` trait and wraps an inner writer, //! forming a chain of responsibility. //! //! The [`create_pipeline`] function assembles the full writer stack from //! configuration and returns an opaque writer that accepts semantic commands. mod append; mod converter; mod file; mod filtering; mod stdout; mod synthesis; mod validating; use super::statistics::OutputStatistics; use super::{AtomicFile, SerializationError, WriterCreationError, WriterError}; use crate::args::is_stdio; use crate::{args, config}; use std::path::PathBuf; use std::sync::Arc; use append::AppendClangOutputWriter; use converter::ConverterClangOutputWriter; use file::ClangOutputWriter; use filtering::{DuplicateEntryFilter, FilteredOutputWriter, SourceEntryFilter}; use stdout::ClangStdoutOutputWriter; use synthesis::HeaderEntrySynthesizer; use validating::ValidatingOutputWriter; /// A trait representing a writer for iterator type `T`. /// /// This trait is implemented by types that can consume an iterator of type `T` /// and write its elements to some output. The writing process may succeed or fail, /// returning either `()` on success or an error. pub(crate) trait IteratorWriter { /// Writes the iterator as a sequence of elements. /// /// Consumes the iterator and returns either nothing on success or an error. /// /// The error carries no path: the writers do not know which file they /// end up in, so the caller adds it. fn write(self, items: impl Iterator) -> Result<(), SerializationError>; } /// The shared validating/deduplicating/synthesizing/source-filtering stack /// that sits between a base writer and the final format converter. Both the /// file pipeline and the stdout pipeline build this identically, so /// filtering and formatting behave the same regardless of the sink. type FilteredStack = FilteredOutputWriter< HeaderEntrySynthesizer, DuplicateEntryFilter>>, SourceEntryFilter, >; /// The assembled writer pipeline type for Clang compilation databases /// written to a file: optional append, then the write to the temp file of /// an [`AtomicFile`]. type ClangWriterStack = ConverterClangOutputWriter>>; /// The assembled writer pipeline type for Clang compilation databases /// streamed to standard output: no atomic file and no append, since /// neither is possible on a stream. type ClangWriterStdoutStack = ConverterClangOutputWriter>; /// The concrete pipeline behind [`SemanticCommandWriter`]. /// /// A plain trait object is not an option here: `IteratorWriter::write` takes /// a generic `impl Iterator` parameter, which makes the trait non-object-safe. /// An enum over the two concrete pipeline shapes is the direct alternative. enum Pipeline { File(ClangWriterStack), Stdout(ClangWriterStdoutStack), } /// An opaque writer that accepts semantic commands and produces a compilation database. /// /// This struct hides the concrete pipeline type from consumers. Use [`create_pipeline`] /// to construct one. pub(crate) struct SemanticCommandWriter { inner: Pipeline, path: PathBuf, output: AtomicFile, } impl SemanticCommandWriter { /// Writes semantic commands through the pipeline. /// /// Returns the output to commit. On error the output is dropped, which /// rolls it back, and the error names the output path. pub(crate) fn write( self, semantics: impl Iterator, ) -> Result { let Self { inner, path, output } = self; match inner { Pipeline::File(writer) => writer.write(semantics), Pipeline::Stdout(writer) => writer.write(semantics), } .map_err(|err| WriterError::Io(path, err))?; Ok(output) } } /// Builds the shared validating/deduplicating/synthesizing/source-filtering /// stack on top of `base_writer`. Used by both the file and stdout branches /// of [`create_pipeline`] so dedup, header synthesis, and source filtering /// behave identically regardless of the sink. fn build_filtered_stack>( base_writer: Base, config: &config::Main, stats: Arc, ) -> Result, WriterCreationError> { let validating_writer = ValidatingOutputWriter::new(base_writer, Arc::clone(&stats)); let duplicate_filter = DuplicateEntryFilter::try_from(config.duplicates.clone()) .map_err(|err| WriterCreationError::Configuration(err.to_string()))?; let unique_writer = FilteredOutputWriter::new(validating_writer, duplicate_filter, Arc::clone(&stats), |s| { &s.duplicates_detected }); let synthesizer = HeaderEntrySynthesizer::new(unique_writer, config.headers.clone(), Arc::clone(&stats)); let source_filter_writer = FilteredOutputWriter::new( synthesizer, SourceEntryFilter::from(config.sources.clone()), Arc::clone(&stats), |s| &s.entries_filtered_by_source, ); Ok(source_filter_writer) } /// Assembles the full output writer pipeline from configuration. /// /// When `args.path` is the `-` sentinel, the pipeline streams the /// compilation database to standard output instead of a file: the atomic /// file and the append step are skipped, since neither applies /// to a stream (see [`ClangWriterStdoutStack`]). `--append` combined with /// `-` is rejected up front, since appending to a stream is impossible. /// /// Otherwise the pipeline processes semantic commands through the following /// stages: /// 1. Convert semantic commands to compilation database entries /// 2. Append entries from an existing database (if configured) /// 3. Source file path filtering /// 4. Header entry synthesis (if configured; clones a donor translation /// unit's arguments onto sibling header files) /// 5. Duplicate entry filtering /// 6. Entry validation (drop invalid entries with a warning; earlier filters /// never see an entry that will be dropped here) /// 7. Serialization to the temp file of an [`AtomicFile`], which the caller /// commits once the run succeeded pub(crate) fn create_pipeline( args: &args::BuildSemantic, config: &config::Main, stats: Arc, ) -> Result { if is_stdio(&args.path) { if args.append { return Err(WriterCreationError::Configuration("cannot append to standard output".to_string())); } let base_writer = ClangStdoutOutputWriter::new(Arc::clone(&stats)); let source_filter_writer = build_filtered_stack(base_writer, config, Arc::clone(&stats))?; let formatted_writer = ConverterClangOutputWriter::new(source_filter_writer, &config.format, Arc::clone(&stats)); return Ok(SemanticCommandWriter { inner: Pipeline::Stdout(formatted_writer), path: PathBuf::from("standard output"), output: AtomicFile::none(), }); } let final_path = &args.path; let (output, file) = AtomicFile::create(final_path)?; let base_writer = ClangOutputWriter::new(file, Arc::clone(&stats)); let source_filter_writer = build_filtered_stack(base_writer, config, Arc::clone(&stats))?; let append_writer = AppendClangOutputWriter::new(source_filter_writer, final_path, args.append, Arc::clone(&stats))?; let formatted_writer = ConverterClangOutputWriter::new(append_writer, &config.format, Arc::clone(&stats)); Ok(SemanticCommandWriter { inner: Pipeline::File(formatted_writer), path: final_path.clone(), output }) } #[cfg(test)] mod fixtures { use super::{IteratorWriter, SerializationError}; /// A test writer that collects all entries into a shared vector. /// /// This allows tests to verify exactly which entries pass through a writer /// pipeline, including their contents and ordering. When inspection is not /// needed, simply ignore the collected vector. pub(crate) struct CollectingWriter { pub collected: std::sync::Arc>>, } impl CollectingWriter { pub fn new() -> (Self, std::sync::Arc>>) { let collected = std::sync::Arc::new(std::sync::Mutex::new(Vec::new())); (Self { collected: std::sync::Arc::clone(&collected) }, collected) } } impl IteratorWriter for CollectingWriter { fn write( self, entries: impl Iterator, ) -> Result<(), SerializationError> { let mut collected = self.collected.lock().unwrap(); collected.extend(entries); Ok(()) } } } #[cfg(test)] mod tests { use super::*; use crate::config; use semantic::{ArgumentKind, Command, CompilerPass, PassEffect}; use std::sync::atomic::Ordering; fn make_compile_command(file: &str) -> semantic::Command { Command::from_strings( "/home/user", "/usr/bin/gcc", vec![ (ArgumentKind::Compiler, vec!["/usr/bin/gcc"]), (ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling)), vec!["-c"]), (ArgumentKind::Source { binary: false }, vec![file]), ], ) } #[test] fn test_create_pipeline_writes_entries() { let dir = tempfile::tempdir().unwrap(); let output_path = dir.path().join("compile_commands.json"); let config = config::Main::default(); let args = args::BuildSemantic { path: output_path.clone(), append: false }; let stats = OutputStatistics::new(); let pipeline = create_pipeline(&args, &config, Arc::clone(&stats)).unwrap(); let commands = vec![make_compile_command("file1.c"), make_compile_command("file2.c")]; pipeline.write(commands.into_iter()).unwrap().commit().unwrap(); assert!(output_path.exists()); let content = std::fs::read_to_string(&output_path).unwrap(); assert!(content.contains("file1.c")); assert!(content.contains("file2.c")); assert_eq!(stats.compilation_entries_produced.load(Ordering::Relaxed), 2); assert_eq!(stats.entries_written.load(Ordering::Relaxed), 2); assert_eq!(stats.duplicates_detected.load(Ordering::Relaxed), 0); assert_eq!(stats.entries_filtered_by_source.load(Ordering::Relaxed), 0); } #[test] fn test_create_pipeline_deduplicates_entries() { let dir = tempfile::tempdir().unwrap(); let output_path = dir.path().join("compile_commands.json"); let config = config::Main::default(); let args = args::BuildSemantic { path: output_path.clone(), append: false }; let stats = OutputStatistics::new(); let pipeline = create_pipeline(&args, &config, Arc::clone(&stats)).unwrap(); let commands = vec![ make_compile_command("file1.c"), make_compile_command("file1.c"), make_compile_command("file2.c"), ]; pipeline.write(commands.into_iter()).unwrap().commit().unwrap(); assert_eq!(stats.compilation_entries_produced.load(Ordering::Relaxed), 3); assert_eq!(stats.duplicates_detected.load(Ordering::Relaxed), 1); assert_eq!(stats.entries_written.load(Ordering::Relaxed), 2); } #[test] fn test_create_pipeline_filters_by_source() { let dir = tempfile::tempdir().unwrap(); let output_path = dir.path().join("compile_commands.json"); let config = config::Main { sources: config::SourceFilter { directories: vec![config::DirectoryRule { path: std::path::PathBuf::from("/usr/include"), action: config::DirectoryAction::Exclude, }], files: vec![], }, ..config::Main::default() }; let args = args::BuildSemantic { path: output_path.clone(), append: false }; let stats = OutputStatistics::new(); let pipeline = create_pipeline(&args, &config, Arc::clone(&stats)).unwrap(); let commands = vec![make_compile_command("src/main.c"), make_compile_command("/usr/include/stdio.h")]; pipeline.write(commands.into_iter()).unwrap().commit().unwrap(); assert_eq!(stats.entries_filtered_by_source.load(Ordering::Relaxed), 1); assert_eq!(stats.entries_written.load(Ordering::Relaxed), 1); let content = std::fs::read_to_string(&output_path).unwrap(); assert!(content.contains("src/main.c")); assert!(!content.contains("stdio.h")); } #[test] fn test_create_pipeline_empty_input() { let dir = tempfile::tempdir().unwrap(); let output_path = dir.path().join("compile_commands.json"); let config = config::Main::default(); let args = args::BuildSemantic { path: output_path.clone(), append: false }; let stats = OutputStatistics::new(); let pipeline = create_pipeline(&args, &config, Arc::clone(&stats)).unwrap(); pipeline.write(std::iter::empty()).unwrap().commit().unwrap(); assert!(output_path.exists()); assert_eq!(stats.semantic_commands_received.load(Ordering::Relaxed), 0); assert_eq!(stats.entries_written.load(Ordering::Relaxed), 0); } } rizsotto-Bear-0c30b27/crates/bear/src/output/writers/stdout.rs000066400000000000000000000021721527177720300245250ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later use super::IteratorWriter; use super::file::write_entries; use crate::output::SerializationError; use crate::output::clang; use crate::output::statistics::OutputStatistics; use std::io; use std::sync::Arc; /// The type represents a writer that writes JSON compilation database /// entries directly to standard output, for `bear semantic --output -`. /// /// Unlike [`super::file::ClangOutputWriter`], there is no temp file or /// rename step: a stream has no atomic-replace semantics and cannot be /// appended to, so this writer is only ever the base of a pipeline that /// skips the atomic and append decorators. pub(crate) struct ClangStdoutOutputWriter { stats: Arc, } impl ClangStdoutOutputWriter { pub(crate) fn new(stats: Arc) -> Self { Self { stats } } } impl IteratorWriter for ClangStdoutOutputWriter { fn write(self, entries: impl Iterator) -> Result<(), SerializationError> { let output = io::BufWriter::new(io::stdout()); write_entries(output, entries, self.stats) } } rizsotto-Bear-0c30b27/crates/bear/src/output/writers/synthesis.rs000066400000000000000000001024211527177720300252320ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Header entry synthesis for the output pipeline. //! //! When enabled via configuration, this stage clones the compile flags of a //! compiled translation unit onto header files discovered on disk, so that //! editors and linters can resolve compile flags for headers as well as //! sources. See `docs/requirements/output-header-entries.md`. //! //! Two discovery strategies are implemented here: //! //! - `Siblings`: header files that live in the same directory as a compiled //! C, C++, or Objective-C source. //! - `DependencyFiles`: reads the make-style dependency file (`.d`) a //! donor's build already emitted, and synthesizes an entry per header //! prerequisite listed there, scoped to headers that resolve inside the //! compilation's working directory. //! //! Both strategies stream: entries observed as they pass through are //! recorded, and the synthesized header entries are emitted once, in an //! epilogue, after the input iterator is exhausted. See //! [`HeaderCollector`]. use crate::config::{HeaderStrategy, Headers}; use crate::output::SerializationError; use crate::output::clang::Entry; use crate::output::statistics::OutputStatistics; use semantic::{is_c_family_source, is_header_file, looks_like_a_source_file}; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; use std::rc::Rc; use std::sync::Arc; use std::sync::atomic::Ordering; use super::IteratorWriter; /// Collects donor observations while entries stream through, then yields the /// synthesized header entries once the stream is exhausted. Implementations /// hold whatever state they need (e.g. one donor per directory, or a /// dedup set) so that memory stays proportional to the number of donors or /// directories considered, not to the number of entries in the database. trait HeaderCollector { /// Called once per entry as it streams through, in order. fn observe(&mut self, entry: &Entry); /// Called once, after all entries have been observed, to produce the /// synthesized header entries. fn synthesize(&self) -> Vec; } /// Synthesizes compilation database entries for header files by cloning a /// donor translation unit's arguments, per the configured discovery strategy. pub(crate) struct HeaderEntrySynthesizer> { writer: T, config: Headers, stats: Arc, } impl> HeaderEntrySynthesizer { pub(crate) fn new(writer: T, config: Headers, stats: Arc) -> Self { Self { writer, config, stats } } /// Drives `collector` over `entries`: each entry is observed as it /// streams through, and once the stream is exhausted, an epilogue asks /// the collector to synthesize the header entries and appends them. fn write_with_collector( self, entries: impl Iterator, collector: impl HeaderCollector + 'static, ) -> Result<(), SerializationError> { let collector = Rc::new(RefCell::new(collector)); let collector1 = Rc::clone(&collector); let collector2 = Rc::clone(&collector); let stats = Arc::clone(&self.stats); let recording = entries.inspect(move |entry| { collector1.borrow_mut().observe(entry); }); let epilogue = std::iter::once_with(move || { let synthesized = collector2.borrow().synthesize(); stats.entries_synthesized.fetch_add(synthesized.len(), Ordering::Relaxed); synthesized }) .flatten(); self.writer.write(recording.chain(epilogue)) } } impl> IteratorWriter for HeaderEntrySynthesizer { fn write(self, entries: impl Iterator) -> Result<(), SerializationError> { if !self.config.enabled { return self.writer.write(entries); } match self.config.strategy { HeaderStrategy::Siblings => self.write_with_collector(entries, DirectoryCollector::new()), HeaderStrategy::DependencyFiles => self.write_with_collector(entries, DepFilesCollector::new()), } } } /// One scan target: the donor whose arguments are cloned, plus how the /// compiler refers to this directory (used to build header file paths). struct DirRecord { arguments: Vec, directory: PathBuf, source_file: PathBuf, display_dir: PathBuf, } /// Collects the first eligible donor observed per source directory, then /// synthesizes header entries for each recorded directory's header files. struct DirectoryCollector { /// Keyed by the physical directory to scan for headers. records: HashMap, } impl DirectoryCollector { fn new() -> Self { Self { records: HashMap::new() } } /// Records `entry`'s own directory as a scan target, unless it is /// command-form (no `arguments` to clone) or not a C-family source. /// First-seen donor wins per physical directory. fn observe(&mut self, entry: &Entry) { if entry.arguments.is_empty() { return; } if !is_c_family_source(&entry.file) { return; } let physical = physical_parent(&entry.directory, &entry.file); let display = entry.file.parent().map(Path::to_path_buf).unwrap_or_default(); self.record(physical, entry, display); } fn record(&mut self, physical_dir: PathBuf, entry: &Entry, display_dir: PathBuf) { self.records.entry(physical_dir).or_insert_with(|| DirRecord { arguments: entry.arguments.clone(), directory: entry.directory.clone(), source_file: entry.file.clone(), display_dir, }); } /// Scans each recorded directory once and synthesizes an entry per header /// file found there, cloning that directory's donor arguments. The /// header's `file` path is built from the recorded display prefix, so an /// include-dir header is reported through the same path the compiler /// used to reach it (e.g. `-Iinclude` -> `include/util.h`). fn synthesize(&self) -> Vec { let mut result = Vec::new(); for (physical_dir, rec) in &self.records { let read_dir = match std::fs::read_dir(physical_dir) { Ok(read_dir) => read_dir, Err(err) => { log::debug!("Skipping header synthesis for {:?}: {}", physical_dir, err); continue; } }; let mut header_names: Vec = read_dir .filter_map(|entry| entry.ok()) .filter(|entry| entry.file_type().is_ok_and(|ft| ft.is_file())) .map(|entry| entry.file_name()) .filter(|name| is_header_file(Path::new(name))) .collect(); header_names.sort(); for name in header_names { let header_file = join_header_path(&rec.display_dir, &name, &rec.source_file); let Some(rewritten) = rewrite_arguments(&rec.arguments, &rec.source_file, &header_file) else { log::debug!( "Skipping header synthesis for directory {:?}: could not locate a source token in donor arguments", physical_dir ); break; }; result.push(Entry::with_arguments( header_file, rewritten, rec.directory.clone(), None::, )); } } result } } impl HeaderCollector for DirectoryCollector { fn observe(&mut self, entry: &Entry) { DirectoryCollector::observe(self, entry) } fn synthesize(&self) -> Vec { DirectoryCollector::synthesize(self) } } /// One eligible donor: its own compile flags, and the dependency file its /// build is expected to have emitted. The dependency file is a build /// artifact that does not exist yet at the time its compiler invocation is /// intercepted (interception happens before the compiler runs, not after), /// so it must be read later, in the epilogue - never during `observe`. struct DepRecord { arguments: Vec, directory: PathBuf, source_file: PathBuf, dep_path: PathBuf, } /// Collects, per eligible donor, the dependency file (`.d`) its build is /// expected to emit, then - once all entries have streamed through, in the /// epilogue - reads each recorded dependency file and synthesizes an entry /// for each in-project header prerequisite listed there. Unlike /// [`DirectoryCollector`], no directory is scanned: the exact header set /// comes from the dependency file, so headers reached from other /// directories (e.g. via `-I`) are picked up precisely, without scanning /// those directories wholesale. struct DepFilesCollector { donors: Vec, } impl DepFilesCollector { fn new() -> Self { Self { donors: Vec::new() } } } impl HeaderCollector for DepFilesCollector { /// Records `entry` as a donor, with the dependency file its build is /// expected to have emitted, unless it is command-form (no `arguments` /// to clone), not a C-family source, or its arguments do not locate a /// dependency file at all. fn observe(&mut self, entry: &Entry) { if entry.arguments.is_empty() { return; } if !is_c_family_source(&entry.file) { return; } let Some(dep_rel) = locate_dep_file(&entry.arguments) else { return; }; let dep_path = if dep_rel.is_absolute() { dep_rel } else { entry.directory.join(&dep_rel) }; self.donors.push(DepRecord { arguments: entry.arguments.clone(), directory: entry.directory.clone(), source_file: entry.file.clone(), dep_path, }); } /// Reads each recorded donor's dependency file once and synthesizes an /// entry per in-project header prerequisite listed there, deduplicated /// across donors that share a header. fn synthesize(&self) -> Vec { let mut seen: HashSet<(PathBuf, PathBuf)> = HashSet::new(); let mut result = Vec::new(); for donor in &self.donors { let content = match std::fs::read_to_string(&donor.dep_path) { Ok(content) => content, Err(err) => { log::debug!( "Skipping header synthesis for dependency file {:?}: {}", donor.dep_path, err ); continue; } }; // The compilation's own working directory is the reference frame: // dependency-file paths are relative to it, and a header is in // scope only when it resolves inside that directory. System headers // (absolute, or reached via `..`) are left out. let base = lexical_normalize(&donor.directory); for prereq in parse_make_prerequisites(&content) { if !is_header_file(&prereq) { // The source file itself is also listed as a prerequisite. continue; } let raw = if prereq.is_absolute() { prereq.clone() } else { donor.directory.join(&prereq) }; let phys = lexical_normalize(&raw); if !phys.starts_with(&base) { continue; } if !seen.insert((donor.directory.clone(), prereq.clone())) { continue; } if let Some(args) = rewrite_arguments(&donor.arguments, &donor.source_file, &prereq) { result.push(Entry::with_arguments( prereq.clone(), args, donor.directory.clone(), None::, )); } } } result } } /// Locates the dependency file a donor's build would have emitted, from its /// own compile arguments: prefers an explicit `-MF ` (or glued /// `-MF`); else derives it from the object output (`-o ` -> /// `.d`); else from the target name (`-MT ` -> `.d`). /// Returns `None` when no dependency file can be located. fn locate_dep_file(args: &[String]) -> Option { if let Some(value) = extract_flag_value(args, "-MF") { return Some(PathBuf::from(value)); } if let Some(value) = extract_flag_value(args, "-o") { return Some(PathBuf::from(value).with_extension("d")); } if let Some(value) = extract_flag_value(args, "-MT") { return Some(PathBuf::from(value).with_extension("d")); } None } /// Extracts the value of `flag` from `args`, in both separate (`flag value`) /// and glued (`flagvalue`) forms. Returns the first match. fn extract_flag_value(args: &[String], flag: &str) -> Option { let mut iter = args.iter(); while let Some(arg) = iter.next() { if arg == flag { return iter.next().cloned(); } if let Some(rest) = arg.strip_prefix(flag) && !rest.is_empty() { return Some(rest.to_string()); } } None } /// Parses the prerequisites of the first rule in a make-style dependency /// file (as emitted by `-MD`/`-MMD`/`-MF`). `-MP` emits extra phony rules /// for each header on later lines (`header:` with no prerequisites), which /// are not the first rule and are ignored here. fn parse_make_prerequisites(content: &str) -> Vec { // Unfold line continuations (backslash-newline) into spaces so the // first rule becomes one logical line. let unfolded = content.replace("\\\r\n", " ").replace("\\\n", " "); let first_line = unfolded.lines().next().unwrap_or(""); let Some((_, prereqs)) = first_line.split_once(':') else { return Vec::new() }; // Tokenize on unescaped whitespace, treating "\ " as a literal space // (make escapes spaces in filenames this way). let mut tokens = Vec::new(); let mut current = String::new(); let mut chars = prereqs.chars().peekable(); while let Some(c) = chars.next() { match c { '\\' if chars.peek() == Some(&' ') => { current.push(' '); chars.next(); } c if c.is_whitespace() => { if !current.is_empty() { tokens.push(PathBuf::from(std::mem::take(&mut current))); } } c => current.push(c), } } if !current.is_empty() { tokens.push(PathBuf::from(current)); } tokens } /// Lexically normalizes a path by folding `.` and `..` components without /// touching the filesystem. The dependency-files scope check relies on this so /// a `..` sequence cannot textually escape the working directory (e.g. a /// prerequisite of `../../shared/foo.h` resolving outside it must not pass the /// guard). Symlinks are deliberately not resolved: that needs disk access and /// would fail on directories the build has not created. fn lexical_normalize(path: &Path) -> PathBuf { use std::path::Component; let mut out = PathBuf::new(); for component in path.components() { match component { Component::CurDir => {} Component::ParentDir => { if matches!(out.components().next_back(), Some(Component::Normal(_))) { out.pop(); } else { out.push(".."); } } other => out.push(other.as_os_str()), } } out } /// Joins a directory prefix and a header file name into the header's path. /// /// The separator mirrors the donor's own paths rather than the host OS /// separator, so a synthesized entry reads like the compilation it clones and /// its `file`/argument strings stay stable across platforms (`PathBuf::join` /// would emit a backslash on Windows even when the donor used `/`). POSIX `/` /// is the default; `dir`'s own separators, when it has any, are left intact. /// `sample` is the donor's source file, consulted for the style only when /// `dir` is a single component with no separator of its own. fn join_header_path(dir: &Path, name: &std::ffi::OsStr, sample: &Path) -> PathBuf { if dir.as_os_str().is_empty() { return PathBuf::from(name); } let dir = dir.to_string_lossy(); let backslash = if dir.contains('/') { false } else if dir.contains('\\') { true } else { let sample = sample.to_string_lossy(); sample.contains('\\') && !sample.contains('/') }; let separator = if backslash { '\\' } else { '/' }; PathBuf::from(format!("{dir}{separator}{}", name.to_string_lossy())) } /// Resolves the physical directory a source file lives in, given the entry's /// working directory and (possibly relative) file path. fn physical_parent(dir: &Path, file: &Path) -> PathBuf { let joined = if file.is_absolute() { file.to_path_buf() } else { dir.join(file) }; joined.parent().map(Path::to_path_buf).unwrap_or_else(|| dir.to_path_buf()) } /// Clone `args`, replace the source-file token with `to_file`, and drop the /// output-file flag. Returns None when no source token can be located. fn rewrite_arguments(args: &[String], from_file: &Path, to_file: &Path) -> Option> { let idx = source_token_index(args, from_file)?; let mut out: Vec = args.to_vec(); out[idx] = to_file.to_string_lossy().into_owned(); Some(remove_output_flag(out)) } /// Index of the token to replace: the exact match for `from_file`, else the /// unique token that looks like a source file. None if neither resolves. fn source_token_index(args: &[String], from_file: &Path) -> Option { if let Some(idx) = args.iter().position(|a| Path::new(a) == from_file || a.as_str() == from_file.to_string_lossy()) { return Some(idx); } let mut candidate = None; for (idx, arg) in args.iter().enumerate() { if looks_like_a_source_file(arg) { if candidate.is_some() { // More than one candidate: ambiguous, cannot resolve. return None; } candidate = Some(idx); } } candidate } /// Drop `-o ` (two tokens) and glued `-o` (one token). fn remove_output_flag(args: Vec) -> Vec { let mut out = Vec::with_capacity(args.len()); let mut iter = args.into_iter(); while let Some(arg) = iter.next() { if arg == "-o" { iter.next(); continue; } if arg.starts_with("-o") && arg.len() > 2 { continue; } out.push(arg); } out } #[cfg(test)] mod tests { use super::*; use crate::output::writers::fixtures::CollectingWriter; mod fixture { use tempfile::TempDir; /// Owns a temp directory laid out with a source file and a few /// siblings (headers and a non-header file), so tests can exercise /// disk-scanning behavior without repeating the scaffolding. pub(super) struct Fixture { pub dir: TempDir, } impl Fixture { pub(super) fn new() -> Self { let dir = tempfile::tempdir().unwrap(); let src = dir.path().join("src"); std::fs::create_dir_all(&src).unwrap(); std::fs::write(src.join("main.c"), "").unwrap(); std::fs::write(src.join("util.h"), "").unwrap(); std::fs::write(src.join("helper.hpp"), "").unwrap(); std::fs::write(src.join("readme.txt"), "").unwrap(); Self { dir } } pub(super) fn path(&self) -> &std::path::Path { self.dir.path() } } } // --- rewrite_arguments --- #[test] fn test_rewrite_arguments() { struct Case { name: &'static str, args: Vec<&'static str>, from_file: &'static str, to_file: &'static str, expected: Option>, } let cases = vec![ Case { name: "exact source token match", args: vec!["cc", "-c", "src/main.c", "-o", "src/main.o"], from_file: "src/main.c", to_file: "src/util.h", expected: Some(vec!["cc", "-c", "src/util.h"]), }, Case { name: "glued output flag is dropped", args: vec!["cc", "-c", "src/main.c", "-osrc/main.o"], from_file: "src/main.c", to_file: "src/util.h", expected: Some(vec!["cc", "-c", "src/util.h"]), }, Case { name: "separate output flag is dropped", args: vec!["cc", "-c", "src/main.c", "-o", "src/main.o"], from_file: "src/main.c", to_file: "src/helper.hpp", expected: Some(vec!["cc", "-c", "src/helper.hpp"]), }, Case { name: "no matching source token yields None", args: vec!["cc", "-c", "-o", "src/main.o"], from_file: "src/main.c", to_file: "src/util.h", expected: None, }, ]; for case in cases { let args: Vec = case.args.iter().map(|s| s.to_string()).collect(); let from_file = Path::new(case.from_file); let to_file = Path::new(case.to_file); let sut = rewrite_arguments(&args, from_file, to_file); let expected = case.expected.map(|v| v.into_iter().map(String::from).collect::>()); assert_eq!(sut, expected, "case: {}", case.name); if let Some(actual) = &sut { assert!(!actual.iter().any(|a| a == "-o" || a.starts_with("-o") && a.len() > 2)); } } } // --- join_header_path --- #[test] fn test_join_header_path_mirrors_donor_separator() { struct Case { name: &'static str, dir: &'static str, file: &'static str, sample: &'static str, expected: &'static str, } // String-based, so the outcome is the same on every platform (this is // the whole point: not the host OS separator). Verifies the Windows // behavior from any host. let cases = [ Case { name: "forward-slash donor keeps forward slashes", dir: "src", file: "util.h", sample: "src/main.c", expected: "src/util.h", }, Case { name: "backslash donor keeps backslashes", dir: "src", file: "util.h", sample: "src\\main.c", expected: "src\\util.h", }, Case { name: "include-dir prefix keeps forward slashes", dir: "include", file: "util.h", sample: "src/main.c", expected: "include/util.h", }, Case { name: "separator already in the prefix is respected", dir: "a/b", file: "x.h", sample: "irrelevant", expected: "a/b/x.h", }, Case { name: "empty prefix yields the bare name", dir: "", file: "util.h", sample: "main.c", expected: "util.h", }, ]; for case in cases { let sut = join_header_path( Path::new(case.dir), std::ffi::OsStr::new(case.file), Path::new(case.sample), ); assert_eq!(sut.to_string_lossy(), case.expected, "case: {}", case.name); } } // --- HeaderEntrySynthesizer / siblings strategy --- fn enabled_config() -> Headers { Headers { enabled: true, strategy: HeaderStrategy::Siblings } } fn donor_entry(dir: &std::path::Path) -> Entry { Entry::from_arguments_str( "src/main.c", vec!["cc", "-c", "src/main.c", "-o", "src/main.o"], dir.to_str().unwrap(), None, ) } #[test] fn test_synthesizes_header_entries_for_siblings() { let fixture = fixture::Fixture::new(); let stats = OutputStatistics::new(); let (writer, collected) = CollectingWriter::new(); let sut = HeaderEntrySynthesizer::new(writer, enabled_config(), Arc::clone(&stats)); sut.write(vec![donor_entry(fixture.path())].into_iter()).unwrap(); let out = collected.lock().unwrap(); assert_eq!(out.len(), 3, "expected donor + 2 synthesized header entries: {:?}", *out); assert_eq!(out[0].file, PathBuf::from("src/main.c")); assert_eq!(out[1].file, PathBuf::from("src/helper.hpp")); assert_eq!(out[1].arguments, vec!["cc", "-c", "src/helper.hpp"]); assert_eq!(out[1].directory, fixture.path()); assert_eq!(out[1].output, None); assert!(out[1].command.is_empty()); assert_eq!(out[2].file, PathBuf::from("src/util.h")); assert_eq!(out[2].arguments, vec!["cc", "-c", "src/util.h"]); assert_eq!(out[2].directory, fixture.path()); assert_eq!(out[2].output, None); assert!(out[2].command.is_empty()); assert!(!out.iter().any(|e| e.file == Path::new("src/readme.txt"))); assert_eq!(stats.entries_synthesized.load(Ordering::Relaxed), 2); } #[test] fn test_disabled_by_default_passes_through_only() { let fixture = fixture::Fixture::new(); let stats = OutputStatistics::new(); let (writer, collected) = CollectingWriter::new(); let config = Headers { enabled: false, strategy: HeaderStrategy::Siblings }; let sut = HeaderEntrySynthesizer::new(writer, config, Arc::clone(&stats)); sut.write(vec![donor_entry(fixture.path())].into_iter()).unwrap(); let out = collected.lock().unwrap(); assert_eq!(out.len(), 1); assert_eq!(out[0].file, PathBuf::from("src/main.c")); assert_eq!(stats.entries_synthesized.load(Ordering::Relaxed), 0); } #[test] fn test_command_form_donor_is_not_eligible() { let fixture = fixture::Fixture::new(); let stats = OutputStatistics::new(); let (writer, collected) = CollectingWriter::new(); let sut = HeaderEntrySynthesizer::new(writer, enabled_config(), Arc::clone(&stats)); let entry = Entry::from_command_str("src/main.c", "cc -c src/main.c", fixture.path().to_str().unwrap(), None); sut.write(vec![entry].into_iter()).unwrap(); let out = collected.lock().unwrap(); assert_eq!(out.len(), 1); assert_eq!(stats.entries_synthesized.load(Ordering::Relaxed), 0); } // Requirements: output-header-entries #[test] fn test_non_c_family_donor_is_not_eligible() { let fixture = fixture::Fixture::new(); std::fs::write(fixture.path().join("src").join("thing.swift"), "").unwrap(); let stats = OutputStatistics::new(); let (writer, collected) = CollectingWriter::new(); let sut = HeaderEntrySynthesizer::new(writer, enabled_config(), Arc::clone(&stats)); let entry = Entry::from_arguments_str( "src/thing.swift", vec!["swiftc", "-c", "src/thing.swift"], fixture.path().to_str().unwrap(), None, ); sut.write(vec![entry].into_iter()).unwrap(); let out = collected.lock().unwrap(); assert_eq!(out.len(), 1); assert_eq!(stats.entries_synthesized.load(Ordering::Relaxed), 0); } // --- parse_make_prerequisites --- #[test] fn test_parse_make_prerequisites() { struct Case { name: &'static str, content: &'static str, expected: Vec<&'static str>, } let cases = vec![ Case { name: "single line", content: "main.o: main.c util.h\n", expected: vec!["main.c", "util.h"], }, Case { name: "line continuation joins two lines", content: "main.o: main.c \\\n util.h\n", expected: vec!["main.c", "util.h"], }, Case { name: "escaped space in a filename", content: "main.o: main.c a\\ b.h\n", expected: vec!["main.c", "a b.h"], }, Case { name: "-MP phony rules on later lines are ignored", content: "main.o: main.c util.h\n\nutil.h:\n", expected: vec!["main.c", "util.h"], }, Case { name: "no ':' yields empty", content: "not a rule at all\n", expected: vec![] }, ]; for case in cases { let sut = parse_make_prerequisites(case.content); let expected: Vec = case.expected.iter().map(PathBuf::from).collect(); assert_eq!(sut, expected, "case: {}", case.name); } } // --- locate_dep_file --- #[test] fn test_locate_dep_file() { struct Case { name: &'static str, args: Vec<&'static str>, expected: Option<&'static str>, } let cases = vec![ Case { name: "explicit -MF", args: vec!["cc", "-c", "main.c", "-MF", "dep.d", "-o", "main.o"], expected: Some("dep.d"), }, Case { name: "glued -MF", args: vec!["cc", "-c", "main.c", "-MFdep.d", "-o", "main.o"], expected: Some("dep.d"), }, Case { name: "derived from -o", args: vec!["cc", "-c", "main.c", "-o", "main.o"], expected: Some("main.d"), }, Case { name: "derived from -MT", args: vec!["cc", "-c", "main.c", "-MT", "main.o"], expected: Some("main.d"), }, Case { name: "none present", args: vec!["cc", "-c", "main.c"], expected: None }, Case { name: "-MF wins over -o", args: vec!["cc", "-c", "main.c", "-MF", "custom.d", "-o", "main.o"], expected: Some("custom.d"), }, ]; for case in cases { let args: Vec = case.args.iter().map(|s| s.to_string()).collect(); let sut = locate_dep_file(&args); let expected = case.expected.map(PathBuf::from); assert_eq!(sut, expected, "case: {}", case.name); } } // --- HeaderEntrySynthesizer / dependency-files strategy --- fn dependency_files_config() -> Headers { Headers { enabled: true, strategy: HeaderStrategy::DependencyFiles } } #[test] fn test_synthesizes_header_entries_for_dependency_files() { let dir = tempfile::tempdir().unwrap(); let root = dir.path(); std::fs::create_dir_all(root.join("src")).unwrap(); std::fs::write(root.join("src").join("main.c"), "").unwrap(); std::fs::write(root.join("src").join("util.h"), "").unwrap(); std::fs::write(root.join("main.d"), "main.o: src/main.c src/util.h /usr/include/stdio.h\n").unwrap(); let stats = OutputStatistics::new(); let (writer, collected) = CollectingWriter::new(); let sut = HeaderEntrySynthesizer::new(writer, dependency_files_config(), Arc::clone(&stats)); let entry = Entry::from_arguments_str( "src/main.c", vec!["cc", "-c", "src/main.c", "-MF", "main.d", "-o", "src/main.o"], root.to_str().unwrap(), None, ); sut.write(vec![entry].into_iter()).unwrap(); let out = collected.lock().unwrap(); assert_eq!(out.len(), 2, "expected donor + 1 synthesized header entry: {:?}", *out); assert_eq!(out[0].file, PathBuf::from("src/main.c")); assert_eq!(out[1].file, PathBuf::from("src/util.h")); assert_eq!(out[1].arguments, vec!["cc", "-c", "src/util.h", "-MF", "main.d"]); assert_eq!(out[1].directory, root); assert_eq!(out[1].output, None); assert_eq!(stats.entries_synthesized.load(Ordering::Relaxed), 1); } #[test] fn test_dependency_files_missing_dep_file_synthesizes_nothing() { let dir = tempfile::tempdir().unwrap(); let root = dir.path(); std::fs::create_dir_all(root.join("src")).unwrap(); std::fs::write(root.join("src").join("main.c"), "").unwrap(); let stats = OutputStatistics::new(); let (writer, collected) = CollectingWriter::new(); let sut = HeaderEntrySynthesizer::new(writer, dependency_files_config(), Arc::clone(&stats)); let entry = Entry::from_arguments_str( "src/main.c", vec!["cc", "-c", "src/main.c", "-MF", "missing.d", "-o", "src/main.o"], root.to_str().unwrap(), None, ); sut.write(vec![entry].into_iter()).unwrap(); let out = collected.lock().unwrap(); assert_eq!(out.len(), 1, "expected donor only, no synthesized entries: {:?}", *out); assert_eq!(stats.entries_synthesized.load(Ordering::Relaxed), 0); } } rizsotto-Bear-0c30b27/crates/bear/src/output/writers/validating.rs000066400000000000000000000110631527177720300253240ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later use super::IteratorWriter; use crate::output::SerializationError; use crate::output::clang::Entry; use crate::output::statistics::OutputStatistics; use std::sync::Arc; use std::sync::atomic::Ordering; /// Drops entries that fail `Entry::validate`, logs each drop at `WARN` level, /// and increments `OutputStatistics::entries_dropped_invalid`. /// /// Placed last in the pipeline so earlier filter stages (duplicate, source) /// never see an entry that will be dropped. Downstream writers /// (`ClangOutputWriter`) may therefore assume every entry they receive is /// valid per `Entry::validate`. pub(crate) struct ValidatingOutputWriter> { writer: T, stats: Arc, } impl> ValidatingOutputWriter { pub(crate) fn new(writer: T, stats: Arc) -> Self { Self { writer, stats } } } impl> IteratorWriter for ValidatingOutputWriter { fn write(self, entries: impl Iterator) -> Result<(), SerializationError> { let stats = Arc::clone(&self.stats); let valid_entries = entries.filter_map(move |entry| match entry.validate() { Ok(()) => Some(entry), Err(reason) => { stats.entries_dropped_invalid.fetch_add(1, Ordering::Relaxed); log::warn!( "Dropping invalid compilation database entry for file {:?} in directory {:?}: {}", entry.file, entry.directory, reason ); None } }); self.writer.write(valid_entries) } } #[cfg(test)] mod tests { use super::*; use crate::output::writers::fixtures::CollectingWriter; fn valid_entry(file: &str) -> Entry { Entry::from_arguments_str(file, vec!["gcc", "-c"], "/project", None) } fn entry_with_empty_directory() -> Entry { Entry::from_arguments_str("main.c", vec!["gcc", "-c"], "", None) } fn entry_with_empty_file() -> Entry { Entry::from_arguments_str("", vec!["gcc", "-c"], "/project", None) } #[test] fn test_validating_writer_passes_valid_entries_unchanged() { let stats = OutputStatistics::new(); let (writer, collected) = CollectingWriter::new(); let sut = ValidatingOutputWriter::new(writer, Arc::clone(&stats)); let entries = vec![valid_entry("a.c"), valid_entry("b.c"), valid_entry("c.c")]; sut.write(entries.into_iter()).unwrap(); let out = collected.lock().unwrap(); assert_eq!(out.len(), 3); assert_eq!(stats.entries_dropped_invalid.load(Ordering::Relaxed), 0); } #[test] fn test_validating_writer_drops_invalid_entries_and_keeps_valid_ones() { let stats = OutputStatistics::new(); let (writer, collected) = CollectingWriter::new(); let sut = ValidatingOutputWriter::new(writer, Arc::clone(&stats)); let entries = vec![ valid_entry("a.c"), entry_with_empty_directory(), valid_entry("b.c"), entry_with_empty_file(), valid_entry("c.c"), ]; sut.write(entries.into_iter()).unwrap(); let out = collected.lock().unwrap(); assert_eq!(out.len(), 3); assert_eq!(out[0].file, std::path::PathBuf::from("a.c")); assert_eq!(out[1].file, std::path::PathBuf::from("b.c")); assert_eq!(out[2].file, std::path::PathBuf::from("c.c")); assert_eq!(stats.entries_dropped_invalid.load(Ordering::Relaxed), 2); } #[test] fn test_validating_writer_drops_every_entry() { let stats = OutputStatistics::new(); let (writer, collected) = CollectingWriter::new(); let sut = ValidatingOutputWriter::new(writer, Arc::clone(&stats)); let entries = vec![entry_with_empty_directory(), entry_with_empty_file()]; sut.write(entries.into_iter()).unwrap(); let out = collected.lock().unwrap(); assert!(out.is_empty()); assert_eq!(stats.entries_dropped_invalid.load(Ordering::Relaxed), 2); } #[test] fn test_validating_writer_empty_input() { let stats = OutputStatistics::new(); let (writer, collected) = CollectingWriter::new(); let sut = ValidatingOutputWriter::new(writer, Arc::clone(&stats)); sut.write(std::iter::empty()).unwrap(); let out = collected.lock().unwrap(); assert!(out.is_empty()); assert_eq!(stats.entries_dropped_invalid.load(Ordering::Relaxed), 0); } } rizsotto-Bear-0c30b27/crates/bear/src/parse_sh/000077500000000000000000000000001527177720300214005ustar00rootroot00000000000000rizsotto-Bear-0c30b27/crates/bear/src/parse_sh/mod.rs000066400000000000000000000016401527177720300225260ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Streaming shell-text parsing for the `parse-sh` producer mode. //! //! See `docs/requirements/interception-shell-text-parsing.md` for the //! contract, and `docs/rationale/parse-sh-single-tokenizer.md` for the //! internal shape: a single streaming tokenizer owns every lexical rule //! (quotes, comments, redirections, here-documents, continuations, make //! markers), and a parser folds its token stream into execution events //! while tracking the working directory and environment. Both are //! incremental state machines over a caller-supplied buffered reader: //! memory is bounded by the longest logical line, and events are yielded //! as their line completes. No ambient I/O (`std::env`, `std::fs`); the //! CLI wiring lives in the modes. pub mod parser; pub mod tokenizer; pub use parser::{Context, Event, Parser, SkippedLine, parse}; pub use tokenizer::SkipReason; rizsotto-Bear-0c30b27/crates/bear/src/parse_sh/parser.rs000066400000000000000000001367661527177720300232650ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Folds the token stream (see [`super::tokenizer`]) into //! [`intercept::Execution`] values, as a streaming iterator: each event is //! yielded as soon as its line completes, nothing is accumulated across //! lines. The parser owns all interpreter state: the working directory //! (driven by `cd` and by the recursive-make `Entering/Leaving directory` //! markers), the per-command environment overlay, and the positional //! classification of words (leading `VAR=value` assignments, keywords and //! group braces in command position). It never looks at characters: //! everything lexical was resolved by the tokenizer. //! //! No ambient I/O: the parser reads only the supplied reader, never //! `std::env` or `std::fs`. The initial working directory and base //! environment are inputs supplied by the caller through [`Context`]; the //! mode layer is responsible for reading them from the real process. use super::tokenizer::{Marker, SkipReason, Token, TokenError, Tokenizer, Word}; use std::collections::{HashMap, VecDeque}; use std::io; use std::path::{Path, PathBuf}; /// Inputs the caller controls: the working directory in effect at the /// start of the input, and the base environment to overlay per-command /// assignments onto. pub struct Context { pub working_dir: PathBuf, pub environment: HashMap, } /// One streamed result of parsing: a recognized command, or a line that /// was loudly skipped. Both are ordinary items so the caller can forward /// executions and report skips in input order without buffering either. pub enum Event { Execution(intercept::Execution), Skipped(SkippedLine), } /// One line that was not turned into an execution event, with the reason /// and the absolute physical line it started on. pub struct SkippedLine { pub line: usize, pub reason: SkipReason, } /// Parses shell command text from `input`, starting from /// `context.working_dir` and `context.environment`. Returns an iterator /// of [`Event`]s; a read failure surfaces as an `Err` item and ends the /// stream. Memory is bounded by the longest logical line. pub fn parse(input: R, context: &Context) -> Parser<'_, R> { Parser { tokens: Tokenizer::new(input), working_dir: context.working_dir.clone(), previous_working_dir: None, directory_stack: Vec::new(), initial_working_dir: context.working_dir.clone(), base_environment: &context.environment, ready: VecDeque::new(), line_executions: Vec::new(), draining: false, assignments: Vec::new(), words: Vec::new(), command_line: 0, open_braces: Vec::new(), finished: false, } } /// Shell keywords that put a command in "not a simple command" territory /// when found in the executable position. const KEYWORDS: &[&str] = &["case", "for", "while", "until", "if", "do", "done", "then", "else", "elif", "fi", "esac", "function"]; /// The streaming parser: an [`Iterator`] of parsed [`Event`]s over the /// tokenizer's output. A lightweight state machine -- its only buffers are /// the current command's words, the current line's not-yet-committed /// executions (a skip must still be able to poison the line), and the /// `ready` handover queue, all bounded by one logical line. pub struct Parser<'a, R> { tokens: Tokenizer, working_dir: PathBuf, /// The directory before the last `cd`, so `cd -` can restore it; the /// parse-time model of the shell's OLDPWD. previous_working_dir: Option, /// Directories pushed by `Entering directory` markers, popped by /// `Leaving directory` markers; the best-effort recursive-make model. directory_stack: Vec, /// Fallback for an unmatched `Leaving directory` (empty stack). initial_working_dir: PathBuf, base_environment: &'a HashMap, /// Events completed and ready to be yielded, in input order. ready: VecDeque, /// Executions completed on the current line, not yet committed. A /// skip poisons its whole line, so nothing is emitted until the /// line's Newline proves the line held no unsupported construct. line_executions: Vec, /// Set from a skip until the next Newline: the rest of the line is /// discarded, and further skips on it are not reported again. draining: bool, assignments: Vec<(String, String)>, words: Vec, /// Line of the current command's first word, for parser-side skips. command_line: usize, /// Opening physical lines of `{` groups currently open, innermost /// last. A `{ ...; }` group runs in the current shell, so it needs no /// state snapshot -- the stack only tracks nesting depth so a `}` /// with no matching `{`, and a `{` still open at end of input, are /// caught as unbalanced braces. Persists across lines: groups may /// span newlines. open_braces: Vec, /// Set when the token stream ended or a read error was yielded. finished: bool, } impl Iterator for Parser<'_, R> { type Item = io::Result; fn next(&mut self) -> Option { loop { if let Some(event) = self.ready.pop_front() { return Some(Ok(event)); } if self.finished { return None; } match self.tokens.next() { None => { self.finished = true; // The tokenizer ends every line with a Newline token, // so these buffers are already committed; drain them // defensively and let the loop yield anything left. self.finish_command(); self.commit_line(); // A group left open at end of input is a syntax error // `sh` would have rejected; its inner commands were // already emitted (best-effort), so flag the imbalance // loudly against the earliest still-open `{` rather // than retract them. if let Some(&line) = self.open_braces.first() { self.ready.push_back(Event::Skipped(SkippedLine { line, reason: SkipReason::UnbalancedBrace, })); } } Some(Err(TokenError::Io(error))) => { self.finished = true; return Some(Err(error)); } Some(Err(TokenError::Skip(skip))) => self.skip_line(skip.line, skip.reason), Some(Ok(token)) => self.accept(token), } } } } impl Parser<'_, R> { fn accept(&mut self, token: Token) { match token { Token::Newline => { self.finish_command(); self.commit_line(); self.draining = false; } Token::Separator => { if !self.draining { self.finish_command(); } } Token::Marker(marker) => self.apply_marker(marker), Token::Word(word) => { // Group braces are reserved words only in command position // (no words pending on the current command). They must be // tracked even while draining -- see `accept_brace` -- so // brace handling runs before the drain gate; everything // else on a drained line is discarded. if self.words.is_empty() && (word.text == "{" || word.text == "}") { self.accept_brace(&word.text, word.line); } else if !self.draining { self.accept_word(word); } } } } /// Handles a group brace found in command position. A `{ ...; }` group /// runs in the current shell, so the braces are pure structure and /// neither emits a command; the stack only tracks nesting depth. /// /// Tracking must survive draining, or a `{`...`}` pair split by an /// unrelated skip on one line (`{ gcc $X a.c; }`) would desync: the /// `{` is recorded before the skip, and swallowing the `}` after it /// would leak a phantom "still open" entry and fabricate an /// unbalanced-brace report. So a close is always honored -- it may /// balance a real earlier open, and over-popping is a harmless skip /// -- while an open is ignored once draining, because the drained /// text's command position is untrustworthy and a spurious open would /// itself fabricate the imbalance. Any leading assignment before a /// brace is meaningless (`sh` rejects `VAR=x {`) and is discarded. fn accept_brace(&mut self, text: &str, line: usize) { self.assignments.clear(); if text == "{" { if !self.draining { self.open_braces.push(line); } } else if self.open_braces.pop().is_none() { self.skip_line(line, SkipReason::UnbalancedBrace); } } /// The line held no unsupported construct: its executions become /// yieldable events. fn commit_line(&mut self) { for execution in self.line_executions.drain(..) { self.ready.push_back(Event::Execution(execution)); } } /// Records one loud skip for the current line (the first reason wins) /// and discards everything the line produced or still produces: a /// construct the parser cannot follow makes the whole line untrusted, /// per the contract's "never guesses" clause. fn skip_line(&mut self, line: usize, reason: SkipReason) { if !self.draining { self.ready.push_back(Event::Skipped(SkippedLine { line, reason })); self.draining = true; } self.clear_command(); self.line_executions.clear(); } fn clear_command(&mut self) { self.assignments.clear(); self.words.clear(); } fn accept_word(&mut self, word: Word) { if self.words.is_empty() { if let Some((name, value)) = split_assignment(&word.text) { self.assignments.push((name, value)); return; } // The executable position: the one place where an unexpanded // glob or a shell control word means the line is not a simple // command. if word.has_unquoted_glob { self.skip_line(word.line, SkipReason::GlobInExecutable); return; } if KEYWORDS.contains(&word.text.as_str()) { self.skip_line(word.line, SkipReason::Keyword); return; } // Group braces in command position are intercepted before the // drain gate in `accept`, so they never reach here; a `{`/`}` // that does arrive is a plain argument in a later position. self.command_line = word.line; } self.words.push(word.text); } /// Completes the current command: `cd` mutates the working directory /// and produces no event (it is a shell builtin that never reaches /// `exec()`), an assignment-only or empty segment produces nothing, /// and everything else becomes an execution on the line buffer. fn finish_command(&mut self) { if self.words.is_empty() { self.assignments.clear(); return; } if self.words[0] == "cd" { self.apply_cd(); self.clear_command(); return; } let execution = self.build_execution(); self.line_executions.push(execution); self.clear_command(); } fn build_execution(&mut self) -> intercept::Execution { let mut environment = self.base_environment.clone(); for (name, value) in self.assignments.drain(..) { environment.insert(name, value); } let executable = PathBuf::from(&self.words[0]); intercept::Execution { executable, arguments: std::mem::take(&mut self.words), working_dir: self.working_dir.clone(), environment, } } /// A bare `cd` leaves the model unchanged (the real target would be /// `$HOME`, which an expansion-bearing line never reaches anyway). /// `cd -` restores the previous directory when one is known and is a /// loud skip otherwise -- the `-` is an OLDPWD reference, never a /// directory name to join. fn apply_cd(&mut self) { let Some(target) = self.words.get(1) else { return; }; if target == "-" { match self.previous_working_dir.take() { Some(previous) => { self.previous_working_dir = Some(self.working_dir.clone()); self.working_dir = previous; } None => self.skip_line(self.command_line, SkipReason::UnsupportedCd), } return; } self.previous_working_dir = Some(self.working_dir.clone()); self.working_dir = resolve_path(&self.working_dir, target); } fn apply_marker(&mut self, marker: Marker) { match marker { Marker::Entering(path) => { self.directory_stack.push(self.working_dir.clone()); self.working_dir = PathBuf::from(path); } Marker::Leaving => { self.working_dir = self.directory_stack.pop().unwrap_or_else(|| self.initial_working_dir.clone()); } } } } /// Splits a fully-resolved word into a `(name, value)` assignment pair when /// it looks like `IDENT=value`. Operates on the already quote-resolved text: /// a valid assignment's name portion can never itself contain quotes, so /// checking the resolved text is equivalent to checking the raw prefix. fn split_assignment(text: &str) -> Option<(String, String)> { let eq = text.find('=')?; let name = &text[..eq]; let mut chars = name.chars(); let first = chars.next()?; if !(first.is_ascii_alphabetic() || first == '_') { return None; } if !chars.all(|c| c.is_ascii_alphanumeric() || c == '_') { return None; } Some((name.to_string(), text[eq + 1..].to_string())) } /// Lexically resolves `target` against `base`: an absolute target replaces /// `base` outright, a relative one is joined onto it; `.` components are /// dropped and `..` pops the previous normal component. Purely textual -- /// no filesystem access, so symlinks are not resolved. /// /// The result is only ever built from a stack of "normal" (real) path /// segments; root-ness is tracked separately as a flag rather than as a /// poppable stack entry. That is what pins `..` at the filesystem root: /// popping an empty stack is a no-op, so an absolute base can never be /// popped past its root (real `cd /..` stays at `/`, it does not become a /// relative or empty path) while `cd /abs/x` still replaces the base /// outright. fn resolve_path(base: &Path, target: &str) -> PathBuf { let target_path = Path::new(target); let target_is_absolute = target_path.is_absolute(); let result_is_absolute = target_is_absolute || base.is_absolute(); let mut stack: Vec = Vec::new(); if !target_is_absolute { push_components(base, &mut stack); } push_components(target_path, &mut stack); let mut resolved = if result_is_absolute { PathBuf::from(std::path::MAIN_SEPARATOR.to_string()) } else { PathBuf::new() }; for part in &stack { resolved.push(part); } resolved } /// Folds `path`'s components onto `stack`: `.` is dropped, `..` pops the /// previous normal segment (a no-op on an empty stack), and root/prefix /// markers contribute nothing (root-ness is tracked separately by the /// caller). fn push_components(path: &Path, stack: &mut Vec) { for comp in path.components() { match comp { std::path::Component::CurDir => {} std::path::Component::ParentDir => { stack.pop(); } std::path::Component::Normal(part) => stack.push(part.to_os_string()), std::path::Component::RootDir | std::path::Component::Prefix(_) => {} } } } #[cfg(test)] mod tests { use super::*; /// The collected form of a whole parse, for assertion convenience; /// production callers consume [`parse`] incrementally instead. struct Interpretation { executions: Vec, skipped: Vec, } fn interpret(input: &str, context: &Context) -> Interpretation { let mut executions = Vec::new(); let mut skipped = Vec::new(); for event in parse(input.as_bytes(), context) { match event.expect("an in-memory reader cannot fail") { Event::Execution(execution) => executions.push(execution), Event::Skipped(line) => skipped.push(line), } } Interpretation { executions, skipped } } fn context(working_dir: &str, environment: &[(&str, &str)]) -> Context { Context { working_dir: PathBuf::from(working_dir), environment: environment.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(), } } fn execution( executable: &str, arguments: &[&str], working_dir: &str, environment: &[(&str, &str)], ) -> intercept::Execution { intercept::Execution { executable: PathBuf::from(executable), arguments: arguments.iter().map(|a| a.to_string()).collect(), working_dir: PathBuf::from(working_dir), environment: environment.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(), } } /// Shorthand for the common shape: commands from `/build` with an /// empty base environment, asserting only the argument vectors. fn arguments_of(input: &str) -> Vec> { let sut = interpret(input, &context("/build", &[])); assert!(sut.skipped.is_empty(), "unexpected skips for {input:?}"); sut.executions.into_iter().map(|e| e.arguments).collect() } fn args(words: &[&str]) -> Vec { words.iter().map(|w| w.to_string()).collect() } // Requirements: interception-shell-text-parsing #[test] fn events_stream_in_input_order() { let input = "gcc -c a.c\ngcc $X b.c\ngcc -c c.c\n"; let sut: Vec = parse(input.as_bytes(), &context("/build", &[])) .map(|event| event.expect("an in-memory reader cannot fail")) .collect(); assert_eq!(sut.len(), 3); assert!(matches!(&sut[0], Event::Execution(e) if e.arguments == args(&["gcc", "-c", "a.c"]))); assert!(matches!(&sut[1], Event::Skipped(s) if s.line == 2)); assert!(matches!(&sut[2], Event::Execution(e) if e.arguments == args(&["gcc", "-c", "c.c"]))); } // Requirements: interception-shell-text-parsing #[test] fn a_read_failure_surfaces_as_the_last_event() { struct FailingReader; impl std::io::Read for FailingReader { fn read(&mut self, _: &mut [u8]) -> std::io::Result { Err(std::io::Error::other("read failure")) } } let context = context("/build", &[]); let mut sut = parse(std::io::BufReader::new(FailingReader), &context); assert!(matches!(sut.next(), Some(Err(_)))); assert!(sut.next().is_none(), "the stream must be over after a read error"); } // Requirements: interception-shell-text-parsing #[test] fn emits_one_execution_per_simple_command() { let sut = interpret("mv objs/a.o a.lo", &context("/build", &[])); assert_eq!(sut.executions, vec![execution("mv", &["mv", "objs/a.o", "a.lo"], "/build", &[])]); assert!(sut.skipped.is_empty()); } // Requirements: interception-shell-text-parsing #[test] fn emits_execution_for_a_compile_command() { let sut = interpret("gcc -c -o foo.o foo.c", &context("/build", &[])); assert_eq!( sut.executions, vec![execution("gcc", &["gcc", "-c", "-o", "foo.o", "foo.c"], "/build", &[])] ); } // Requirements: interception-shell-text-parsing #[test] fn resolves_quotes_escapes_and_continuations_into_arguments() { let cases: Vec<(&str, Vec>)> = vec![ ( "gcc -O3 -I. -include zconf.h -c -o adler32.o ../zlib-1.3.1/adler32.c", vec![args(&[ "gcc", "-O3", "-I.", "-include", "zconf.h", "-c", "-o", "adler32.o", "../zlib-1.3.1/adler32.c", ])], ), ("ar rc libz.a a.o b.o ", vec![args(&["ar", "rc", "libz.a", "a.o", "b.o"])]), ("gcc -DNAME='a b' foo.c", vec![args(&["gcc", "-DNAME=a b", "foo.c"])]), ("gcc \"-DX=y z\" foo.c", vec![args(&["gcc", "-DX=y z", "foo.c"])]), ("gcc foo\\ bar.c", vec![args(&["gcc", "foo bar.c"])]), ("gcc -c \\\n foo.c", vec![args(&["gcc", "-c", "foo.c"])]), ("gcc foo.c # build it", vec![args(&["gcc", "foo.c"])]), ("# comment", vec![]), ("", vec![]), (" \n \n", vec![]), ("gcc X=y foo.c", vec![args(&["gcc", "X=y", "foo.c"])]), ("gcc *.c", vec![args(&["gcc", "*.c"])]), ("gcc '$CFLAGS' foo.c", vec![args(&["gcc", "$CFLAGS", "foo.c"])]), ]; for (input, expected) in cases { let sut = arguments_of(input); assert_eq!(sut, expected, "case: {input:?}"); } } // Requirements: interception-shell-text-parsing #[test] fn splits_commands_on_separators() { let cases: Vec<(&str, Vec>)> = vec![ ( "mkdir objs 2>/dev/null || test -d objs", vec![args(&["mkdir", "objs"]), args(&["test", "-d", "objs"])], ), ("echo a; echo b", vec![args(&["echo", "a"]), args(&["echo", "b"])]), ("echo a\necho b", vec![args(&["echo", "a"]), args(&["echo", "b"])]), ("echo a && echo b", vec![args(&["echo", "a"]), args(&["echo", "b"])]), ("echo a | echo b", vec![args(&["echo", "a"]), args(&["echo", "b"])]), ("echo a &", vec![args(&["echo", "a"])]), ]; for (input, expected) in cases { let sut = arguments_of(input); assert_eq!(sut, expected, "case: {input:?}"); } } // Requirements: interception-shell-text-parsing #[test] fn strips_redirections_from_arguments() { let cases: Vec<(&str, Vec>)> = vec![ ("cmd < input.txt", vec![args(&["cmd"])]), ("cmd >> log.txt", vec![args(&["cmd"])]), ("cmd 1>out 2>err", vec![args(&["cmd"])]), ("cmd >&2", vec![args(&["cmd"])]), ("cmd <<< input", vec![args(&["cmd"])]), ("cmd >| out.txt", vec![args(&["cmd"])]), ]; for (input, expected) in cases { let sut = arguments_of(input); assert_eq!(sut, expected, "case: {input:?}"); } } // Requirements: interception-shell-text-parsing #[test] fn skips_unsupported_constructs_with_the_right_reason() { let cases: Vec<(&str, SkipReason)> = vec![ ("gcc $(pkg-config --cflags x) foo.c", SkipReason::CommandSubstitution), ("gcc `date`", SkipReason::CommandSubstitution), ("gcc $CFLAGS foo.c", SkipReason::ParameterExpansion), ("gcc ${CFLAGS} foo.c", SkipReason::ParameterExpansion), ("*.sh arg", SkipReason::GlobInExecutable), ("cat </dev/null 2>&1", SkipReason::Subshell), ("gcc 'unterminated", SkipReason::UnterminatedQuote), ("gcc \"also unterminated", SkipReason::UnterminatedQuote), ]; for (input, reason) in cases { let sut = interpret(input, &context("/build", &[])); assert!(sut.executions.is_empty(), "no executions expected for {input:?}"); assert_eq!(sut.skipped.len(), 1, "exactly one skip expected for {input:?}"); assert_eq!(sut.skipped[0].reason, reason, "case: {input:?}"); } } // Requirements: interception-shell-text-parsing #[test] fn a_line_with_several_unsupported_constructs_is_one_skip() { // Two keyword segments (`for ...; do`) and a `; done` would each // have been a skip per segment; the line-granular rule reports the // line once, with the first reason. let sut = interpret("for i in a b; do", &context("/build", &[])); assert!(sut.executions.is_empty()); assert_eq!(sut.skipped.len(), 1, "one skip per line, not one per segment"); assert_eq!(sut.skipped[0].reason, SkipReason::Keyword); } // Requirements: interception-shell-text-parsing #[test] fn a_skip_poisons_the_whole_line_in_both_directions() { // The contract skips the whole line, so a supported command // sharing a line with an unsupported construct is dropped no // matter which side of the separator it is on. let cases: Vec<&str> = vec!["gcc -c ok.c && gcc $X bad.c", "gcc $X bad.c && gcc -c ok.c"]; for input in cases { let sut = interpret(input, &context("/build", &[])); assert!(sut.executions.is_empty(), "whole line must be dropped for {input:?}"); assert_eq!(sut.skipped.len(), 1, "case: {input:?}"); assert_eq!(sut.skipped[0].reason, SkipReason::ParameterExpansion, "case: {input:?}"); } } // Requirements: interception-shell-text-parsing // `cd`/`..` resolution goes through `std::path`, so these POSIX // absolute-path assertions hold on Unix only; on Windows the same // input yields backslash-separated, drive-relative paths. parse-sh // targets POSIX `sh` dry-run output, so Unix is where this matters. #[cfg(unix)] #[test] fn cd_on_same_line_affects_the_following_command_only() { let sut = interpret("cd sub && gcc -c foo.c", &context("/build", &[])); assert_eq!(sut.executions, vec![execution("gcc", &["gcc", "-c", "foo.c"], "/build/sub", &[])]); assert!(sut.skipped.is_empty()); } // Requirements: interception-shell-text-parsing #[cfg(unix)] #[test] fn cd_persists_across_logical_lines() { let sut = interpret("cd sub\ngcc -c foo.c", &context("/build", &[])); assert_eq!(sut.executions, vec![execution("gcc", &["gcc", "-c", "foo.c"], "/build/sub", &[])]); } // Requirements: interception-shell-text-parsing #[cfg(unix)] #[test] fn relative_cd_with_dotdot_normalizes_lexically() { let sut = interpret("cd ../bar && gcc x.c", &context("/build/foo", &[])); assert_eq!(sut.executions, vec![execution("gcc", &["gcc", "x.c"], "/build/bar", &[])]); } // Requirements: interception-shell-text-parsing #[cfg(unix)] #[test] fn cd_dotdot_stays_pinned_at_the_filesystem_root() { let cases: Vec<(&str, &str, &str)> = vec![ ("/build/foo", "cd ../bar && gcc x.c", "/build/bar"), ("/build", "cd ../../other && gcc x.c", "/other"), ("/", "cd .. && gcc x.c", "/"), ("/build", "cd /abs/x && gcc x.c", "/abs/x"), ("/build", "cd . && gcc x.c", "/build"), ]; for (working_dir, input, expected_dir) in cases { let sut = interpret(input, &context(working_dir, &[])); assert_eq!( sut.executions, vec![execution("gcc", &["gcc", "x.c"], expected_dir, &[])], "case: {input:?} from {working_dir:?}" ); } } // Requirements: interception-shell-text-parsing #[cfg(unix)] #[test] fn cd_dash_restores_the_previous_working_directory() { let sut = interpret("cd sub\ncd -\ngcc x.c", &context("/build", &[])); assert_eq!( sut.executions, vec![execution("gcc", &["gcc", "x.c"], "/build", &[])], "cd - must restore the previous directory, never join a l l `-`" ); assert!(sut.skipped.is_empty()); } // Requirements: interception-shell-text-parsing #[cfg(unix)] #[test] fn cd_dash_toggles_between_the_last_two_directories() { let sut = interpret("cd sub\ncd -\ncd -\ngcc x.c", &context("/build", &[])); assert_eq!(sut.executions, vec![execution("gcc", &["gcc", "x.c"], "/build/sub", &[])]); } // Requirements: interception-shell-text-parsing #[cfg(unix)] #[test] fn cd_dash_without_a_previous_directory_is_a_loud_skip() { let sut = interpret("cd -\ngcc x.c", &context("/build", &[])); assert_eq!( sut.executions, vec![execution("gcc", &["gcc", "x.c"], "/build", &[])], "the working directory must stay unchanged" ); assert_eq!(sut.skipped.len(), 1); assert_eq!(sut.skipped[0].reason, SkipReason::UnsupportedCd); assert_eq!(sut.skipped[0].line, 1); } // Requirements: interception-shell-text-parsing #[cfg(unix)] #[test] fn bare_cd_leaves_the_working_directory_unchanged() { let sut = interpret("cd\ngcc -c foo.c", &context("/build", &[])); assert_eq!(sut.executions, vec![execution("gcc", &["gcc", "-c", "foo.c"], "/build", &[])]); assert!(sut.skipped.is_empty(), "a bare cd must not be a skip"); } // Requirements: interception-shell-text-parsing #[test] fn make_markers_push_and_pop_the_working_directory() { let input = "make[1]: Entering directory '/build/lib'\n\ gcc -c a.c\n\ make[1]: Leaving directory '/build/lib'\n\ gcc -c b.c\n"; let sut = interpret(input, &context("/build", &[])); assert_eq!( sut.executions, vec![ execution("gcc", &["gcc", "-c", "a.c"], "/build/lib", &[]), execution("gcc", &["gcc", "-c", "b.c"], "/build", &[]), ] ); assert!(sut.skipped.is_empty(), "markers must not be reported as skips"); } // Requirements: interception-shell-text-parsing #[test] fn recognizes_entering_marker_without_job_number() { let input = "make: Entering directory `/build/lib'\ngcc -c a.c\n"; let sut = interpret(input, &context("/build", &[])); assert_eq!(sut.executions, vec![execution("gcc", &["gcc", "-c", "a.c"], "/build/lib", &[])]); } // Requirements: interception-shell-text-parsing #[test] fn leaving_marker_with_empty_stack_falls_back_to_initial_working_dir() { let input = "make[1]: Leaving directory '/build/lib'\ngcc -c a.c\n"; let sut = interpret(input, &context("/build", &[])); assert_eq!(sut.executions, vec![execution("gcc", &["gcc", "-c", "a.c"], "/build", &[])]); } // Requirements: interception-shell-text-parsing #[test] fn nested_make_markers_pop_to_the_enclosing_directory() { let input = "make[1]: Entering directory '/a/b'\n\ make[2]: Entering directory '/a/b/c'\n\ gcc -c inner.c\n\ make[2]: Leaving directory '/a/b/c'\n\ gcc -c outer.c\n"; let sut = interpret(input, &context("/a", &[])); assert_eq!( sut.executions, vec![ execution("gcc", &["gcc", "-c", "inner.c"], "/a/b/c", &[]), execution("gcc", &["gcc", "-c", "outer.c"], "/a/b", &[]), ] ); assert!(sut.skipped.is_empty(), "markers must not be reported as skips"); } // Requirements: interception-shell-text-parsing #[test] fn leaving_past_the_bottom_of_the_stack_falls_back_to_initial_working_dir() { let input = "make[1]: Entering directory '/a/b'\n\ make[1]: Leaving directory '/a/b'\n\ make[1]: Leaving directory '/a/b'\n\ gcc -c a.c\n"; let sut = interpret(input, &context("/a", &[])); assert_eq!(sut.executions, vec![execution("gcc", &["gcc", "-c", "a.c"], "/a", &[])]); } // Requirements: interception-shell-text-parsing // // A recursive-make log saved on Windows carries `\r\n` line endings; // the marker must still parse and the directory must carry no `\r`. #[test] fn crlf_terminated_make_marker_still_parses() { let input = "make[1]: Entering directory '/build/lib'\r\ngcc -c a.c\r\n"; let sut = interpret(input, &context("/build", &[])); assert_eq!(sut.executions, vec![execution("gcc", &["gcc", "-c", "a.c"], "/build/lib", &[])]); } // Requirements: interception-shell-text-parsing #[test] fn per_command_assignment_overlays_the_base_environment() { let sut = interpret("CC=gcc gcc -c foo.c", &context("/build", &[("PATH", "/usr/bin")])); assert_eq!( sut.executions, vec![execution("gcc", &["gcc", "-c", "foo.c"], "/build", &[("PATH", "/usr/bin"), ("CC", "gcc")])] ); } // Requirements: interception-shell-text-parsing #[test] fn a_plain_command_keeps_the_base_environment_unchanged() { let sut = interpret("gcc -c foo.c", &context("/build", &[("PATH", "/usr/bin")])); assert_eq!( sut.executions, vec![execution("gcc", &["gcc", "-c", "foo.c"], "/build", &[("PATH", "/usr/bin")])] ); } // Requirements: interception-shell-text-parsing #[test] fn skips_propagate_with_the_absolute_physical_line() { let input = "gcc -c foo.c\n(ranlib libz.a || true) >/dev/null 2>&1\n"; let sut = interpret(input, &context("/build", &[])); assert_eq!(sut.executions, vec![execution("gcc", &["gcc", "-c", "foo.c"], "/build", &[])]); assert_eq!(sut.skipped.len(), 1); assert_eq!( sut.skipped[0].line, 2, "skip must report the physical start line, not the lexer's line-1" ); assert_eq!(sut.skipped[0].reason, SkipReason::Subshell); } // Requirements: interception-shell-text-parsing #[test] fn a_quoted_value_split_by_a_real_newline_yields_no_fabricated_executions() { // `gcc 'a` / `b' foo.c` -- quote state never crosses a newline, so // the quote that spans one never gets to close: each half must be // skipped loudly as UnterminatedQuote, and neither half may // fabricate a command. let input = "gcc 'a\nb' foo.c\n"; let sut = interpret(input, &context("/build", &[])); assert!(sut.executions.is_empty(), "must not fabricate a command from a split quote"); assert_eq!(sut.skipped.len(), 2); assert_eq!(sut.skipped[0].reason, SkipReason::UnterminatedQuote); assert_eq!(sut.skipped[1].reason, SkipReason::UnterminatedQuote); } // Requirements: interception-shell-text-parsing #[test] fn an_unterminated_quote_in_a_redirect_target_is_a_loud_skip() { let sut = interpret("cmd > 'oops", &context("/build", &[])); assert!(sut.executions.is_empty(), "the command before the redirect must not survive"); assert_eq!(sut.skipped.len(), 1); assert_eq!(sut.skipped[0].reason, SkipReason::UnterminatedQuote); } // Requirements: interception-shell-text-parsing #[test] fn a_quoted_redirect_target_spanning_a_newline_fabricates_nothing() { // The double-quoted target swallows the newline in real sh; here // each physical line must be an UnterminatedQuote skip, and the // `gcc -c fake.c` text inside the quote must not become an event. let input = "cmd > \"a\ngcc -c fake.c > b\"\n"; let sut = interpret(input, &context("/build", &[])); assert!(sut.executions.is_empty(), "must not fabricate commands from quoted redirect text"); assert_eq!(sut.skipped.len(), 2); assert_eq!(sut.skipped[0].line, 1); assert_eq!(sut.skipped[0].reason, SkipReason::UnterminatedQuote); assert_eq!(sut.skipped[1].line, 2); assert_eq!(sut.skipped[1].reason, SkipReason::UnterminatedQuote); } // Requirements: interception-shell-text-parsing #[test] fn substitution_in_a_discarded_redirect_target_is_not_a_skip() { // The redirect target never reaches the argv, so an unresolved // path there loses nothing the database needs: the event is // emitted cleanly, and no fragment of the target (even one with // a space inside the substitution) leaks into the arguments. let cases: Vec<&str> = vec!["gcc -c foo.c > $(logdir)/x.log", "gcc -c foo.c > $(dirname x)/log"]; for input in cases { let sut = interpret(input, &context("/build", &[])); assert_eq!( sut.executions, vec![execution("gcc", &["gcc", "-c", "foo.c"], "/build", &[])], "case: {input:?}" ); assert!(sut.skipped.is_empty(), "case: {input:?}"); } } // Requirements: interception-shell-text-parsing #[test] fn an_unterminated_substitution_in_a_redirect_target_is_a_loud_skip() { let sut = interpret("cmd > $(oops\ngcc -c a.c\n", &context("/build", &[])); assert_eq!(sut.executions, vec![execution("gcc", &["gcc", "-c", "a.c"], "/build", &[])]); assert_eq!(sut.skipped.len(), 1); assert_eq!(sut.skipped[0].line, 1); assert_eq!(sut.skipped[0].reason, SkipReason::UnterminatedQuote); } // Requirements: interception-shell-text-parsing #[test] fn all_skipped_input_yields_no_executions() { let input = "(ranlib libz.a || true) >/dev/null 2>&1\ngcc $CFLAGS foo.c\n"; let sut = interpret(input, &context("/build", &[])); assert!(sut.executions.is_empty()); assert_eq!(sut.skipped.len(), 2); assert_eq!(sut.skipped[0].reason, SkipReason::Subshell); assert_eq!(sut.skipped[1].reason, SkipReason::ParameterExpansion); } // Requirements: interception-shell-text-parsing #[test] fn heredoc_body_lines_are_swallowed_not_fabricated_into_commands() { let input = "cat <| out.txt", &context("/build", &[])); assert_eq!(sut.executions, vec![execution("cc", &["cc", "-c", "x.c"], "/build", &[])]); assert!(sut.skipped.is_empty()); } // Requirements: interception-shell-text-parsing #[test] fn a_brace_group_is_transparent_and_emits_its_inner_commands() { let cases: Vec<(&str, Vec>)> = vec![ ("{ echo a; }", vec![args(&["echo", "a"])]), ("{ echo a;}", vec![args(&["echo", "a"])]), ("{ gcc -c a.c; gcc -c b.c; }", vec![args(&["gcc", "-c", "a.c"]), args(&["gcc", "-c", "b.c"])]), // The braces are structural: a trailing redirect on the group // is stripped like any other, and nothing brace-shaped leaks // into an argument vector. ("{ echo a; } > log", vec![args(&["echo", "a"])]), ]; for (input, expected) in cases { let sut = arguments_of(input); assert_eq!(sut, expected, "case: {input:?}"); } } // Requirements: interception-shell-text-parsing #[test] fn a_brace_group_may_span_lines() { let input = "{\ngcc -c a.c\ngcc -c b.c\n}\ngcc -c c.c\n"; let sut = interpret(input, &context("/build", &[])); assert_eq!( sut.executions, vec![ execution("gcc", &["gcc", "-c", "a.c"], "/build", &[]), execution("gcc", &["gcc", "-c", "b.c"], "/build", &[]), execution("gcc", &["gcc", "-c", "c.c"], "/build", &[]), ] ); assert!(sut.skipped.is_empty()); } // Requirements: interception-shell-text-parsing #[test] fn nested_brace_groups_balance_by_depth() { let sut = interpret("{ { echo a; }; echo b; }", &context("/build", &[])); assert_eq!( sut.executions, vec![ execution("echo", &["echo", "a"], "/build", &[]), execution("echo", &["echo", "b"], "/build", &[]), ] ); assert!(sut.skipped.is_empty()); } // Requirements: interception-shell-text-parsing #[cfg(unix)] #[test] fn cd_inside_a_group_persists_after_the_closing_brace() { // A brace group runs in the current shell, so unlike a subshell // its `cd` leaks out: the command after the group must be recorded // in the directory the group changed to. let sut = interpret("{ cd sub; gcc -c a.c; }\ngcc -c b.c", &context("/build", &[])); assert_eq!( sut.executions, vec![ execution("gcc", &["gcc", "-c", "a.c"], "/build/sub", &[]), execution("gcc", &["gcc", "-c", "b.c"], "/build/sub", &[]), ] ); assert!(sut.skipped.is_empty()); } // Requirements: interception-shell-text-parsing #[test] fn a_closing_brace_keeps_being_an_argument_when_not_in_command_position() { // `}` is a reserved word only in command position; after other // words it is an ordinary argument, and must not be read as a // group close (which would then look unbalanced). let sut = interpret("echo a }", &context("/build", &[])); assert_eq!(sut.executions, vec![execution("echo", &["echo", "a", "}"], "/build", &[])]); assert!(sut.skipped.is_empty()); } // Requirements: interception-shell-text-parsing #[test] fn an_unmatched_closing_brace_skips_its_line_loudly() { let sut = interpret("echo a; }", &context("/build", &[])); assert!(sut.executions.is_empty(), "an unmatched `}}` is a syntax error: the line is skipped"); assert_eq!(sut.skipped.len(), 1); assert_eq!(sut.skipped[0].reason, SkipReason::UnbalancedBrace); } // Requirements: interception-shell-text-parsing #[test] fn a_skip_between_matching_braces_does_not_leak_a_phantom_open() { // The `{` opens before the `$X` skip poisons the line and the `}` // closes after it: the pair is balanced in the source, so the run // must report only the parameter expansion, never a fabricated // unbalanced brace at end of input from a half-tracked group. let sut = interpret("{ gcc $X foo.c; }", &context("/build", &[])); assert!(sut.executions.is_empty(), "the poisoned line emits nothing"); assert_eq!(sut.skipped.len(), 1, "one skip only: the braces balance"); assert_eq!(sut.skipped[0].reason, SkipReason::ParameterExpansion); } // Requirements: interception-shell-text-parsing #[test] fn a_phantom_open_does_not_mask_a_later_unmatched_brace() { // Line 1's braces balance despite its skip; the stray `}` on line // 2 must still be caught, not absorbed by a leaked open from line // 1. Both lines are fully skipped, so nothing is emitted. let sut = interpret("{ gcc $X foo.c; }\necho a; }", &context("/build", &[])); assert!(sut.executions.is_empty(), "both lines are skipped"); assert_eq!(sut.skipped.len(), 2); assert_eq!(sut.skipped[0].reason, SkipReason::ParameterExpansion); assert_eq!(sut.skipped[0].line, 1); assert_eq!(sut.skipped[1].reason, SkipReason::UnbalancedBrace); assert_eq!(sut.skipped[1].line, 2); } // Requirements: interception-shell-text-parsing #[test] fn a_group_left_open_at_end_of_input_is_reported_against_its_opening_line() { // The inner command is best-effort kept (it is very likely real), // but the unclosed group is flagged loudly on the `{` line. let input = "gcc -c a.c\n{ gcc -c b.c\n"; let sut = interpret(input, &context("/build", &[])); assert_eq!( sut.executions, vec![ execution("gcc", &["gcc", "-c", "a.c"], "/build", &[]), execution("gcc", &["gcc", "-c", "b.c"], "/build", &[]), ] ); assert_eq!(sut.skipped.len(), 1); assert_eq!(sut.skipped[0].reason, SkipReason::UnbalancedBrace); assert_eq!(sut.skipped[0].line, 2, "the skip cites the line the open brace was on"); } } rizsotto-Bear-0c30b27/crates/bear/src/parse_sh/tokenizer.rs000066400000000000000000001175661527177720300240000ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! The single tokenizer for the `parse-sh` producer mode: the only code in //! this module that reads characters. It owns quotes and escapes (words are //! emitted fully assembled), comments, backslash-newline continuations, //! redirections (consumed, never tokens), here-documents (the pending //! delimiter queue and body consumption are internal; bodies are discarded, //! not stored), and the recursive-make directory markers. See //! `docs/rationale/parse-sh-single-tokenizer.md` for why every lexical rule //! lives here and nowhere else. //! //! The iterator yields `Result`: a [`TokenError::Skip`] //! marks an unsupported construct and is a recoverable item, not stream //! termination. The tokenizer keeps scanning the rest of the line after //! yielding one -- stopping would blind it to a later `<<` on the same //! line, and the here-document body would then be lexed as commands. //! Deciding what to discard after a skip is the parser's job (it drains to //! the next [`Token::Newline`]). A [`TokenError::Io`] read failure is //! fatal: it is the stream's last item. use std::collections::VecDeque; use std::fmt; use std::io; /// One token of the supported shell subset. Redirections and here-document /// bodies never appear: both are consumed inside the tokenizer. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Token { Word(Word), /// `;`, `&&`, `||`, `|`, or `&`: ends the current command. Separator, /// End of a logical line: ends the current command and bounds the /// reach of a skip (a skip poisons its whole line, nothing beyond). Newline, /// A recursive-make `Entering directory` / `Leaving directory` line. Marker(Marker), } /// A fully assembled word: quotes resolved, escapes applied, and adjacent /// quoted segments concatenated (`foo'bar'baz` is one word). #[derive(Debug, Clone, PartialEq, Eq)] pub struct Word { pub text: String, /// Set when `*`, `?`, or `[` appeared while genuinely unquoted. Only /// the parser knows whether this word ends up in executable position, /// where an unexpanded glob would fabricate a wrong event. pub has_unquoted_glob: bool, /// 1-based physical line the word's logical line starts on. pub line: usize, } /// A recursive-make directory marker, recognized on its own line. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Marker { Entering(String), /// The printed path is not carried: leaving pops back to whatever /// directory was in effect on entry. Leaving, } /// The error half of a tokenizer item. Only `Io` ends the stream. #[derive(Debug)] pub enum TokenError { /// An unsupported construct: recoverable, scanning continues on the /// same line. Skip(Skip), /// The reader failed; no further items follow. Io(io::Error), } /// A construct outside the supported subset, reported loudly per the /// contract: the physical line it starts on and the reason. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Skip { pub line: usize, pub reason: SkipReason, } /// Why a line was skipped instead of parsed. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SkipReason { /// `( ... )` subshell (a child shell whose state must not leak back /// out, so it stays unsupported; a `{ ...; }` group runs in the /// current shell and is parsed transparently instead). Subshell, /// A `}` that closes no open group, or a `{` left open at end of /// input: the brace nesting does not balance. UnbalancedBrace, /// Backtick or `$( ... )` command substitution. CommandSubstitution, /// `$VAR` or `${...}` parameter expansion. ParameterExpansion, /// `*`, `?`, or `[` in the executable word. GlobInExecutable, /// `<<` here-document. HereDoc, /// A shell keyword (`case`, `for`, `while`, ...) in command position. Keyword, /// A `'` or `"` quote opened but never closed before end of line. UnterminatedQuote, /// A `cd` form the working-directory model cannot follow /// (`cd -` before any previous directory is known). UnsupportedCd, } impl fmt::Display for SkipReason { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let text = match self { Self::Subshell => "subshell", Self::UnbalancedBrace => "unbalanced brace", Self::CommandSubstitution => "command substitution", Self::ParameterExpansion => "parameter expansion", Self::GlobInExecutable => "glob in executable", Self::HereDoc => "here-document", Self::Keyword => "shell keyword", Self::UnterminatedQuote => "unterminated quote", Self::UnsupportedCd => "unsupported cd", }; f.write_str(text) } } /// Streaming tokenizer over shell command text, consuming any buffered /// reader one logical line at a time (backslash-newline continuations /// joined), so memory is bounded by the longest logical line -- nothing is /// accumulated across lines but the here-doc delimiter queue. Quote state /// can never leak across a newline: an unclosed quote at end of line is an /// [`SkipReason::UnterminatedQuote`] skip, per the line-oriented contract. pub struct Tokenizer { input: R, /// 1-based physical line of the next unread character. line: usize, /// Tokens and skips already produced for the current logical line. pending: VecDeque>, /// Delimiters of here-documents whose bodies start after the current /// line, in redirect order. While non-empty, incoming lines are body /// data: consumed, matched against the front delimiter, never tokens. heredocs: VecDeque, /// Set at end of input or after a read error: the stream is over. finished: bool, } impl Tokenizer { pub fn new(input: R) -> Self { Self { input, line: 1, pending: VecDeque::new(), heredocs: VecDeque::new(), finished: false } } /// Reads the next logical line: physical lines joined over /// backslash-newline, paired with the 1-based physical line the result /// starts on. Continuations are stripped textually regardless of quote /// context: real `sh` would treat a backslash-newline inside single /// quotes as literal, but that distinction is not exercised by the /// dry-run text this tokenizer targets, so the simpler unconditional /// rule wins over threading quote state through line assembly. fn next_logical_line(&mut self) -> io::Result> { let start = self.line; let mut text = String::new(); let mut read_any = false; loop { let read = self.input.read_line(&mut text)?; if read == 0 { return Ok(if read_any { Some((text, start)) } else { None }); } read_any = true; if !text.ends_with('\n') { // Last line of the input, without a trailing newline; a // trailing backslash here escapes nothing and stays. return Ok(Some((text, start))); } text.pop(); self.line += 1; if text.ends_with('\\') { text.pop(); continue; } return Ok(Some((text, start))); } } fn process_line(&mut self, text: &str, start_line: usize) { if let Some(delimiter) = self.heredocs.front() { // Here-document body lines (and the terminator itself) are // data, not commands: they must never be tokenized, or they // would fabricate execution events. Pragmatic match: leading // tabs are stripped before comparing (the `<<-` rule, applied // without tracking the dash), but nothing else -- to real sh a // space-indented or trailing-decorated delimiter is body, and // ending the body early would tokenize its remaining lines. if text.trim_start_matches('\t') == delimiter { self.heredocs.pop_front(); } return; } if let Some(marker) = parse_make_marker(text) { self.pending.push_back(Ok(Token::Marker(marker))); self.pending.push_back(Ok(Token::Newline)); return; } LineScanner { chars: text.chars().collect(), pos: 0, line: start_line, out: &mut self.pending, heredocs: &mut self.heredocs, } .run(); self.pending.push_back(Ok(Token::Newline)); } } impl Iterator for Tokenizer { type Item = Result; fn next(&mut self) -> Option { loop { if let Some(item) = self.pending.pop_front() { return Some(item.map_err(TokenError::Skip)); } if self.finished { return None; } match self.next_logical_line() { Err(error) => { self.finished = true; return Some(Err(TokenError::Io(error))); } Ok(None) => { self.finished = true; return None; } Ok(Some((text, start_line))) => self.process_line(&text, start_line), } } } } #[derive(Clone, Copy, PartialEq, Eq)] enum Quote { None, Single, Double, } /// A word as read by [`LineScanner::read_word`], before it is turned into /// a token or a skip. struct ScannedWord { text: String, skip: Option, has_unquoted_glob: bool, } /// Tokenizes one logical line. All emitted skips carry the line's starting /// physical line number, which is also what the parser reports. struct LineScanner<'a> { chars: Vec, pos: usize, line: usize, out: &'a mut VecDeque>, heredocs: &'a mut VecDeque, } impl LineScanner<'_> { fn peek(&self) -> Option { self.chars.get(self.pos).copied() } fn peek_at(&self, offset: usize) -> Option { self.chars.get(self.pos + offset).copied() } fn advance(&mut self) -> Option { let c = self.peek(); if c.is_some() { self.pos += 1; } c } fn skip(&mut self, reason: SkipReason) { self.out.push_back(Err(Skip { line: self.line, reason })); } fn word(&mut self, text: String, has_unquoted_glob: bool) { self.out.push_back(Ok(Token::Word(Word { text, has_unquoted_glob, line: self.line }))); } fn at_separator(&self) -> bool { matches!(self.peek(), Some(';') | Some('&') | Some('|')) } fn run(mut self) { loop { while matches!(self.peek(), Some(c) if c.is_whitespace()) { self.advance(); } let Some(c) = self.peek() else { break }; match c { // A '#' at a word boundary starts a comment to end of // line; '#' inside an already-started word is handled by // read_word and never reaches here. Nothing after it is // scanned, so a `<<` inside a comment cannot queue a // here-document. '#' => break, ';' => { self.advance(); self.out.push_back(Ok(Token::Separator)); } '&' | '|' => { self.advance(); if self.peek() == Some(c) { self.advance(); } self.out.push_back(Ok(Token::Separator)); } // An unquoted parenthesis at word start is subshell // grouping (in `sh` it is an operator; a command like // `gcc (a)` is a syntax error, never a real invocation). // Mid-word parentheses stay word characters, see // read_word. `{`/`}` are NOT handled here: they are // reserved words, special only in command position, // which only the parser can see. '(' | ')' => { self.advance(); self.skip(SkipReason::Subshell); } _ if self.looks_like_redirect_start() => self.scan_redirect(), _ => { let word = self.read_word(); match word.skip { Some(reason) => self.skip(reason), None => self.word(word.text, word.has_unquoted_glob), } } } } } fn looks_like_redirect_start(&self) -> bool { match self.peek() { Some('<') | Some('>') => true, Some(c) if c.is_ascii_digit() => { let mut off = 1; while matches!(self.peek_at(off), Some(d) if d.is_ascii_digit()) { off += 1; } matches!(self.peek_at(off), Some('<') | Some('>')) } _ => false, } } /// Consumes one redirection (optional fd-prefix, operator, and target /// or dup-form) and strips it entirely: nothing from a redirect ever /// becomes a token. `<<<` (herestring) is an ordinary redirect to /// strip; only `<<` (here-document) yields a skip and queues its /// delimiter for body consumption. fn scan_redirect(&mut self) { while matches!(self.peek(), Some(c) if c.is_ascii_digit()) { self.advance(); } if self.peek() == Some('<') && self.peek_at(1) == Some('<') && self.peek_at(2) == Some('<') { self.pos += 3; self.discard_target(); return; } if self.peek() == Some('<') && self.peek_at(1) == Some('<') { self.pos += 2; if self.peek() == Some('-') { self.advance(); } while matches!(self.peek(), Some(c) if c.is_whitespace()) { self.advance(); } // A missing delimiter word queues nothing: there is no body // to consume, only the `<<` line itself to skip. if let Some(delimiter) = self.read_heredoc_delimiter() { self.heredocs.push_back(delimiter); } self.skip(SkipReason::HereDoc); return; } if self.peek() == Some('>') && self.peek_at(1) == Some('>') { self.pos += 2; } else if self.peek() == Some('>') { self.advance(); // `>|` is the POSIX clobber redirect (`set -C` override): the // `|` belongs to the operator, not to a pipe. if self.peek() == Some('|') { self.advance(); } } else { // Only '<' can reach here: the doubled/tripled forms and '>' // were consumed above, and looks_like_redirect_start proved // the operator character. self.advance(); } // Dup form: `N>&M`, `>&2`, `2>&-` -- the whole token is the // redirect, no separate target word follows. if self.peek() == Some('&') { self.advance(); while matches!(self.peek(), Some(c) if c.is_ascii_digit()) { self.advance(); } if self.peek() == Some('-') { self.advance(); } return; } self.discard_target(); } /// Consumes and discards a redirect's target word, whether fused /// (`>file`) or separated by whitespace (`> file`). Targets are /// discarded uninspected -- an expansion or substitution inside one /// does not skip the line, because the command's argv is still fully /// known and only the redirect path is unresolved (see the contract's /// redirection clause). The one exception is an unterminated quote or /// substitution: that means the line boundary itself was misread, so /// it must surface as a loud skip rather than let the construct's /// content fabricate commands on the next line. fn discard_target(&mut self) { while matches!(self.peek(), Some(c) if c.is_whitespace()) { self.advance(); } if self.peek().is_none() || self.at_separator() || self.peek() == Some('#') { return; } let word = self.read_word(); if word.skip == Some(SkipReason::UnterminatedQuote) { self.skip(SkipReason::UnterminatedQuote); } } /// Reads the here-document delimiter word: either a bare word, or a /// single run of `'...'` / `"..."` with the quotes stripped. A bare /// delimiter ends at whitespace or at any shell operator character -- /// including `(`, `)`, and a backtick, so a here-doc inside a /// subshell (`(cat < Option { let mut word = String::new(); match self.peek() { Some(q @ ('\'' | '"')) => { self.advance(); while let Some(c) = self.peek() { if c == q { self.advance(); break; } word.push(c); self.advance(); } } _ => { while let Some(c) = self.peek() { if c.is_whitespace() || matches!(c, ';' | '&' | '|' | '<' | '>' | '(' | ')' | '`') { break; } word.push(c); self.advance(); } } } if word.is_empty() { None } else { Some(word) } } /// Reads one whitespace/metacharacter-delimited word, resolving quotes /// and backslash escapes, and flagging an unsupported construct or an /// unquoted glob character as it goes. Assumes the caller has already /// ruled out comment, separator, redirect, and parenthesis starts at /// this position. fn read_word(&mut self) -> ScannedWord { let mut text = String::new(); let mut skip: Option = None; let mut has_unquoted_glob = false; let mut quote = Quote::None; while let Some(c) = self.peek() { match quote { Quote::Single => { self.advance(); if c == '\'' { quote = Quote::None; } else { text.push(c); } } Quote::Double => match c { '"' => { self.advance(); quote = Quote::None; } '\\' => { self.advance(); if let Some(next) = self.advance() { text.push(next); } } '$' => { let reason = if self.peek_at(1) == Some('(') { SkipReason::CommandSubstitution } else { SkipReason::ParameterExpansion }; skip.get_or_insert(reason); self.advance(); text.push(c); } '`' => { skip.get_or_insert(SkipReason::CommandSubstitution); self.advance(); text.push(c); } _ => { self.advance(); text.push(c); } }, Quote::None => { if c.is_whitespace() || matches!(c, ';' | '&' | '|' | '<' | '>') { break; } match c { '\'' => { self.advance(); quote = Quote::Single; } '"' => { self.advance(); quote = Quote::Double; } '\\' => { self.advance(); if let Some(next) = self.advance() { text.push(next); } } '$' if self.peek_at(1) == Some('(') => { skip.get_or_insert(SkipReason::CommandSubstitution); self.advance(); self.advance(); text.push_str("$("); // The whole substitution is one shell word even // when it contains spaces: consume to the // matching parenthesis so no fragment of it can // leak out as a separate word -- or, after a // discarded redirect target, as a fabricated // argument. if !self.consume_balanced(&mut text, '(', ')') { skip = Some(SkipReason::UnterminatedQuote); } } '$' if self.peek_at(1) == Some('{') => { skip.get_or_insert(SkipReason::ParameterExpansion); self.advance(); self.advance(); text.push_str("${"); if !self.consume_balanced(&mut text, '{', '}') { skip = Some(SkipReason::UnterminatedQuote); } } '$' => { skip.get_or_insert(SkipReason::ParameterExpansion); self.advance(); text.push(c); } '`' => { skip.get_or_insert(SkipReason::CommandSubstitution); self.advance(); text.push(c); if !self.consume_balanced(&mut text, '`', '`') { skip = Some(SkipReason::UnterminatedQuote); } } '*' | '?' | '[' => { has_unquoted_glob = true; self.advance(); text.push(c); } _ => { self.advance(); text.push(c); } } } } } if quote != Quote::None { // The read loop only exits mid-quote by running out of line // (the `Quote::None` arm is the only one that breaks on a // delimiter). Quote state never crosses a newline: the // contract is line-oriented, so each half of a quote that a // real newline split must be skipped loudly rather than lexed // into a fabricated command. skip = Some(SkipReason::UnterminatedQuote); } ScannedWord { text, skip, has_unquoted_glob } } /// Consumes a substitution/expansion span up to the matching `close`, /// appending everything to `text`, quote- and escape-aware so a close /// character inside quotes does not end the span. Returns false when /// the line ends before the span closes: the construct continues on /// the next physical line, which this line-oriented reader must treat /// like an unterminated quote, or the span's tail would fabricate /// commands there. fn consume_balanced(&mut self, text: &mut String, open: char, close: char) -> bool { let mut depth = 1usize; let mut quote = Quote::None; while let Some(c) = self.advance() { text.push(c); match quote { Quote::Single => { if c == '\'' { quote = Quote::None; } } Quote::Double => match c { '\\' => { if let Some(next) = self.advance() { text.push(next); } } '"' => quote = Quote::None, _ => {} }, Quote::None => match c { '\\' => { if let Some(next) = self.advance() { text.push(next); } } '\'' => quote = Quote::Single, '"' => quote = Quote::Double, // Close is checked first so an open==close pair (a // backtick span) terminates on its second character. c if c == close => { depth -= 1; if depth == 0 { return true; } } c if c == open => depth += 1, _ => {} }, } } false } } /// Recognizes a make recursive-directory marker on an already /// continuation-joined logical line. Requires a `: ` immediately before /// `Entering directory ` / `Leaving directory `, with the optional `[]` /// job-number suffix (`make[1]: ...`) allowed but not required. Real `make` /// prints these markers on their own line and never on a command line, so /// dry-run input is not misread; a hand-crafted line that embeds the exact /// `: Entering directory ''` shape (e.g. an `echo` of it) /// would be, but that is outside the supported dry-run contract. fn parse_make_marker(line: &str) -> Option { let trimmed = line.trim(); const ENTERING: &str = ": Entering directory "; const LEAVING: &str = ": Leaving directory "; if let Some(rest) = find_after_marker(trimmed, ENTERING) { return extract_quoted_path(rest).map(Marker::Entering); } if let Some(rest) = find_after_marker(trimmed, LEAVING) { return extract_quoted_path(rest).map(|_| Marker::Leaving); } None } /// Returns the text after `marker` when `line` starts with a program-name /// prefix (optionally followed by `[]`) immediately followed by /// `marker`. The prefix itself is not validated beyond "non-empty, /// contains no ':'" -- `make`, `gmake`, `make[1]` all qualify. fn find_after_marker<'a>(line: &'a str, marker: &str) -> Option<&'a str> { let idx = line.find(marker)?; let prefix = &line[..idx]; if prefix.is_empty() || prefix.contains(':') { return None; } Some(&line[idx + marker.len()..]) } /// Strips one layer of quoting from the start of `rest` -- `'...'`, /// `` `...' `` (older GNU make's backtick-open/single-close form), or /// `"..."` -- and returns the inner path. Trailing text after the closing /// quote (there should be none) is ignored. fn extract_quoted_path(rest: &str) -> Option { let mut chars = rest.chars(); let open = chars.next()?; let close = match open { '\'' => '\'', '`' => '\'', '"' => '"', _ => return None, }; let body = chars.as_str(); let end = body.find(close)?; Some(body[..end].to_string()) } #[cfg(test)] mod tests { use super::*; fn tokens(input: &str) -> Vec> { Tokenizer::new(input.as_bytes()) .map(|item| { item.map_err(|error| match error { TokenError::Skip(skip) => skip, // An in-memory reader cannot fail. TokenError::Io(error) => panic!("unexpected read error: {error}"), }) }) .collect() } fn word(text: &str, line: usize) -> Result { Ok(Token::Word(Word { text: text.to_string(), has_unquoted_glob: false, line })) } fn glob_word(text: &str, line: usize) -> Result { Ok(Token::Word(Word { text: text.to_string(), has_unquoted_glob: true, line })) } fn sep() -> Result { Ok(Token::Separator) } fn nl() -> Result { Ok(Token::Newline) } fn skipped(line: usize, reason: SkipReason) -> Result { Err(Skip { line, reason }) } // Requirements: interception-shell-text-parsing #[test] fn assembles_words_across_quotes_and_escapes() { let cases: Vec<(&str, Vec>)> = vec![ ("gcc -c foo.c", vec![word("gcc", 1), word("-c", 1), word("foo.c", 1), nl()]), ("gcc -DNAME='a b' foo.c", vec![word("gcc", 1), word("-DNAME=a b", 1), word("foo.c", 1), nl()]), ("gcc \"-DX=y z\" foo.c", vec![word("gcc", 1), word("-DX=y z", 1), word("foo.c", 1), nl()]), ("gcc foo\\ bar.c", vec![word("gcc", 1), word("foo bar.c", 1), nl()]), ("gcc foo'bar'baz", vec![word("gcc", 1), word("foobarbaz", 1), nl()]), ("gcc '$CFLAGS' foo.c", vec![word("gcc", 1), word("$CFLAGS", 1), word("foo.c", 1), nl()]), ("gcc foo#bar", vec![word("gcc", 1), word("foo#bar", 1), nl()]), ("gcc *.c", vec![word("gcc", 1), glob_word("*.c", 1), nl()]), ]; for (input, expected) in cases { let sut = tokens(input); assert_eq!(sut, expected, "case: {input:?}"); } } // Requirements: interception-shell-text-parsing #[test] fn joins_line_continuations_and_numbers_physical_lines() { let cases: Vec<(&str, Vec>)> = vec![ ("gcc -c \\\n foo.c", vec![word("gcc", 1), word("-c", 1), word("foo.c", 1), nl()]), ("gcc a\ngcc b", vec![word("gcc", 1), word("a", 1), nl(), word("gcc", 2), word("b", 2), nl()]), ("a \\\n b\ngcc c", vec![word("a", 1), word("b", 1), nl(), word("gcc", 3), word("c", 3), nl()]), ]; for (input, expected) in cases { let sut = tokens(input); assert_eq!(sut, expected, "case: {input:?}"); } } // Requirements: interception-shell-text-parsing // // A build log saved on Windows (or fetched through a CRLF-translating // channel) carries `\r\n` line endings; the carriage return must not // survive into the last word of the line. #[test] fn crlf_line_endings_leave_no_carriage_return_in_words() { let cases: Vec<(&str, Vec>)> = vec![ ("gcc -c foo.c\r\n", vec![word("gcc", 1), word("-c", 1), word("foo.c", 1), nl()]), ( "gcc a\r\ngcc b\r\n", vec![word("gcc", 1), word("a", 1), nl(), word("gcc", 2), word("b", 2), nl()], ), ]; for (input, expected) in cases { let sut = tokens(input); assert_eq!(sut, expected, "case: {input:?}"); } } // Requirements: interception-shell-text-parsing // // Documented deviation (see `next_logical_line`): continuations are // stripped textually regardless of quote context, so a backslash-newline // inside single quotes joins instead of staying literal as real `sh` // would keep it. Pin the joined form so a change to the rule is a // conscious one. #[test] fn backslash_newline_inside_single_quotes_joins_the_lines() { let sut = tokens("gcc '-DX=a\\\nb' foo.c"); assert_eq!(sut, vec![word("gcc", 1), word("-DX=ab", 1), word("foo.c", 1), nl()]); } // Requirements: interception-shell-text-parsing #[test] fn emits_separators_for_the_command_operators() { let cases: Vec<(&str, Vec>)> = vec![ ("a; b", vec![word("a", 1), sep(), word("b", 1), nl()]), ("a && b", vec![word("a", 1), sep(), word("b", 1), nl()]), ("a || b", vec![word("a", 1), sep(), word("b", 1), nl()]), ("a | b", vec![word("a", 1), sep(), word("b", 1), nl()]), ("a &", vec![word("a", 1), sep(), nl()]), ]; for (input, expected) in cases { let sut = tokens(input); assert_eq!(sut, expected, "case: {input:?}"); } } // Requirements: interception-shell-text-parsing #[test] fn comments_and_blank_lines_produce_only_newlines() { let cases: Vec<(&str, Vec>)> = vec![ ("# comment", vec![nl()]), ("", vec![]), (" \n \n", vec![nl(), nl()]), ("gcc foo.c # build it", vec![word("gcc", 1), word("foo.c", 1), nl()]), ]; for (input, expected) in cases { let sut = tokens(input); assert_eq!(sut, expected, "case: {input:?}"); } } // Requirements: interception-shell-text-parsing #[test] fn consumes_redirections_without_emitting_tokens() { let cases: Vec<(&str, Vec>)> = vec![ ("cmd < input.txt", vec![word("cmd", 1), nl()]), ("cmd >> log.txt", vec![word("cmd", 1), nl()]), ("cmd 1>out 2>err", vec![word("cmd", 1), nl()]), ("cmd >&2", vec![word("cmd", 1), nl()]), ("cmd 2>&-", vec![word("cmd", 1), nl()]), ("cmd <<< input", vec![word("cmd", 1), nl()]), // `>|` is one operator (POSIX clobber): the `|` must not be // read as a pipe, and the target must not become a word. ("cmd >| out.txt", vec![word("cmd", 1), nl()]), ("cmd >/dev/null 2>&1", vec![word("cmd", 1), nl()]), ]; for (input, expected) in cases { let sut = tokens(input); assert_eq!(sut, expected, "case: {input:?}"); } } // Requirements: interception-shell-text-parsing #[test] fn an_unterminated_quote_in_a_redirect_target_is_a_loud_skip() { let sut = tokens("cmd > 'oops"); assert_eq!(sut, vec![word("cmd", 1), skipped(1, SkipReason::UnterminatedQuote), nl()]); } // Requirements: interception-shell-text-parsing #[test] fn substitution_in_a_discarded_redirect_target_is_not_a_skip() { let cases: Vec<&str> = vec![ "gcc -c foo.c > $(logdir)/x.log", // The substitution is one shell word despite the space inside // it: no fragment (`x)/log`) may leak back as an argument. "gcc -c foo.c > $(dirname x)/log", "gcc -c foo.c > `dirname x`/log", "gcc -c foo.c > ${LOGDIR}/x.log", ]; for input in cases { let sut = tokens(input); assert_eq!(sut, vec![word("gcc", 1), word("-c", 1), word("foo.c", 1), nl()], "case: {input:?}"); } } // Requirements: interception-shell-text-parsing #[test] fn an_unterminated_substitution_in_a_redirect_target_is_a_loud_skip() { // The `$(` never closes on this line, so the line boundary is // misread: same loud-skip treatment as an unterminated quote. let sut = tokens("cmd > $(oops"); assert_eq!(sut, vec![word("cmd", 1), skipped(1, SkipReason::UnterminatedQuote), nl()]); } // Requirements: interception-shell-text-parsing #[test] fn skips_unsupported_constructs_but_keeps_scanning_the_line() { let cases: Vec<(&str, Vec>)> = vec![ ( "gcc $CFLAGS foo.c", vec![word("gcc", 1), skipped(1, SkipReason::ParameterExpansion), word("foo.c", 1), nl()], ), ( // The whole `$(...)` span is one (skipped) word; scanning // resumes at `foo.c`. "gcc $(pkg-config --cflags x) foo.c", vec![word("gcc", 1), skipped(1, SkipReason::CommandSubstitution), word("foo.c", 1), nl()], ), ("gcc `date`", vec![word("gcc", 1), skipped(1, SkipReason::CommandSubstitution), nl()]), ("gcc 'unterminated", vec![word("gcc", 1), skipped(1, SkipReason::UnterminatedQuote), nl()]), ]; for (input, expected) in cases { let sut = tokens(input); assert_eq!(sut, expected, "case: {input:?}"); } } // Requirements: interception-shell-text-parsing #[test] fn a_heredoc_is_a_skip_and_its_body_produces_no_tokens() { let sut = tokens("cat <(&'a [u8]); impl io::Read for FailingReader<'_> { fn read(&mut self, buf: &mut [u8]) -> io::Result { if self.0.is_empty() { return Err(io::Error::other("read failure")); } let n = self.0.len().min(buf.len()); buf[..n].copy_from_slice(&self.0[..n]); self.0 = &self.0[n..]; Ok(n) } } let mut sut = Tokenizer::new(io::BufReader::new(FailingReader(b"gcc a.c\n"))); assert!(matches!(sut.next(), Some(Ok(Token::Word(_))))); assert!(matches!(sut.next(), Some(Ok(Token::Word(_))))); assert!(matches!(sut.next(), Some(Ok(Token::Newline)))); assert!(matches!(sut.next(), Some(Err(TokenError::Io(_))))); assert!(sut.next().is_none(), "the stream must be over after a read error"); } // Requirements: interception-shell-text-parsing #[test] fn emits_markers_for_recursive_make_directory_lines() { let sut = tokens("make[1]: Entering directory '/build/lib'\nmake[1]: Leaving directory '/build/lib'\n"); assert_eq!( sut, vec![ Ok(Token::Marker(Marker::Entering("/build/lib".to_string()))), nl(), Ok(Token::Marker(Marker::Leaving)), nl(), ] ); } } rizsotto-Bear-0c30b27/crates/intercept-preload/000077500000000000000000000000001527177720300215155ustar00rootroot00000000000000rizsotto-Bear-0c30b27/crates/intercept-preload/CLAUDE.md000066400000000000000000000053261527177720300230020ustar00rootroot00000000000000## Preload interception library `README.md` in this directory is the reference for what this crate is, the C/Rust split and the reasons for it, the supported-platform matrix, and the build instructions. This file covers only the constraints an agent needs. ## Architecture constraint The C shim (`src/c/shim.c`) and Rust (`src/implementation.rs`) split is mandatory; the rationale is in `README.md`. Do not merge the C and Rust parts. ## Unsafe code This crate is where almost all of Bear's `unsafe` lives (FFI, `dlsym`, async-signal-safe handlers). Annotate it consistently: - Every `unsafe fn` carries a `/// # Safety` doc comment stating the caller's obligations. - Each `unsafe {}` block gets an inline `// SAFETY:` comment (`clippy::undocumented_unsafe_blocks` checks it). When the enclosing function's `# Safety` section covers it, the comment can say so and name the requirement. ## Build script On ELF platforms, `build.rs` picks the linker. The order, the probe link that confirms each candidate, and why GNU ld is out are in `docs/rationale/preload-linker-selection.md`; the user-facing linker prerequisite is in `INSTALL.md`. If the linker set, order, or probe changes, update the code and the rationale. If the set changes, update `INSTALL.md` too. `INTERCEPT_FAMILY` in `build.rs` lists the symbols `src/c/shim.c` exports. Source of truth is `src/c/shim.c` itself. Adding or removing an intercepted function is a three-place edit: 1. `src/c/shim.c` - the `EXPORT` wrapper itself (and the matching `extern` declaration of the `rust_` implementation). 2. `intercept-preload/build.rs::INTERCEPT_FAMILY` - so the symbol passes the filter against `platform_checks::detect()` and is written into the version script / exports list. 3. `platform-checks/src/lib.rs::SYMBOL_PROBES` - so the symbol is probed on the target at all. If it is not listed here it never enters the `detect()` result, and the `INTERCEPT_FAMILY` filter silently drops it, leaving the wrapper hidden by `local: *;`. A probed symbol outside `INTERCEPT_FAMILY` (e.g. `_NSGetEnviron`) is never exported. It reaches the shim only as a `has_symbol_X` -D flag, and detected headers arrive as `has_header_X` flags. Miss any one of the three and the wrapper compiles but is invisible to the dynamic linker, so libc handles the call instead of us. Pairs that travel together (e.g. `popen`/`pclose`) each need their own entry in all three places. ## Before modifying - The "doctor" logic that maintains interception across `exec` calls is subtle - read it fully before changing - Reports go to the TCP collector in `crates/intercept-supervisor`, in the wire format from `crates/intercept` (`tcp.rs`) - do not change the protocol without updating the collector rizsotto-Bear-0c30b27/crates/intercept-preload/Cargo.toml000066400000000000000000000015111527177720300234430ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later [package] name = "intercept-preload" description = "Dynamic library to be used with Bear for LD_PRELOAD interception" version.workspace = true authors.workspace = true repository.workspace = true homepage.workspace = true license.workspace = true edition.workspace = true rust-version.workspace = true publish = false [lib] name = "exec" path = "src/lib.rs" crate-type = ["rlib", "cdylib"] [dependencies] intercept = { path = "../intercept" } thiserror.workspace = true log.workspace = true ctor.workspace = true [target.'cfg(target_family = "unix")'.dependencies] libc.workspace = true errno.workspace = true [build-dependencies] platform-checks = { path = "../../build-support/platform-checks" } cc.workspace = true [dev-dependencies] serde_json.workspace = true [lints] workspace = true rizsotto-Bear-0c30b27/crates/intercept-preload/README.md000066400000000000000000000034221527177720300227750ustar00rootroot00000000000000# Dynamic library for Bear interception This crate provides a dynamic library for Unix systems that can be used with Bear for intercepting system calls via the `LD_PRELOAD` mechanism (or `DYLD_INSERT_LIBRARIES` on macOS). ## Overview `libexec` is designed to work with the Bear compilation database generator. It intercepts system calls like `execve` to track command execution during builds. The library is split into a C shim (`src/c/shim.c`) and Rust implementation (`src/implementation.rs`). This separation exists because: 1. Stable Rust cannot handle C variadic arguments (`execl` family) 2. On FreeBSD, libc functions may call each other internally — having all exported symbols in C call into Rust (which uses `dlsym(RTLD_NEXT, ...)`) avoids recursive interception issues ## Supported Platforms | Platform | Mechanism | Symbol visibility | |---|---|---| | Linux, Android, FreeBSD, OpenBSD, NetBSD, DragonFly BSD | `LD_PRELOAD` | ELF version scripts | | macOS | `DYLD_INSERT_LIBRARIES` | `-exported_symbols_list` | On unsupported platforms (e.g. Windows), the build displays a warning and skips library generation. ## Features - Intercepts `exec` family calls, `posix_spawn`, `popen`, and `system` (not `popen` and `system` on Android) - Automatically "doctors" child process environments to maintain interception across `exec` calls - Reports intercepted executions to a TCP collector - Platform capability detection at build time (only intercepts functions available on the host) ## Building To build `libexec` in debug mode: ```sh cargo build -p intercept-preload ``` For the release version: ```sh cargo build -p intercept-preload --release ``` The resulting shared library will be in `target/debug/libexec.so` (or `.dylib` on macOS) and `target/release/libexec.so` respectively.rizsotto-Bear-0c30b27/crates/intercept-preload/build.rs000066400000000000000000000276411527177720300231740ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later use std::io::Write; // Symbols that the C shim (src/c/shim.c) actually exports for LD_PRELOAD // interception. The version script / exports list is restricted to this // family, so any probe in `platform_checks::detect()` outside it is not // exported. Such probes still reach the shim as -D flags. const INTERCEPT_FAMILY: &[&str] = &[ "execve", "execv", "execvpe", "execvp", "execvP", "exect", "execl", "execlp", "execle", "pclose", "posix_spawn", "posix_spawnp", "popen", "system", ]; fn main() { // Tell cargo to invalidate the built crate whenever source changes println!("cargo:rerun-if-changed=src/lib.rs"); println!("cargo:rerun-if-changed=src/c/shim.c"); // Replay platform-checks results as cfg directives for this crate let detected = platform_checks::detect(); platform_checks::emit_cfg(&detected); platform_checks::emit_check_cfg(); if cfg!(target_family = "unix") { let out_dir = std::env::var("OUT_DIR").expect("cargo sets OUT_DIR for build scripts"); let mut intercept_symbols: Vec<&str> = detected.symbols.iter().copied().filter(|s| INTERCEPT_FAMILY.contains(s)).collect(); // Our popen and system run the shell through the posix_spawn helper // functions (posix_spawnattr_init, ...), which the libc crate does not // bind for Android. Leave these calls to bionic there; the matching // Rust implementations are gated off in src/implementation.rs. let is_android = std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("android"); if is_android { intercept_symbols.retain(|s| !matches!(*s, "popen" | "pclose" | "system")); } // Compile the C shim for all intercepted functions // This handles variadic arguments properly (execl family) and provides // a clean separation between C exports and Rust implementation. // // We use cargo_metadata(false) to prevent cc from emitting its own // cargo:rustc-link-lib directive, which would link without --whole-archive let mut shim = cc::Build::new(); for (name, value) in platform_checks::SYSTEM_EXTENSIONS { shim.define(name, *value); } // Symbols in INTERCEPT_FAMILY that were dropped above stay undefined, // because the shim wrapper would not link without its Rust side. let extra_symbols = detected.symbols.iter().filter(|s| !INTERCEPT_FAMILY.contains(s)); for symbol in intercept_symbols.iter().chain(extra_symbols) { let flag = format!("has_symbol_{}", symbol); shim.define(flag.as_str(), None); } for header in &detected.headers { shim.define(platform_checks::header_cfg_name(header).as_str(), None); } shim.file("src/c/shim.c") .warnings(true) .extra_warnings(true) .pic(true) // Position independent code for shared library .cargo_metadata(false) // Don't let cc emit link directives .out_dir(&out_dir) .compile("shim"); // Manually specify linking with --whole-archive to ensure all C symbols // are included in the shared library, even if they're not referenced from Rust. // This is critical because the C shim exports need to be available for // LD_PRELOAD interception to work. if cfg!(target_os = "macos") { // Generate macOS export file let exports_path = format!("{}/exports.txt", out_dir); generate_macos_exports(&exports_path, &intercept_symbols); // macOS uses -force_load instead of --whole-archive println!("cargo:rustc-cdylib-link-arg=-Wl,-force_load,{}/libshim.a", out_dir); // macOS uses -exported_symbols_list for symbol visibility println!("cargo:rustc-cdylib-link-arg=-Wl,-exported_symbols_list,{}", exports_path); // Set rpath to look for dependencies in the same directory as the library // macOS uses @loader_path instead of $ORIGIN println!("cargo:rustc-link-arg=-Wl,-rpath,@loader_path"); } else { // Generate Linux/ELF version script let exports_path = format!("{}/exports.map", out_dir); generate_linux_exports(&exports_path, &intercept_symbols); // Linux and other ELF platforms use --whole-archive println!("cargo:rustc-cdylib-link-arg=-Wl,--whole-archive"); println!("cargo:rustc-cdylib-link-arg={}/libshim.a", out_dir); println!("cargo:rustc-cdylib-link-arg=-Wl,--no-whole-archive"); // Use a dynamically generated version script to control symbol visibility // This ensures all intercepted functions are exported as GLOBAL symbols // The version script's "local: *" hides all other symbols println!("cargo:rustc-cdylib-link-arg=-Wl,--version-script={}", exports_path); // Set rpath to look for dependencies in the same directory as the library println!("cargo:rustc-link-arg=-Wl,-rpath,$ORIGIN"); emit_elf_linker_selection(); } } else { // We don't build on other platforms println!("cargo:warning=libexec is not supported on this platform"); } } /// Pick a linker that can combine rustc's own cdylib version script with /// the exports.map above; GNU ld refuses that combination outright. The /// preference order, the probe that confirms a candidate before it is /// used, and the reasoning behind both are recorded in /// docs/rationale/preload-linker-selection.md. fn emit_elf_linker_selection() { let candidates = elf_linker_candidates(); // Being installed is not the same as being able to perform this link. let selected = match LinkProbe::new() { Some(probe) => candidates.iter().position(|candidate| probe.can_link(&candidate.flags)), None => (!candidates.is_empty()).then_some(0), }; let Some(index) = selected else { // On hosts whose default linker already is lld (e.g. FreeBSD) the // link succeeds anyway, so phrase this as a hint, not a verdict. println!( "cargo:warning=could not confirm a usable lld or mold linker; relying \ on the toolchain default. If linking libexec fails with 'anonymous \ version tag cannot be combined with other version tags', install \ lld or mold (GNU ld cannot link this library)" ); return; }; if index > 0 { println!( "cargo:warning={} cannot link libexec on this host; using {} instead", candidates[0].name, candidates[index].name ); } for flag in &candidates[index].flags { println!("cargo:rustc-cdylib-link-arg={}", flag); } } struct LinkerCandidate { /// How the candidate is named in build warnings. name: &'static str, /// The `cc` flags that select it. flags: Vec, } fn elf_linker_candidates() -> Vec { let mut candidates = Vec::new(); if let Some(gcc_ld_dir) = bundled_rust_lld_dir() { candidates.push(LinkerCandidate { name: "the toolchain's bundled rust-lld", flags: vec![format!("-B{}", gcc_ld_dir.display()), "-fuse-ld=lld".to_string()], }); } if find_in_path("ld.lld") { candidates.push(LinkerCandidate { name: "the system lld", flags: vec!["-fuse-ld=lld".to_string()] }); } if find_in_path("mold") { candidates .push(LinkerCandidate { name: "the system mold", flags: vec!["-fuse-ld=mold".to_string()] }); } candidates } /// A miniature of the cdylib link, kept in `OUT_DIR`, in which both known /// failure modes reproduce. struct LinkProbe { compiler: cc::Tool, object: std::path::PathBuf, script: std::path::PathBuf, library: std::path::PathBuf, } impl LinkProbe { /// `None` when the host cannot be probed at all (no usable `cc`, /// unwritable `OUT_DIR`). fn new() -> Option { let compiler = cc::Build::new().try_get_compiler().ok()?; let dir = std::path::PathBuf::from(std::env::var_os("OUT_DIR")?).join("linker-probe"); std::fs::create_dir_all(&dir).ok()?; let source = dir.join("probe.c"); std::fs::write(&source, "int probe_symbol(void) { return 0; }\n").ok()?; let script = dir.join("probe.map"); std::fs::write(&script, "{ global: probe_symbol; local: *; };\n").ok()?; let object = dir.join("probe.o"); let compiled = compiler.to_command().arg("-fPIC").arg("-c").arg(&source).arg("-o").arg(&object).output().ok()?; compiled.status.success().then_some(Self { compiler, object, script, library: dir.join("probe.so") }) } fn can_link(&self, flags: &[String]) -> bool { let version_script = format!("-Wl,--version-script={}", self.script.display()); let linked = self .compiler .to_command() .arg("-shared") .arg(&self.object) .arg("-o") .arg(&self.library) // Twice: one stands for rustc's script, one for ours. .arg(&version_script) .arg(&version_script) .args(flags) .output(); matches!(linked, Ok(linked) if linked.status.success()) } } /// rustup toolchains ship rust-lld with cc-compatible shims under /// /lib/rustlib//bin/gcc-ld; preferring it keeps the build /// free of system linker dependencies. The shims run on the build host, /// hence HOST rather than TARGET. fn bundled_rust_lld_dir() -> Option { let rustc = std::env::var_os("RUSTC")?; let output = std::process::Command::new(rustc).args(["--print", "sysroot"]).output().ok()?; if !output.status.success() { return None; } let sysroot = String::from_utf8(output.stdout).ok()?; let host = std::env::var("HOST").ok()?; let dir = std::path::Path::new(sysroot.trim()).join("lib/rustlib").join(host).join("bin/gcc-ld"); dir.join("ld.lld").is_file().then_some(dir) } fn find_in_path(name: &str) -> bool { std::env::var_os("PATH") .map(|paths| std::env::split_paths(&paths).any(|dir| dir.join(name).is_file())) .unwrap_or(false) } /// Generate the Linux ELF version script based on intercept symbols fn generate_linux_exports(path: &str, intercept_symbols: &[&str]) { let mut file = std::fs::File::create(path).expect("Failed to create exports.map"); writeln!(file, "/* Generated version script for libexec library */") .expect("Failed to write exports.map"); writeln!(file, "{{").expect("Failed to write exports.map"); writeln!(file, " global:").expect("Failed to write exports.map"); // Export symbols that were detected on this platform for symbol in intercept_symbols { writeln!(file, " {};", symbol).expect("Failed to write exports.map"); } writeln!(file).expect("Failed to write exports.map"); writeln!(file, " /* Library version info */").expect("Failed to write exports.map"); writeln!(file, " LIBEXEC_VERSION;").expect("Failed to write exports.map"); writeln!(file).expect("Failed to write exports.map"); writeln!(file, " local:").expect("Failed to write exports.map"); writeln!(file, " *;").expect("Failed to write exports.map"); writeln!(file, "}};").expect("Failed to write exports.map"); } /// Generate the macOS exported symbols list based on intercept symbols fn generate_macos_exports(path: &str, intercept_symbols: &[&str]) { let mut file = std::fs::File::create(path).expect("Failed to create exports.txt"); // macOS exported_symbols_list format: one symbol per line, prefixed with underscore for symbol in intercept_symbols { writeln!(file, "_{}", symbol).expect("Failed to write exports.txt"); } // Library version info writeln!(file, "_LIBEXEC_VERSION").expect("Failed to write exports.txt"); } rizsotto-Bear-0c30b27/crates/intercept-preload/src/000077500000000000000000000000001527177720300223045ustar00rootroot00000000000000rizsotto-Bear-0c30b27/crates/intercept-preload/src/c/000077500000000000000000000000001527177720300225265ustar00rootroot00000000000000rizsotto-Bear-0c30b27/crates/intercept-preload/src/c/shim.c000066400000000000000000000231301527177720300236310ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later // // C shim for intercepted libc functions // // This file provides thin C wrappers for all intercepted functions. The actual // interception logic (reporting and calling real functions via dlsym) is // implemented in Rust. This separation exists because: // // 1. Stable Rust cannot handle C variadic arguments (execl family) // 2. On FreeBSD, libc functions may call each other internally. By having all // exported symbols in C call into Rust (which uses dlsym(RTLD_NEXT, ...)), // we avoid recursive interception issues. // // For variadic functions (execl, execlp, execle), we use a two-pass approach // with VLAs (C99) to collect arguments without heap allocation: // 1. First pass: count the number of variadic arguments // 2. Second pass: copy arguments into a stack-allocated VLA // #include #include #include #include #include // Ensure symbols are exported from the shared library #define EXPORT __attribute__((visibility("default"))) // Platform-specific environment access // // When the dynamic linker loads the library, the `environ` variable might not // be available yet. Where `_NSGetEnviron()` exists (macOS), use it to // reliably access the environment during library initialization. #if defined(has_header_crt_externs_h) && defined(has_symbol__NSGetEnviron) #include #define get_environ() (*_NSGetEnviron()) #else extern char **environ; #define get_environ() environ #endif // Rust implementation functions // // These are defined in implementation.rs with #[no_mangle] and handle: // - Reporting the execution to the collector // - Calling the real function via dlsym(RTLD_NEXT, ...) // Session initialization - called from constructor to capture environment extern void rust_session_init(char *const *envp); // Exec family functions extern int rust_execve(const char *path, char *const argv[], char *const envp[]); extern int rust_execvpe(const char *file, char *const argv[], char *const envp[]); extern int rust_execvp(const char *file, char *const argv[]); extern int rust_execvP(const char *file, const char *search_path, char *const argv[]); extern int rust_exect(const char *path, char *const argv[], char *const envp[]); extern int rust_posix_spawn(pid_t *pid, const char *path, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr_t *attrp, char *const argv[], char *const envp[]); extern int rust_posix_spawnp(pid_t *pid, const char *file, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr_t *attrp, char *const argv[], char *const envp[]); extern FILE *rust_popen(const char *command, const char *mode); extern int rust_pclose(FILE *stream); extern int rust_system(const char *command); // Library constructor // // This function is called when the library is loaded into memory. It captures // the current environment and passes it to Rust for session initialization. // This is critical because: // // 1. On macOS, `environ` is not available during early library initialization, // so we use `_NSGetEnviron()` instead. // 2. Build systems may clear or modify environment variables like LD_PRELOAD // and INTERCEPT_COLLECTOR_ADDRESS. By capturing them early, we can restore // them when executing child processes. __attribute__((constructor)) static void on_load(void) { char *const *envp = get_environ(); if (envp != NULL) { rust_session_init(envp); } } // Count variadic arguments until NULL terminator. // Uses its own va_copy, so the caller's va_list is not affected. static size_t va_count_args(va_list ap) { size_t count = 0; while (va_arg(ap, const char *) != NULL) ++count; return count; } // // Variadic exec helpers — why we inline va_arg extraction // // The C standard says that after a va_list is passed to a function that calls // va_arg on it, the va_list's value in the caller is indeterminate. On x86_64 // va_list is an array type (effectively passed by reference), so the caller's // copy happens to be advanced — but on i686 (32-bit) va_list is a plain char* // passed by value, so the caller's copy is NOT advanced. To avoid undefined // behavior on any platform, all va_arg extraction is done directly in the // calling function rather than delegated to a helper. // // static void va_copy_args(va_list ap, char **argv, size_t n) // { // for (size_t i = 0; i < n; ++i) // argv[i] = va_arg(ap, char *); // } // // // execl - execute a file // // int execl(const char *path, const char *arg0, ... /*, (char *)0 */); // #if defined(has_symbol_execl) EXPORT int execl(const char *path, const char *arg0, ...) { va_list ap; va_start(ap, arg0); // First pass: count the number of arguments after arg0 va_list ap_count; va_copy(ap_count, ap); const size_t argc = va_count_args(ap_count); va_end(ap_count); // Second pass: extract arguments into stack-allocated VLA // Layout: [arg0, arg1, ..., argN, NULL] = 1 + argc + 1 elements char *argv[argc + 2]; argv[0] = (char *)arg0; for (size_t i = 1; i <= argc + 1; ++i) argv[i] = va_arg(ap, char *); va_end(ap); return rust_execve(path, argv, get_environ()); } #endif // // execlp - execute a file, searching PATH // // int execlp(const char *file, const char *arg0, ... /*, (char *)0 */); // #if defined(has_symbol_execlp) EXPORT int execlp(const char *file, const char *arg0, ...) { va_list ap; va_start(ap, arg0); // First pass: count the number of arguments after arg0 va_list ap_count; va_copy(ap_count, ap); const size_t argc = va_count_args(ap_count); va_end(ap_count); // Second pass: extract arguments into stack-allocated VLA char *argv[argc + 2]; argv[0] = (char *)arg0; for (size_t i = 1; i <= argc + 1; ++i) argv[i] = va_arg(ap, char *); va_end(ap); #if defined(has_symbol_execvpe) return rust_execvpe(file, argv, get_environ()); #else return rust_execvp(file, argv); #endif } #endif // // execle - execute a file with environment // // int execle(const char *path, const char *arg0, ... /*, (char *)0, char *const envp[] */); // // Note: The environment pointer comes AFTER the NULL terminator in the variadic list // #if defined(has_symbol_execle) EXPORT int execle(const char *path, const char *arg0, ...) { va_list ap; va_start(ap, arg0); // First pass: count the number of arguments after arg0 (excluding NULL and envp) va_list ap_count; va_copy(ap_count, ap); const size_t argc = va_count_args(ap_count); va_end(ap_count); // Second pass: extract arguments into stack-allocated VLA char *argv[argc + 2]; argv[0] = (char *)arg0; for (size_t i = 1; i <= argc + 1; ++i) argv[i] = va_arg(ap, char *); // The next argument after NULL is the environment pointer char *const *envp = va_arg(ap, char *const *); va_end(ap); return rust_execve(path, argv, envp); } #endif // // execv - execute a file // #if defined(has_symbol_execv) EXPORT int execv(const char *path, char *const argv[]) { return rust_execve(path, argv, get_environ()); } #endif // // execve - execute a file with environment // #if defined(has_symbol_execve) EXPORT int execve(const char *path, char *const argv[], char *const envp[]) { return rust_execve(path, argv, envp); } #endif // // execvp - execute a file, searching PATH // #if defined(has_symbol_execvp) EXPORT int execvp(const char *file, char *const argv[]) { #if defined(has_symbol_execvpe) return rust_execvpe(file, argv, get_environ()); #else return rust_execvp(file, argv); #endif } #endif // // execvpe - execute a file, searching PATH, with environment (GNU extension) // #if defined(has_symbol_execvpe) EXPORT int execvpe(const char *file, char *const argv[], char *const envp[]) { return rust_execvpe(file, argv, envp); } #endif // // execvP - execute a file with custom search path (BSD extension) // #if defined(has_symbol_execvP) EXPORT int execvP(const char *file, const char *search_path, char *const argv[]) { return rust_execvP(file, search_path, argv); } #endif // // exect - execute a file with tracing (BSD, deprecated) // #if defined(has_symbol_exect) EXPORT int exect(const char *path, char *const argv[], char *const envp[]) { return rust_exect(path, argv, envp); } #endif // // posix_spawn - spawn a process // #if defined(has_symbol_posix_spawn) EXPORT int posix_spawn(pid_t *pid, const char *path, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr_t *attrp, char *const argv[], char *const envp[]) { return rust_posix_spawn(pid, path, file_actions, attrp, argv, envp); } #endif // // posix_spawnp - spawn a process, searching PATH // #if defined(has_symbol_posix_spawnp) EXPORT int posix_spawnp(pid_t *pid, const char *file, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr_t *attrp, char *const argv[], char *const envp[]) { return rust_posix_spawnp(pid, file, file_actions, attrp, argv, envp); } #endif // // popen - open a pipe to a process // #if defined(has_symbol_popen) EXPORT FILE *popen(const char *command, const char *mode) { return rust_popen(command, mode); } EXPORT int pclose(FILE *stream) { return rust_pclose(stream); } #endif // // system - execute a shell command // #if defined(has_symbol_system) EXPORT int system(const char *command) { return rust_system(command); } #endif rizsotto-Bear-0c30b27/crates/intercept-preload/src/implementation.rs000066400000000000000000001371221527177720300257050ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! This file implements the Rust side of the preload library interception. //! //! All exported functions are prefixed with `rust_` and are called from the C shim //! (`src/c/shim.c`). This separation exists because: //! //! 1. Stable Rust cannot handle C variadic arguments (execl family) //! 2. On FreeBSD, libc functions may call each other internally. By having all //! exported symbols in C call into Rust (which uses dlsym(RTLD_NEXT, ...)), //! we avoid recursive interception issues. //! //! Each `rust_*` function: //! - Reports the execution to the collector //! - Optionally "doctors" the environment to ensure child processes continue interception //! - Calls the real function via dlsym(RTLD_NEXT, ...) //! //! ## Environment Doctoring //! //! When SESSION is set (meaning we're in an interception session), we check if the //! environment passed to exec functions still has the correct preload settings. //! If not, we "doctor" the environment to restore them, ensuring child processes //! continue to be intercepted even if the build system cleared the variables. use std::collections::HashMap; use std::ffi::CStr; use std::path::PathBuf; #[cfg(all(any(has_symbol_popen, has_symbol_system), not(target_os = "android")))] use std::ptr; use std::sync::OnceLock; use std::sync::atomic::{AtomicPtr, Ordering}; use ctor::ctor; use intercept::Execution; use intercept::reporter::{Reporter, ReporterFactory}; use intercept::tcp::ReporterOnTcp; use libc::{RTLD_NEXT, c_char, c_int}; #[cfg(any( has_symbol_posix_spawn, has_symbol_posix_spawnp, all(any(has_symbol_popen, has_symbol_system), not(target_os = "android")) ))] use libc::{pid_t, posix_spawn_file_actions_t, posix_spawnattr_t}; use crate::session::{DoctoredEnvironment, SESSION_CTX, in_session, init_session_from_envp}; #[ctor(unsafe)] static REAL_EXECVE: AtomicPtr = { // SAFETY: the name is a NUL-terminated literal. dlsym may return null // (symbol absent); every user of this static checks for null before calling // through it. let ptr = unsafe { libc::dlsym(RTLD_NEXT, c"execve".as_ptr() as *const _) }; AtomicPtr::new(ptr) }; #[ctor(unsafe)] static REAL_EXECVPE: AtomicPtr = { // SAFETY: the name is a NUL-terminated literal. dlsym may return null // (symbol absent); every user of this static checks for null before calling // through it. let ptr = unsafe { libc::dlsym(RTLD_NEXT, c"execvpe".as_ptr() as *const _) }; AtomicPtr::new(ptr) }; #[ctor(unsafe)] static REAL_EXECVP: AtomicPtr = { // SAFETY: the name is a NUL-terminated literal. dlsym may return null // (symbol absent); every user of this static checks for null before calling // through it. let ptr = unsafe { libc::dlsym(RTLD_NEXT, c"execvp".as_ptr() as *const _) }; AtomicPtr::new(ptr) }; #[ctor(unsafe)] static REAL_EXECVP_OPENBSD: AtomicPtr = { // SAFETY: the name is a NUL-terminated literal. dlsym may return null // (symbol absent); every user of this static checks for null before calling // through it. let ptr = unsafe { libc::dlsym(RTLD_NEXT, c"execvP".as_ptr() as *const _) }; AtomicPtr::new(ptr) }; #[ctor(unsafe)] static REAL_EXECT: AtomicPtr = { // SAFETY: the name is a NUL-terminated literal. dlsym may return null // (symbol absent); every user of this static checks for null before calling // through it. let ptr = unsafe { libc::dlsym(RTLD_NEXT, c"exect".as_ptr() as *const _) }; AtomicPtr::new(ptr) }; #[ctor(unsafe)] static REAL_POSIX_SPAWN: AtomicPtr = { // SAFETY: the name is a NUL-terminated literal. dlsym may return null // (symbol absent); every user of this static checks for null before calling // through it. let ptr = unsafe { libc::dlsym(RTLD_NEXT, c"posix_spawn".as_ptr() as *const _) }; AtomicPtr::new(ptr) }; #[ctor(unsafe)] static REAL_POSIX_SPAWNP: AtomicPtr = { // SAFETY: the name is a NUL-terminated literal. dlsym may return null // (symbol absent); every user of this static checks for null before calling // through it. let ptr = unsafe { libc::dlsym(RTLD_NEXT, c"posix_spawnp".as_ptr() as *const _) }; AtomicPtr::new(ptr) }; #[ctor(unsafe)] static REAL_POPEN: AtomicPtr = { // SAFETY: the name is a NUL-terminated literal. dlsym may return null // (symbol absent); every user of this static checks for null before calling // through it. let ptr = unsafe { libc::dlsym(RTLD_NEXT, c"popen".as_ptr() as *const _) }; AtomicPtr::new(ptr) }; #[ctor(unsafe)] static REAL_SYSTEM: AtomicPtr = { // SAFETY: the name is a NUL-terminated literal. dlsym may return null // (symbol absent); every user of this static checks for null before calling // through it. let ptr = unsafe { libc::dlsym(RTLD_NEXT, c"system".as_ptr() as *const _) }; AtomicPtr::new(ptr) }; #[ctor(unsafe)] static REAL_PCLOSE: AtomicPtr = { // SAFETY: the name is a NUL-terminated literal. dlsym may return null // (symbol absent); every user of this static checks for null before calling // through it. let ptr = unsafe { libc::dlsym(RTLD_NEXT, c"pclose".as_ptr() as *const _) }; AtomicPtr::new(ptr) }; /// Map from FILE* address to child pid for our popen implementation. /// When our `rust_popen` spawns a child, it stores the pid here so /// `rust_pclose` can later `waitpid` it. /// /// A `BTreeMap` rather than a `HashMap`: it is const-constructible, so the /// static needs no lazy init, and it needs no random seed, which a /// `HashMap` would fetch from the OS inside the host process. #[cfg(all(has_symbol_popen, not(target_os = "android")))] static POPEN_CHILDREN: std::sync::Mutex> = std::sync::Mutex::new(std::collections::BTreeMap::new()); /// Reporter for sending execution events to the collector. /// Initialized once by `rust_session_init` when called from the C shim constructor. /// Using `OnceLock` ensures safe single-initialization without raw pointers and /// provides `&ReporterOnTcp` references that are sound across threads (since /// `ReporterOnTcp` is `Sync` — it only holds a `SocketAddr` and creates a fresh /// `TcpStream` per report call). static REPORTER: OnceLock = OnceLock::new(); /// Initialize the session from the environment. /// /// Called from the C shim's `on_load()` constructor. This captures the /// environment variables before the build system has a chance to clear them. /// /// # Safety /// This function is unsafe because it dereferences raw pointers from C. #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_session_init(envp: *const *const c_char) { // Initialize logging first #[cfg(not(test))] intercept::logging::init("preload"); // Initialize the session and get the destination for reporter setup // SAFETY: envp is the process environment from the C constructor. This fn's // # Safety says it is a null-terminated array of NUL-terminated strings; // null is handled. let destination = unsafe { init_session_from_envp(envp) }; // Initialize the reporter from the captured destination if let Some(address) = destination { let reporter = ReporterFactory::create(address); if REPORTER.set(reporter).is_err() { log::warn!("Reporter already initialized, ignoring duplicate init"); } } else { log::debug!("No destination found, reporter not initialized"); } } /// Rust implementation of execve /// /// Called from C shim for: execv, execve, execl, execle /// /// # Safety /// This function is unsafe because it: /// - Dereferences raw pointers /// - Calls the real execve function #[cfg(has_symbol_execve)] #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_execve( path: *const c_char, argv: *const *const c_char, envp: *const *const c_char, ) -> c_int { type ExecveFunc = unsafe extern "C" fn( path: *const c_char, argv: *const *const c_char, envp: *const *const c_char, ) -> c_int; // SAFETY: the pointer args are the caller's execve arguments, valid under // this fn's # Safety; as_path_buf, as_string_vec and as_environment // null-check before reading. resolved_env owns any doctored strings until // the end of this block, so envp_to_use stays valid. func_ptr is non-null // (checked) and comes from dlsym(RTLD_NEXT, "execve"), so the transmute to // the matching fn type is sound. unsafe { report(|| { let result = Execution { executable: as_path_buf(path)?, arguments: as_string_vec(argv)?, working_dir: working_dir()?, environment: as_environment(envp)?, }; Ok(result) }); // Doctor the environment if session is active but the build system stripped preload vars let resolved_env = resolve_environment(envp); let envp_to_use = resolved_env.as_ptr(); let func_ptr = REAL_EXECVE.load(Ordering::SeqCst); if !func_ptr.is_null() { let real_func_ptr: ExecveFunc = std::mem::transmute(func_ptr); real_func_ptr(path, argv, envp_to_use) } else { log::error!("Real execve function not found"); set_errno(libc::ENOSYS); -1 } } } /// Rust implementation of execvpe (GNU extension) /// /// Called from C shim for: execvpe /// On platforms where execvpe is available, execlp and execvp are also routed here. /// /// # Safety /// This function is unsafe because it: /// - Dereferences raw pointers /// - Calls the real execvpe function #[cfg(has_symbol_execvpe)] #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_execvpe( file: *const c_char, argv: *const *const c_char, envp: *const *const c_char, ) -> c_int { type ExecvpeFunc = unsafe extern "C" fn( file: *const c_char, argv: *const *const c_char, envp: *const *const c_char, ) -> c_int; // SAFETY: the pointer args are the caller's execvpe arguments, valid under // this fn's # Safety; as_path_buf, as_string_vec and as_environment // null-check before reading. resolved_env owns any doctored strings until // the end of this block, so envp_to_use stays valid. func_ptr is non-null // (checked) and comes from dlsym(RTLD_NEXT, "execvpe"), so the transmute to // the matching fn type is sound. unsafe { report(|| { let result = Execution { executable: as_path_buf(file)?, arguments: as_string_vec(argv)?, working_dir: working_dir()?, environment: as_environment(envp)?, }; Ok(result) }); // Doctor the environment if session is active but the build system stripped preload vars let resolved_env = resolve_environment(envp); let envp_to_use = resolved_env.as_ptr(); let func_ptr = REAL_EXECVPE.load(Ordering::SeqCst); if !func_ptr.is_null() { let real_func_ptr: ExecvpeFunc = std::mem::transmute(func_ptr); real_func_ptr(file, argv, envp_to_use) } else { log::error!("Real execvpe function not found"); set_errno(libc::ENOSYS); -1 } } } /// Rust implementation of execvp /// /// Called from C shim for: execvp, execlp (on platforms where execvpe is not available) /// /// Unlike `rust_execvpe`, this function does not take an explicit `envp` argument. /// The real `execvp` uses the process's `environ`, so no environment doctoring is /// performed here (same approach as `popen` and `system`). /// /// # Safety /// This function is unsafe because it: /// - Dereferences raw pointers /// - Calls the real execvp function #[cfg(has_symbol_execvp)] #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_execvp(file: *const c_char, argv: *const *const c_char) -> c_int { type ExecvpFunc = unsafe extern "C" fn(file: *const c_char, argv: *const *const c_char) -> c_int; // SAFETY: the pointer args are the caller's execvp arguments, valid under // this fn's # Safety; as_path_buf, as_string_vec and as_environment // null-check before reading. func_ptr is non-null (checked) and comes from // dlsym(RTLD_NEXT, "execvp"), so the transmute to the matching fn type is // sound. unsafe { report(|| { let result = Execution { executable: as_path_buf(file)?, arguments: as_string_vec(argv)?, working_dir: working_dir()?, environment: std::env::vars().collect(), }; Ok(result) }); let func_ptr = REAL_EXECVP.load(Ordering::SeqCst); if !func_ptr.is_null() { let real_func_ptr: ExecvpFunc = std::mem::transmute(func_ptr); real_func_ptr(file, argv) } else { log::error!("Real execvp function not found"); set_errno(libc::ENOSYS); -1 } } } /// Rust implementation of execvP (BSD extension) /// /// Called from C shim for: execvP /// /// # Safety /// This function is unsafe because it: /// - Dereferences raw pointers /// - Calls the real execvP function #[cfg(has_symbol_execvP)] #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_execvP( file: *const c_char, search_path: *const c_char, argv: *const *const c_char, ) -> c_int { type ExecvPFunc = unsafe extern "C" fn( file: *const c_char, search_path: *const c_char, argv: *const *const c_char, ) -> c_int; // SAFETY: the pointer args are the caller's execvP arguments, valid under // this fn's # Safety; as_path_buf, as_string_vec and as_environment // null-check before reading. func_ptr is non-null (checked) and comes from // dlsym(RTLD_NEXT, "execvP"), so the transmute to the matching fn type is // sound. unsafe { report(|| { let result = Execution { executable: as_path_buf(file)?, arguments: as_string_vec(argv)?, working_dir: working_dir()?, environment: std::env::vars().collect(), }; Ok(result) }); let func_ptr = REAL_EXECVP_OPENBSD.load(Ordering::SeqCst); if !func_ptr.is_null() { let real_func_ptr: ExecvPFunc = std::mem::transmute(func_ptr); real_func_ptr(file, search_path, argv) } else { log::error!("Real execvP function not found"); set_errno(libc::ENOSYS); -1 } } } /// Rust implementation of exect (BSD, deprecated) /// /// Called from C shim for: exect /// /// # Safety /// This function is unsafe because it: /// - Dereferences raw pointers /// - Calls the real exect function #[cfg(has_symbol_exect)] #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_exect( path: *const c_char, argv: *const *const c_char, envp: *const *const c_char, ) -> c_int { type ExectFunc = unsafe extern "C" fn( path: *const c_char, argv: *const *const c_char, envp: *const *const c_char, ) -> c_int; // SAFETY: the pointer args are the caller's exect arguments, valid under // this fn's # Safety; as_path_buf, as_string_vec and as_environment // null-check before reading. resolved_env owns any doctored strings until // the end of this block, so envp_to_use stays valid. func_ptr is non-null // (checked) and comes from dlsym(RTLD_NEXT, "exect"), so the transmute to // the matching fn type is sound. unsafe { report(|| { let result = Execution { executable: as_path_buf(path)?, arguments: as_string_vec(argv)?, working_dir: working_dir()?, environment: as_environment(envp)?, }; Ok(result) }); // Doctor the environment if session is active but the build system stripped preload vars let resolved_env = resolve_environment(envp); let envp_to_use = resolved_env.as_ptr(); let func_ptr = REAL_EXECT.load(Ordering::SeqCst); if !func_ptr.is_null() { let real_func_ptr: ExectFunc = std::mem::transmute(func_ptr); real_func_ptr(path, argv, envp_to_use) } else { log::error!("Real exect function not found"); set_errno(libc::ENOSYS); -1 } } } /// Rust implementation of posix_spawn /// /// Called from C shim for: posix_spawn /// /// # Safety /// This function is unsafe because it: /// - Dereferences raw pointers /// - Calls the real posix_spawn function #[cfg(has_symbol_posix_spawn)] #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_posix_spawn( pid: *mut pid_t, path: *const c_char, file_actions: *const posix_spawn_file_actions_t, attrp: *const posix_spawnattr_t, argv: *const *const c_char, envp: *const *const c_char, ) -> c_int { type PosixSpawnFunc = unsafe extern "C" fn( pid: *mut pid_t, path: *const c_char, file_actions: *const posix_spawn_file_actions_t, attrp: *const posix_spawnattr_t, argv: *const *const c_char, envp: *const *const c_char, ) -> c_int; // SAFETY: the pointer args are the caller's posix_spawn arguments, valid // under this fn's # Safety; as_path_buf, as_string_vec and as_environment // null-check before reading. resolved_env owns any doctored strings until // the end of this block, so envp_to_use stays valid. func_ptr is non-null // (checked) and comes from dlsym(RTLD_NEXT, "posix_spawn"), so the // transmute to the matching fn type is sound. unsafe { report(|| { let result = Execution { executable: as_path_buf(path)?, arguments: as_string_vec(argv)?, working_dir: working_dir()?, environment: as_environment(envp)?, }; Ok(result) }); // Doctor the environment if session is active but the build system stripped preload vars let resolved_env = resolve_environment(envp); let envp_to_use = resolved_env.as_ptr(); let func_ptr = REAL_POSIX_SPAWN.load(Ordering::SeqCst); if !func_ptr.is_null() { let real_func_ptr: PosixSpawnFunc = std::mem::transmute(func_ptr); real_func_ptr(pid, path, file_actions, attrp, argv, envp_to_use) } else { log::error!("Real posix_spawn function not found"); libc::ENOSYS } } } /// Rust implementation of posix_spawnp /// /// Called from C shim for: posix_spawnp /// /// # Safety /// This function is unsafe because it: /// - Dereferences raw pointers /// - Calls the real posix_spawnp function #[cfg(has_symbol_posix_spawnp)] #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_posix_spawnp( pid: *mut pid_t, file: *const c_char, file_actions: *const posix_spawn_file_actions_t, attrp: *const posix_spawnattr_t, argv: *const *const c_char, envp: *const *const c_char, ) -> c_int { type PosixSpawnpFunc = unsafe extern "C" fn( pid: *mut pid_t, file: *const c_char, file_actions: *const posix_spawn_file_actions_t, attrp: *const posix_spawnattr_t, argv: *const *const c_char, envp: *const *const c_char, ) -> c_int; // SAFETY: the pointer args are the caller's posix_spawnp arguments, valid // under this fn's # Safety; as_path_buf, as_string_vec and as_environment // null-check before reading. resolved_env owns any doctored strings until // the end of this block, so envp_to_use stays valid. func_ptr is non-null // (checked) and comes from dlsym(RTLD_NEXT, "posix_spawnp"), so the // transmute to the matching fn type is sound. unsafe { report(|| { let result = Execution { executable: as_path_buf(file)?, arguments: as_string_vec(argv)?, working_dir: working_dir()?, environment: as_environment(envp)?, }; Ok(result) }); // Doctor the environment if session is active but the build system stripped preload vars let resolved_env = resolve_environment(envp); let envp_to_use = resolved_env.as_ptr(); let func_ptr = REAL_POSIX_SPAWNP.load(Ordering::SeqCst); if !func_ptr.is_null() { let real_func_ptr: PosixSpawnpFunc = std::mem::transmute(func_ptr); real_func_ptr(pid, file, file_actions, attrp, argv, envp_to_use) } else { log::error!("Real posix_spawnp function not found"); libc::ENOSYS } } } /// Rust implementation of popen /// /// Called from C shim for: popen /// /// Instead of calling the real `popen` (which uses `environ` internally and /// may bypass our LD_PRELOAD on older glibc), we reimplement it using /// `posix_spawnp` with a doctored envp — the same path as exec-family calls. /// This eliminates the need for `ensure_environ_has_session_vars` / `setenv`. /// /// # Safety /// This function is unsafe because it: /// - Dereferences raw pointers (`command` and `mode`) which could be null or invalid /// - Creates pipes and spawns child processes /// - Returns a raw pointer to a FILE structure /// /// The caller must ensure that `command` and `mode` are valid null-terminated C strings. #[cfg(all(has_symbol_popen, not(target_os = "android")))] #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_popen(command: *const c_char, mode: *const c_char) -> *mut libc::FILE { if command.is_null() || mode.is_null() { set_errno(libc::EINVAL); return ptr::null_mut(); } // SAFETY: command is non-null (checked above) and NUL-terminated per this // fn's # Safety. The borrowed str is only used inside this call; the caller // keeps the C string alive. let command_str = match unsafe { CStr::from_ptr(command) }.to_str() { Ok(s) => s, Err(_) => { log::warn!("Failed to parse popen command as UTF-8"); set_errno(libc::EINVAL); return ptr::null_mut(); } }; // SAFETY: mode is non-null (checked above) and NUL-terminated per this fn's // # Safety. The borrowed str is only used inside this call. let mode_str = match unsafe { CStr::from_ptr(mode) }.to_str() { Ok(s) => s, Err(_) => { set_errno(libc::EINVAL); return ptr::null_mut(); } }; // Parse mode: 'r' or 'w' for direction, optional 'e' for O_CLOEXEC (glibc extension) let reading = mode_str.starts_with('r'); let cloexec = mode_str.contains('e'); report(|| { Ok(Execution { executable: PathBuf::from("/bin/sh"), arguments: vec!["/bin/sh".to_string(), "-c".to_string(), command_str.to_string()], working_dir: working_dir()?, environment: std::env::vars().collect(), }) }); // Create the pipe, with O_CLOEXEC if requested via 'e' in mode. let mut pipe_fds: [c_int; 2] = [0; 2]; let pipe_result = if cloexec { create_pipe_cloexec(pipe_fds.as_mut_ptr()) } else { // SAFETY: pipe_fds is a live local [c_int; 2], which is what pipe(2) // writes to. unsafe { libc::pipe(pipe_fds.as_mut_ptr()) } }; if pipe_result != 0 { return ptr::null_mut(); } // Parent reads from pipe_fds[0], child writes to pipe_fds[1] (reading mode) // Parent writes to pipe_fds[1], child reads from pipe_fds[0] (writing mode) let (parent_fd, child_fd, child_target_fd) = if reading { (pipe_fds[0], pipe_fds[1], libc::STDOUT_FILENO) } else { (pipe_fds[1], pipe_fds[0], libc::STDIN_FILENO) }; // Set FD_CLOEXEC on the parent fd to prevent leaking it into unrelated // children if the caller later fork+execs without going through our override. // This matches libc popen behavior. (When 'e' was requested and pipe2 was used, // both fds already have CLOEXEC, but setting it again is harmless.) // SAFETY: parent_fd is an open end of the pipe made above. F_SETFD takes an // int, no pointer. unsafe { libc::fcntl(parent_fd, libc::F_SETFD, libc::FD_CLOEXEC) }; // Set up file actions: redirect child's stdin/stdout to the pipe // SAFETY: posix_spawn_file_actions_t is a plain C struct; all-zero bytes is // a valid value to pass to _init, which sets it up next. let mut file_actions: libc::posix_spawn_file_actions_t = unsafe { std::mem::zeroed() }; // SAFETY: file_actions is a live, exclusively borrowed local. _init is the // required first call on it. if unsafe { libc::posix_spawn_file_actions_init(&mut file_actions) } != 0 { // SAFETY: both fds are open ends of the pipe made above. This path // returns right after, so neither is used again. unsafe { libc::close(pipe_fds[0]); libc::close(pipe_fds[1]); } return ptr::null_mut(); } // Close the parent's end in the child // SAFETY: file_actions was initialized above and is live. The fds are open // pipe ends; these calls only record actions. unsafe { libc::posix_spawn_file_actions_addclose(&mut file_actions, parent_fd) }; // Redirect child's target fd (stdin or stdout) to the child's end of the pipe // SAFETY: file_actions was initialized above and is live. The fds are open // pipe ends; these calls only record actions. unsafe { libc::posix_spawn_file_actions_adddup2(&mut file_actions, child_fd, child_target_fd) }; // Close the child's original pipe fd (now duplicated to target) // SAFETY: file_actions was initialized above and is live. The fds are open // pipe ends; these calls only record actions. unsafe { libc::posix_spawn_file_actions_addclose(&mut file_actions, child_fd) }; // Build argv for /bin/sh -c let sh = c"/bin/sh"; let dash_c = c"-c"; let cmd_cstr = match std::ffi::CString::new(command_str) { Ok(c) => c, Err(_) => { // SAFETY: file_actions was initialized above and is destroyed once // on this path. The pipe fds are open and unused after the return. unsafe { libc::posix_spawn_file_actions_destroy(&mut file_actions); libc::close(pipe_fds[0]); libc::close(pipe_fds[1]); } return ptr::null_mut(); } }; let argv: [*const c_char; 4] = [sh.as_ptr(), dash_c.as_ptr(), cmd_cstr.as_ptr(), ptr::null()]; // Doctor the environment via the same path as exec-family calls let envp = get_environ(); // SAFETY: envp is the process environ (get_environ), a null-terminated // array of NUL-terminated strings, which is resolve_environment's contract. // A concurrent setenv in another thread would race, as it does for libc. let resolved_env = unsafe { resolve_environment(envp) }; let envp_to_use = resolved_env.as_ptr(); // Spawn the child let mut child_pid: pid_t = 0; // SAFETY: func_ptr is non-null (checked first) and comes from // dlsym(RTLD_NEXT, "posix_spawnp"), so the transmute is sound. child_pid, // file_actions and argv are live locals. argv is null-terminated and its // strings (sh, dash_c, cmd_cstr) outlive the call. attrp is null, which is // allowed. envp_to_use is kept alive by resolved_env. let spawn_result = unsafe { let func_ptr = REAL_POSIX_SPAWNP.load(Ordering::SeqCst); if func_ptr.is_null() { log::error!("Real posix_spawnp function not found"); libc::posix_spawn_file_actions_destroy(&mut file_actions); libc::close(pipe_fds[0]); libc::close(pipe_fds[1]); return ptr::null_mut(); } type PosixSpawnpFunc = unsafe extern "C" fn( pid: *mut pid_t, file: *const c_char, file_actions: *const posix_spawn_file_actions_t, attrp: *const posix_spawnattr_t, argv: *const *const c_char, envp: *const *const c_char, ) -> c_int; let real_func: PosixSpawnpFunc = std::mem::transmute(func_ptr); real_func(&mut child_pid, sh.as_ptr(), &file_actions, ptr::null(), argv.as_ptr(), envp_to_use) }; // SAFETY: file_actions was initialized above and is destroyed exactly once, // after the spawn call is done with it. unsafe { libc::posix_spawn_file_actions_destroy(&mut file_actions) }; if spawn_result != 0 { // SAFETY: both fds are still open (we own them) and are not used after // this return. unsafe { libc::close(pipe_fds[0]); libc::close(pipe_fds[1]); set_errno(spawn_result); } return ptr::null_mut(); } // Close the child's end in the parent // SAFETY: child_fd is an open pipe end owned by us. The child has its own // copy and the parent never uses it again. unsafe { libc::close(child_fd) }; // Wrap the parent's fd in a FILE* // SAFETY: parent_fd is open and owned by us. mode is non-null and // NUL-terminated per this fn's # Safety, and fdopen does not keep the // pointer. let file_ptr = unsafe { libc::fdopen(parent_fd, mode) }; if file_ptr.is_null() { // SAFETY: fdopen failed, so parent_fd is still ours to close. child_pid // is the child just spawned; killing and reaping it leaves no zombie. A // null status pointer is allowed for waitpid. unsafe { libc::close(parent_fd); libc::kill(child_pid, libc::SIGKILL); libc::waitpid(child_pid, ptr::null_mut(), 0); } return ptr::null_mut(); } // Track the child pid so pclose can waitpid it POPEN_CHILDREN.lock().unwrap_or_else(|e| e.into_inner()).insert(file_ptr as usize, child_pid); file_ptr } /// Rust implementation of pclose /// /// Called from C shim for: pclose /// /// Retrieves the child pid associated with the FILE* (stored by our popen), /// closes the stream, and waits for the child to exit. /// /// # Safety /// This function is unsafe because it dereferences a raw FILE pointer. #[cfg(all(has_symbol_popen, not(target_os = "android")))] #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_pclose(stream: *mut libc::FILE) -> c_int { if stream.is_null() { set_errno(libc::EINVAL); return -1; } // Look up the child pid let child_pid = POPEN_CHILDREN.lock().unwrap_or_else(|e| e.into_inner()).remove(&(stream as usize)); // If we don't have a tracked pid, this FILE* wasn't opened by our popen. // Fall back to the real pclose. let Some(pid) = child_pid else { let func_ptr = REAL_PCLOSE.load(Ordering::SeqCst); if !func_ptr.is_null() { type PcloseFunc = unsafe extern "C" fn(stream: *mut libc::FILE) -> c_int; // SAFETY: func_ptr is non-null (checked above) and comes from // dlsym(RTLD_NEXT, "pclose"), so PcloseFunc is its real signature. let real_func: PcloseFunc = unsafe { std::mem::transmute(func_ptr) }; // SAFETY: stream is non-null (checked at the top). It was not // opened by our popen, so the real pclose is the right owner for // it. return unsafe { real_func(stream) }; } set_errno(libc::EINVAL); return -1; }; // Close the stream (this closes the underlying fd) // SAFETY: stream is non-null and came from rust_popen (its pid was in // POPEN_CHILDREN and is now removed, so it is closed once). The caller must // not use it after pclose. unsafe { libc::fclose(stream) }; // Wait for the child let mut status: c_int = 0; loop { // SAFETY: status is a live local c_int, the only thing waitpid writes. let ret = unsafe { libc::waitpid(pid, &mut status, 0) }; if ret != -1 || get_errno() != libc::EINTR { break; } } status } /// Rust implementation of system /// /// Called from C shim for: system /// /// Instead of calling the real `system` (which uses `environ` internally and /// may bypass our LD_PRELOAD on older glibc), we reimplement it using /// `posix_spawnp` with a doctored envp — the same path as exec-family calls. /// This eliminates the need for `ensure_environ_has_session_vars` / `setenv`. /// /// Follows POSIX system() semantics: blocks SIGCHLD, ignores SIGINT/SIGQUIT /// during the child's execution, then waitpid's and returns the status. /// `system(NULL)` returns non-zero to indicate a shell is available. /// /// # Safety /// This function is unsafe because it: /// - Dereferences a raw pointer (`command`) which could be null or invalid /// - Spawns child processes and manipulates signal masks /// /// The caller must ensure that `command` is a valid null-terminated C string. #[cfg(all(has_symbol_system, not(target_os = "android")))] #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_system(command: *const c_char) -> c_int { // system(NULL) returns non-zero to indicate a command processor is available if command.is_null() { return 1; } // SAFETY: command is non-null (checked above) and NUL-terminated per this // fn's # Safety. The borrowed str is only used inside this call; the caller // keeps the C string alive. let command_str = match unsafe { CStr::from_ptr(command) }.to_str() { Ok(s) => s, Err(_) => { log::warn!("Failed to parse system command as UTF-8"); set_errno(libc::EINVAL); return -1; } }; report(|| { Ok(Execution { executable: PathBuf::from("/bin/sh"), arguments: vec!["/bin/sh".to_string(), "-c".to_string(), command_str.to_string()], working_dir: working_dir()?, environment: std::env::vars().collect(), }) }); // Build argv for /bin/sh -c let sh = c"/bin/sh"; let dash_c = c"-c"; let cmd_cstr = match std::ffi::CString::new(command_str) { Ok(c) => c, Err(_) => { set_errno(libc::EINVAL); return -1; } }; let argv: [*const c_char; 4] = [sh.as_ptr(), dash_c.as_ptr(), cmd_cstr.as_ptr(), ptr::null()]; // Block SIGCHLD, ignore SIGINT and SIGQUIT (POSIX system() semantics) // SAFETY: sigset_t is a plain C type; all-zero bytes is a valid value, and // sigemptyset initializes it right below. let mut block_mask: libc::sigset_t = unsafe { std::mem::zeroed() }; // SAFETY: sigset_t is a plain C type; all-zero bytes is a valid value, and // sigemptyset initializes it right below. let mut old_mask: libc::sigset_t = unsafe { std::mem::zeroed() }; // SAFETY: block_mask and old_mask are live locals. sigemptyset and // sigaddset write block_mask, sigprocmask reads it and writes old_mask. // SIGCHLD is a valid signal. unsafe { libc::sigemptyset(&mut block_mask); libc::sigaddset(&mut block_mask, libc::SIGCHLD); libc::sigprocmask(libc::SIG_BLOCK, &block_mask, &mut old_mask); } // SAFETY: signal() with SIG_IGN installs no handler code. The old // disposition is saved and restored below, as libc system() does. let old_sigint = unsafe { libc::signal(libc::SIGINT, libc::SIG_IGN) }; // SAFETY: signal() with SIG_IGN installs no handler code. The old // disposition is saved and restored below, as libc system() does. let old_sigquit = unsafe { libc::signal(libc::SIGQUIT, libc::SIG_IGN) }; // Set up spawn attributes to restore default signal handling in the child // SAFETY: posix_spawnattr_t is a plain C type; all-zero bytes is a valid // value to pass to _init, called first below. let mut attr: libc::posix_spawnattr_t = unsafe { std::mem::zeroed() }; // SAFETY: attr is a live local and posix_spawnattr_init is the first call // on it. default_sigs is zeroed and then set up by sigemptyset. old_mask // was filled by sigprocmask above. unsafe { libc::posix_spawnattr_init(&mut attr); libc::posix_spawnattr_setflags( &mut attr, libc::POSIX_SPAWN_SETSIGDEF as libc::c_short | libc::POSIX_SPAWN_SETSIGMASK as libc::c_short, ); // Restore default handlers for SIGINT, SIGQUIT in the child let mut default_sigs: libc::sigset_t = std::mem::zeroed(); libc::sigemptyset(&mut default_sigs); libc::sigaddset(&mut default_sigs, libc::SIGINT); libc::sigaddset(&mut default_sigs, libc::SIGQUIT); libc::posix_spawnattr_setsigdefault(&mut attr, &default_sigs); // Restore the original signal mask in the child libc::posix_spawnattr_setsigmask(&mut attr, &old_mask); } // Doctor the environment via the same path as exec-family calls let envp = get_environ(); // SAFETY: envp is the process environ (get_environ), a null-terminated // array of NUL-terminated strings, which is resolve_environment's contract. // A concurrent setenv in another thread would race, as it does for libc. let resolved_env = unsafe { resolve_environment(envp) }; let envp_to_use = resolved_env.as_ptr(); // Spawn the child let mut child_pid: pid_t = 0; // SAFETY: func_ptr is non-null (checked first) and comes from // dlsym(RTLD_NEXT, "posix_spawnp"), so the transmute is sound. child_pid // and attr are live locals; attr was initialized above. file_actions is // null, which is allowed. argv is null-terminated and its strings (sh, // dash_c, cmd_cstr) outlive the call. envp_to_use is kept alive by // resolved_env. The early-return path destroys attr once and restores the // signal state saved above. let spawn_result = unsafe { let func_ptr = REAL_POSIX_SPAWNP.load(Ordering::SeqCst); if func_ptr.is_null() { log::error!("Real posix_spawnp function not found"); libc::posix_spawnattr_destroy(&mut attr); libc::signal(libc::SIGINT, old_sigint); libc::signal(libc::SIGQUIT, old_sigquit); libc::sigprocmask(libc::SIG_SETMASK, &old_mask, ptr::null_mut()); return -1; } type PosixSpawnpFunc = unsafe extern "C" fn( pid: *mut pid_t, file: *const c_char, file_actions: *const posix_spawn_file_actions_t, attrp: *const posix_spawnattr_t, argv: *const *const c_char, envp: *const *const c_char, ) -> c_int; let real_func: PosixSpawnpFunc = std::mem::transmute(func_ptr); real_func( &mut child_pid, sh.as_ptr(), ptr::null(), // no file actions &attr, argv.as_ptr(), envp_to_use, ) }; // SAFETY: attr was initialized above and is destroyed once (the early // return in the block above skips this line), after the spawn is done with // it. unsafe { libc::posix_spawnattr_destroy(&mut attr) }; let status = if spawn_result != 0 { // Spawn failed — return as if the shell exited with 127 set_errno(spawn_result); -1 } else { // Wait for the child let mut wstatus: c_int = 0; loop { // SAFETY: wstatus is a live local c_int, the only thing waitpid // writes. child_pid is the child spawned above. let ret = unsafe { libc::waitpid(child_pid, &mut wstatus, 0) }; if ret != -1 || get_errno() != libc::EINTR { break; } } wstatus }; // Restore signal handling // SAFETY: old_sigint and old_sigquit are the dispositions signal() returned // above, and old_mask was filled by sigprocmask. Restoring them is valid. unsafe { libc::signal(libc::SIGINT, old_sigint); libc::signal(libc::SIGQUIT, old_sigquit); libc::sigprocmask(libc::SIG_SETMASK, &old_mask, ptr::null_mut()); } status } // ============================================================================= // Helper functions // ============================================================================= /// Represents either the original environment pointer or a doctored environment. /// /// This enum allows us to either pass through the original `envp` unchanged, /// or use a doctored environment that ensures preload settings are preserved. enum ResolvedEnvironment { /// Use the original environment pointer as-is Original(*const *const c_char), /// Use a doctored environment (owns the data) Doctored(DoctoredEnvironment), } impl ResolvedEnvironment { /// Get a pointer to the environment array suitable for passing to exec functions. fn as_ptr(&self) -> *const *const c_char { match self { Self::Original(ptr) => *ptr, Self::Doctored(doc) => doc.as_ptr(), } } } /// Determine whether to use the original environment or create a doctored one. /// /// Logic: /// - If SESSION is not set: use original envp (we're not in an interception session) /// - If SESSION is set and `in_session` returns true: use original envp (environment is correct) /// - If SESSION is set and `in_session` returns false: create doctored environment /// /// # Safety /// The `envp` pointer must be a valid null-terminated array of null-terminated /// C strings in "KEY=VALUE" format, or null. unsafe fn resolve_environment(envp: *const *const c_char) -> ResolvedEnvironment { match SESSION_CTX.get() { None => { // No session, pass through original environment ResolvedEnvironment::Original(envp) } Some(ctx) => { // SAFETY: envp is resolve_environment's argument, so its # Safety // contract (valid null-terminated array of NUL-terminated strings, // or null) is what in_session needs. if unsafe { in_session(ctx, envp) } { // Environment is still aligned with session, pass through ResolvedEnvironment::Original(envp) } else { // Environment was modified, doctor it match DoctoredEnvironment::from_envp(ctx, envp) { Ok(doctored) => ResolvedEnvironment::Doctored(doctored), Err(_) => ResolvedEnvironment::Original(envp), } } } } } /// Get the process's current `environ` pointer. /// /// On Linux/BSD, this is the `extern char **environ` variable. /// On macOS, we use `_NSGetEnviron()` for reliable access. #[cfg(all(any(has_symbol_popen, has_symbol_system), not(target_os = "android")))] fn get_environ() -> *const *const c_char { #[cfg(target_os = "macos")] { unsafe extern "C" { fn _NSGetEnviron() -> *mut *mut *mut c_char; } // SAFETY: _NSGetEnviron returns a valid pointer to the process's // environ for the whole process lifetime; we only read it. unsafe { *_NSGetEnviron() as *const *const c_char } } #[cfg(not(target_os = "macos"))] { unsafe extern "C" { static environ: *const *const c_char; } // SAFETY: environ is the libc global. Reading the pointer is valid. A // concurrent setenv in another thread would race, as it does for libc. unsafe { environ } } } /// Create a pipe with `O_CLOEXEC` set on both fds. /// /// Uses `pipe2(O_CLOEXEC)` on platforms that support it (Linux, FreeBSD), /// falls back to `pipe()` + `fcntl(FD_CLOEXEC)` on macOS. #[cfg(all(has_symbol_popen, not(target_os = "android")))] fn create_pipe_cloexec(fds: *mut c_int) -> c_int { #[cfg(not(target_os = "macos"))] { // SAFETY: fds must point to a [c_int; 2] for pipe2. The only caller // passes pipe_fds from rust_popen, which is a live local array of two. unsafe { libc::pipe2(fds, libc::O_CLOEXEC) } } #[cfg(target_os = "macos")] { // SAFETY: fds must point to a [c_int; 2] for pipe. The only caller // passes pipe_fds from rust_popen, which is a live local array of two. let ret = unsafe { libc::pipe(fds) }; if ret != 0 { return ret; } // SAFETY: fds points to two c_ints (see above), so *fds and *fds.add(1) // are in bounds. The fds are open since pipe succeeded. unsafe { libc::fcntl(*fds, libc::F_SETFD, libc::FD_CLOEXEC); libc::fcntl(*fds.add(1), libc::F_SETFD, libc::FD_CLOEXEC); } 0 } } /// Report a command execution only if reporter is available fn report(f: F) where F: FnOnce() -> Result, { let Some(reporter) = REPORTER.get() else { return; }; let execution = match f() { Ok(execution) => execution, Err(_err) => { log::debug!("Could not generate execution information"); return; } }; if let Err(err) = reporter.report(execution) { log::debug!("Failed to report execution: {err}"); } } /// Convert a C string to a Rust String unsafe fn as_string(s: *const c_char) -> Result { if s.is_null() { return Err(libc::EINVAL); } // SAFETY: s is non-null (checked above) and NUL-terminated, as the callers' // C APIs require. The borrowed &CStr is only used inside this expression; // the result is copied. match unsafe { CStr::from_ptr(s) }.to_str() { Ok(s) => Ok(s.to_string()), Err(_) => Err(libc::EINVAL), } } /// Convert a C string to a PathBuf unsafe fn as_path_buf(s: *const c_char) -> Result { if s.is_null() { return Err(libc::EINVAL); } // SAFETY: s is non-null (checked above) and NUL-terminated, as the callers' // C APIs require. The borrowed &CStr is only used inside this expression; // the result is copied. match unsafe { CStr::from_ptr(s) }.to_str() { Ok(s) => Ok(PathBuf::from(s)), Err(_) => Err(libc::EINVAL), } } /// Convert a null-terminated array of C strings to a Vec unsafe fn as_string_vec(s: *const *const c_char) -> Result, c_int> { if s.is_null() { return Err(libc::EINVAL); } let mut vec = Vec::new(); let mut i = 0; // SAFETY: s is non-null (checked above). The array is null-terminated, and // the loop stops at the first null, so s.add(i) never goes past the // terminator. while !unsafe { (*s.add(i)).is_null() } { // SAFETY: s.add(i) is in bounds because the entry at i was just seen to // be non-null; as_string null-checks and reads a NUL-terminated string. let arg = unsafe { as_string(*s.add(i)) }?; vec.push(arg); i += 1; } Ok(vec) } /// Convert a null-terminated array of "KEY=VALUE" C strings to a HashMap unsafe fn as_environment(s: *const *const c_char) -> Result, c_int> { if s.is_null() { return Err(libc::EINVAL); } let mut map = HashMap::new(); let mut i = 0; // SAFETY: s is non-null (checked above). The array is null-terminated, and // the loop stops at the first null, so s.add(i) never goes past the // terminator. while !unsafe { (*s.add(i)).is_null() } { // SAFETY: s.add(i) is in bounds because the entry at i was just seen to // be non-null; as_string null-checks and reads a NUL-terminated string. let key_and_value = unsafe { as_string(*s.add(i)) }?; if let Some(pos) = key_and_value.find('=') { let key = key_and_value[..pos].to_string(); let value = key_and_value[pos + 1..].to_string(); map.insert(key, value); } // Note: entries without '=' are technically valid but unusual i += 1; } Ok(map) } /// Get the current working directory fn working_dir() -> Result { let cwd = std::env::current_dir().map_err(|_| libc::EINVAL)?; Ok(cwd) } /// Set the thread-local errno value. fn set_errno(value: c_int) { errno::set_errno(errno::Errno(value)); } /// Read the thread-local errno value. #[cfg(all(any(has_symbol_popen, has_symbol_system), not(target_os = "android")))] fn get_errno() -> c_int { errno::errno().0 } rizsotto-Bear-0c30b27/crates/intercept-preload/src/lib.rs000066400000000000000000000021151527177720300234170ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Library for Bear interception via LD_PRELOAD //! //! The library captures system calls and reports them to the collector. // Only include implementation when building on unix #[cfg(target_family = "unix")] mod implementation; #[cfg(target_family = "unix")] mod session; // Re-export implementations #[cfg(target_family = "unix")] pub use implementation::*; /// Package version from Cargo.toml, used to derive the null-terminated C export below. const PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); /// Version information for the library, exported as a null-terminated C string. /// /// Derived automatically from the workspace version in `Cargo.toml` so it can /// never drift out of sync. #[unsafe(no_mangle)] pub static LIBEXEC_VERSION: [u8; PKG_VERSION.len() + 1] = { let src = PKG_VERSION.as_bytes(); let mut buf = [0u8; PKG_VERSION.len() + 1]; let mut i = 0; while i < src.len() { buf[i] = src[i]; i += 1; } // buf[src.len()] is already 0 from zero-initialization — the null terminator. buf }; rizsotto-Bear-0c30b27/crates/intercept-preload/src/session.rs000066400000000000000000001112201527177720300243320ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Session management and environment doctoring for the preload library. //! //! This module handles: //! - Capturing environment variables at library load time //! - Storing session state (preload path, destination address) //! - "Doctoring" environments to ensure child processes continue interception //! //! ## Restoration policy //! //! Bear's interception variables are always restored in child process environments. //! The two key variables are: //! //! - The preload variable (`LD_PRELOAD` on Linux, `DYLD_INSERT_LIBRARIES` on macOS): //! Bear's library must be the **first** entry to ensure our exec overrides have //! priority over competing preload libraries. If another library's `execve` runs //! before ours and strips the preload variable, our doctoring never fires. //! //! - `BEAR_INTERCEPT`: encodes the session state (destination address + library path). //! Must exactly match the captured session state. //! //! When either condition fails, the environment is doctored to restore both. //! //! ## Co-resident library preservation //! //! When an envp has no preload variable at all (e.g. after `env -i`), the startup //! snapshot is used as the base to preserve co-resident libraries (like Gentoo's //! `libsandbox.so`). This is a compatibility-first policy. //! //! The startup snapshot is best-effort: it is captured from `environ` at constructor //! time, which may already reflect modifications by earlier constructors in other //! preload libraries. use std::ffi::{CStr, CString}; use std::net::SocketAddr; use std::path::PathBuf; use std::sync::OnceLock; use intercept::environment::KEY_INTERCEPT_STATE; #[cfg(not(target_os = "macos"))] use intercept::environment::KEY_OS_PRELOAD_PATH; #[cfg(target_os = "macos")] use intercept::environment::{KEY_OS_MACOS_FLAT_NAMESPACE, KEY_OS_MACOS_PRELOAD_PATH}; use intercept::state::PreloadState; use libc::{c_char, c_int}; /// The preload environment variable key for the current platform. #[cfg(target_os = "macos")] pub const PRELOAD_KEY: &str = KEY_OS_MACOS_PRELOAD_PATH; #[cfg(not(target_os = "macos"))] pub const PRELOAD_KEY: &str = KEY_OS_PRELOAD_PATH; /// Combined session state, initialized atomically from the C constructor. /// /// Replaces the previous separate `SESSION` and `INITIAL_PRELOAD` globals, /// ensuring both are always initialized together. pub static SESSION_CTX: OnceLock = OnceLock::new(); /// Holds the parsed session state and the normalized startup preload snapshot. pub struct SessionContext { /// Bear's session state (destination + library path). pub state: PreloadState, /// Best-effort startup snapshot of the preload variable, parsed and normalized. /// `None` if the preload variable was absent at startup. /// Stored as `Vec` so we avoid re-parsing every time we doctor. pub startup_preload: Option>, } /// Iterate over key-value pairs in a C-style envp array. /// /// Yields `(key, value, full_entry)` for well-formed `"KEY=VALUE"` entries. /// The `full_entry` includes the `=` separator, useful when callers need /// to copy the entry verbatim (e.g. `DoctoredEnvironment::from_envp`). /// Skips entries that are not valid UTF-8 or that lack `'='`. /// /// The returned `&str` references borrow directly from the C strings pointed /// to by `envp`. The caller must ensure those strings remain valid for `'a`. /// /// # Safety /// The `envp` pointer must be a valid, non-null, null-terminated array of /// null-terminated C strings that remain valid for lifetime `'a`. unsafe fn envp_iter<'a>(envp: *const *const c_char) -> impl Iterator { let mut ptr = envp; std::iter::from_fn(move || { loop { // SAFETY: ptr starts at envp, which this fn's # Safety says is a // valid, non-null array. ptr only moves forward one element after // the entry it points to was seen non-null, so it never passes the // terminating null. if unsafe { (*ptr).is_null() } { return None; } // SAFETY: CStr::from_ptr returns &CStr with an unbounded lifetime. // We re-borrow it as &'a CStr, which is sound because the caller // guarantees the C strings remain valid for 'a. let cstr: &'a CStr = unsafe { CStr::from_ptr(*ptr) }; // SAFETY: the entry at ptr was just read as non-null, so the // terminating null is at or after ptr + 1, and the add stays inside // the array. ptr = unsafe { ptr.add(1) }; let Ok(entry) = cstr.to_str() else { log::debug!("envp entry is not valid UTF-8, skipping"); continue; }; let Some((key, value)) = entry.split_once('=') else { log::debug!("envp entry has no '=' delimiter, skipping: {entry}"); continue; }; return Some((key, value, entry)); } }) } /// Create a PreloadState by extracting the intercept state from a C-style envp array. /// /// Walks the envp to find `BEAR_INTERCEPT` and attempts to parse it into a /// `PreloadState`. /// /// # Safety /// The `envp` pointer must be a valid null-terminated array of null-terminated /// C strings in "KEY=VALUE" format, or null. /// /// # Returns /// - `Some(PreloadState)` if the intercept state variable is found and successfully parsed /// - `None` if `envp` is null, the variable is not found, or parsing fails unsafe fn from_envp(envp: *const *const c_char) -> Option { if envp.is_null() { return None; } // SAFETY: envp is non-null (checked above). This fn's # Safety gives the // rest of envp_iter's contract (null-terminated array of NUL-terminated // strings). unsafe { envp_iter(envp) }.find(|(key, _, _)| *key == KEY_INTERCEPT_STATE).and_then(|(_, value, _)| { match value.try_into() { Ok(state) => Some(state), Err(_) => { log::debug!("BEAR_INTERCEPT found but failed to parse"); None } } }) } /// Read a single environment variable value from a C-style envp array. /// /// # Safety /// The `envp` pointer must be a valid null-terminated array of null-terminated /// C strings in "KEY=VALUE" format, or null. unsafe fn read_env_value(envp: *const *const c_char, key: &str) -> Option { if envp.is_null() { return None; } // SAFETY: envp is non-null (checked above). This fn's # Safety gives the // rest of envp_iter's contract (null-terminated array of NUL-terminated // strings). unsafe { envp_iter(envp) }.find(|(k, _, _)| *k == key).map(|(_, v, _)| v.to_string()) } /// Normalize a preload value string into a deduplicated list of paths. /// /// Removes empty segments and deduplicates while preserving order. fn normalize_preload(value: &str) -> Vec { let mut seen = std::collections::HashSet::new(); std::env::split_paths(value) .filter(|p| !p.as_os_str().is_empty()) .filter(|p| seen.insert(p.clone())) .collect() } /// Check whether the given envp is already aligned with the expected session state. /// /// Returns `true` when both conditions hold: /// 1. `BEAR_INTERCEPT` is present and parses to a value equal to `state`. /// 2. Bear's library is the **first** entry in the preload variable (`PRELOAD_KEY`). /// /// The first-position requirement exists because preload libraries are loaded in /// order. Bear must be first so that our exec overrides take priority: if a /// competing library's `execve` runs before ours and strips the preload variable, /// our doctoring never fires. When Bear is not first, this function returns `false` /// so that the caller re-doctors the environment to restore the correct ordering. pub unsafe fn in_session(ctx: &SessionContext, envp: *const *const c_char) -> bool { if envp.is_null() { return false; } let mut intercept_state_matches = false; let mut preload_matches = false; // SAFETY: envp is non-null (checked above). The caller must pass a valid // null-terminated array of NUL-terminated strings, which is what envp_iter // needs. for (key, value, _) in unsafe { envp_iter(envp) } { if key == KEY_INTERCEPT_STATE { if let Ok(parsed_state) = PreloadState::try_from(value) { intercept_state_matches = parsed_state == ctx.state; } } else if key == PRELOAD_KEY { preload_matches = std::env::split_paths(value).next().as_deref() == Some(ctx.state.library.as_path()); } } intercept_state_matches && preload_matches } /// Compute the desired preload variable value for a child process. /// /// This is the single source of truth for preload restoration, used by both /// `DoctoredEnvironment::from_envp` (for exec-family calls) and /// `ensure_environ_has_session_vars` (for system/popen calls). /// /// Policy: /// - `Some(value)` with a non-empty value: prepend Bear's library to it. /// - `Some("")` (present but empty) or `None` (absent): fall back to the /// startup snapshot so co-resident libraries survive. /// - If no snapshot exists either, use just Bear's library. pub fn desired_preload_value(ctx: &SessionContext, current: Option<&str>) -> Result { let base = match current { Some(v) if !v.is_empty() => v.to_string(), _ => { // Fall back to the startup snapshot. match &ctx.startup_preload { Some(paths) if !paths.is_empty() => std::env::join_paths(paths) .map_err(|_| libc::EINVAL)? .into_string() .map_err(|_| libc::EINVAL)?, _ => String::new(), } } }; intercept::environment::insert_to_path(&base, &ctx.state.library).map_err(|_| libc::EINVAL) } /// A doctored environment that owns its strings and can provide a C-style envp. /// /// This struct manages the memory for environment strings and provides a /// null-terminated array of pointers suitable for passing to exec functions. pub struct DoctoredEnvironment { /// The environment strings in "KEY=VALUE" format #[allow(dead_code, reason = "kept so the CStrings stay valid while ptrs points into them")] strings: Vec, /// Pointers to the strings, plus a null terminator ptrs: Vec<*const c_char>, } impl DoctoredEnvironment { /// Private constructor that creates a DoctoredEnvironment from a vector of CStrings. /// /// This builds the pointer array from the strings. fn from_strings(strings: Vec) -> Self { let mut ptrs: Vec<*const c_char> = strings.iter().map(|s| s.as_ptr()).collect(); ptrs.push(std::ptr::null()); DoctoredEnvironment { strings, ptrs } } /// Create a doctored environment from a session context and the received environment. /// /// Returns an error if any key or value contains a null byte. /// /// The method is called when `in_session` returned false, therefore the /// environment is not aligned with the preload mode settings. The result environment /// will be aligned with the preload mode settings based on the context. pub fn from_envp(ctx: &SessionContext, envp: *const *const c_char) -> Result { let mut strings = Vec::new(); let mut original_preload_value: Option = None; // Copy existing environment variables, skipping ones we'll set ourselves. // Uses envp_iter for consistent parsing and malformed-entry logging. if !envp.is_null() { // SAFETY: envp is non-null (checked above). Its only caller, // resolve_environment, passes one under its own # Safety contract. // Every entry is copied into an owned CString inside the loop, so // no borrow outlives the call. This fn is not marked unsafe, so the // contract is not enforced by the type. for (key, value, full_entry) in unsafe { envp_iter(envp) } { #[cfg(target_os = "macos")] let dominated_by_bear = key == KEY_INTERCEPT_STATE || key == PRELOAD_KEY || key == KEY_OS_MACOS_FLAT_NAMESPACE; #[cfg(not(target_os = "macos"))] let dominated_by_bear = key == KEY_INTERCEPT_STATE || key == PRELOAD_KEY; if key == PRELOAD_KEY { original_preload_value = Some(value.to_string()); } if !dominated_by_bear { strings.push(CString::new(full_entry).map_err(|_| libc::EINVAL)?); } } } // Add KEY_INTERCEPT_STATE with serialized state (JSON) let state_json: String = ctx.state.clone().try_into().map_err(|_| libc::EINVAL)?; let intercept_entry = format!("{}={}", KEY_INTERCEPT_STATE, state_json); strings.push(CString::new(intercept_entry).map_err(|_| libc::EINVAL)?); // Add PRELOAD_KEY with the library path inserted at front. // Uses the shared policy: fall back to startup snapshot when envp had no preload key. let preload_value = desired_preload_value(ctx, original_preload_value.as_deref())?; let preload_entry = format!("{}={}", PRELOAD_KEY, preload_value); strings.push(CString::new(preload_entry).map_err(|_| libc::EINVAL)?); // On macOS, add the flat namespace flag #[cfg(target_os = "macos")] { let flat_namespace_entry = format!("{}=1", KEY_OS_MACOS_FLAT_NAMESPACE); strings.push(CString::new(flat_namespace_entry).map_err(|_| libc::EINVAL)?); } Ok(Self::from_strings(strings)) } /// Get a pointer to the envp array suitable for passing to exec functions. pub fn as_ptr(&self) -> *const *const c_char { self.ptrs.as_ptr() } } /// Initialize the global session from a C-style envp array. /// /// Returns the destination address if present (for reporter initialization). /// /// # Safety /// The `envp` pointer must be a valid null-terminated array of null-terminated /// C strings in "KEY=VALUE" format, or null. pub unsafe fn init_session_from_envp(envp: *const *const c_char) -> Option { if envp.is_null() { log::info!("session init failed: environ is null pointer"); return None; } // SAFETY: envp is non-null (checked above) and this fn's # Safety gives // from_envp its contract (null-terminated array of NUL-terminated strings). match unsafe { from_envp(envp) } { None => { log::info!("session init failed: variables not found"); None } Some(session) => { let destination = session.destination; // Capture and normalize the preload variable before anything modifies it. // This preserves co-resident libraries (e.g. libsandbox.so). // SAFETY: same contract as above: envp is non-null and valid per // this fn's # Safety. The returned String is owned. let startup_preload = unsafe { read_env_value(envp, PRELOAD_KEY) }.map(|v| normalize_preload(&v)); let ctx = SessionContext { state: session, startup_preload }; if SESSION_CTX.set(ctx).is_err() { log::debug!("SESSION_CTX already set, ignoring duplicate init"); } Some(destination) } } } #[cfg(test)] mod tests { use super::*; use std::ffi::CString; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::PathBuf; use intercept::state::PreloadState; /// Default test destination address (localhost:12345). const TEST_DESTINATION: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 12345); /// Default test library path. const TEST_LIBRARY_PATH: &str = "/usr/lib/libear.so"; /// Helper struct to manage C-style environment arrays for testing. /// This ensures the CStrings stay alive while we use pointers to them. struct TestEnvp { #[allow(dead_code, reason = "kept so the CStrings stay valid while ptrs points into them")] strings: Vec, ptrs: Vec<*const c_char>, } impl TestEnvp { /// Create a TestEnvp from a slice of "KEY=VALUE" strings. fn new(entries: &[&str]) -> Self { let strings: Vec = entries.iter().map(|s| CString::new(*s).unwrap()).collect(); let mut ptrs: Vec<*const c_char> = strings.iter().map(|s| s.as_ptr()).collect(); ptrs.push(std::ptr::null()); // Null terminator TestEnvp { strings, ptrs } } /// Get a pointer to the envp array. fn as_ptr(&self) -> *const *const c_char { self.ptrs.as_ptr() } } /// Helper function to create a test PreloadState with default destination and library. fn create_test_state() -> PreloadState { PreloadState { destination: TEST_DESTINATION, library: PathBuf::from(TEST_LIBRARY_PATH) } } /// Helper function to create a test SessionContext with no startup preload. fn create_test_ctx() -> SessionContext { SessionContext { state: create_test_state(), startup_preload: None } } /// Helper function to serialize a PreloadState to JSON for environment variable. fn state_to_json(state: &PreloadState) -> String { serde_json::to_string(state).unwrap() } #[test] fn test_from_envp_returns_none_for_null_pointer() { // SAFETY: a null envp is allowed by from_envp's contract and returns // None. let result = unsafe { from_envp(std::ptr::null()) }; assert!(result.is_none()); } #[test] fn test_from_envp_returns_none_when_intercept_state_not_found() { let envp = TestEnvp::new(&["PATH=/usr/bin", "HOME=/home/user", "SHELL=/bin/sh"]); // SAFETY: envp holds the CStrings it points into, and it outlives this // call. The array is null-terminated. let result = unsafe { from_envp(envp.as_ptr()) }; assert!(result.is_none()); } #[test] fn test_from_envp_returns_none_for_empty_environment() { let envp = TestEnvp::new(&[]); // SAFETY: envp holds the CStrings it points into, and it outlives this // call. The array is null-terminated. let result = unsafe { from_envp(envp.as_ptr()) }; assert!(result.is_none()); } #[test] fn test_from_envp_returns_none_for_invalid_json() { let envp = TestEnvp::new(&["PATH=/usr/bin", "BEAR_INTERCEPT=invalid_json_here", "HOME=/home/user"]); // SAFETY: envp holds the CStrings it points into, and it outlives this // call. The array is null-terminated. let result = unsafe { from_envp(envp.as_ptr()) }; assert!(result.is_none()); } #[test] fn test_from_envp_returns_state_when_found() { let state = create_test_state(); let envp = { let state_json = state_to_json(&state); let intercept_entry = format!("{}={}", KEY_INTERCEPT_STATE, state_json); TestEnvp::new(&["PATH=/usr/bin", &intercept_entry, "HOME=/home/user"]) }; // SAFETY: envp holds the CStrings it points into, and it outlives this // call. The array is null-terminated. let result = unsafe { from_envp(envp.as_ptr()) }; assert!(result.is_some()); let parsed_state = result.unwrap(); assert_eq!(parsed_state, state); } #[test] fn test_from_envp_finds_state_at_beginning() { let state = create_test_state(); let envp = { let state_json = state_to_json(&state); let intercept_entry = format!("{}={}", KEY_INTERCEPT_STATE, state_json); TestEnvp::new(&[&intercept_entry, "PATH=/usr/bin"]) }; // SAFETY: envp holds the CStrings it points into, and it outlives this // call. The array is null-terminated. let result = unsafe { from_envp(envp.as_ptr()) }; assert!(result.is_some()); assert_eq!(result.unwrap(), state); } #[test] fn test_from_envp_finds_state_at_end() { let state = create_test_state(); let envp = { let state_json = state_to_json(&state); let intercept_entry = format!("{}={}", KEY_INTERCEPT_STATE, state_json); TestEnvp::new(&["PATH=/usr/bin", "HOME=/home/user", &intercept_entry]) }; // SAFETY: envp holds the CStrings it points into, and it outlives this // call. The array is null-terminated. let result = unsafe { from_envp(envp.as_ptr()) }; assert!(result.is_some()); assert_eq!(result.unwrap(), state); } #[test] fn test_in_session_returns_false_for_null_pointer() { let ctx = create_test_ctx(); // SAFETY: a null envp is allowed by in_session's contract and returns // false. let result = unsafe { in_session(&ctx, std::ptr::null()) }; assert!(!result); } #[test] fn test_in_session_returns_false_when_intercept_state_missing() { let ctx = create_test_ctx(); let preload_entry = format!("{}={}", PRELOAD_KEY, TEST_LIBRARY_PATH); let envp = TestEnvp::new(&["PATH=/usr/bin", &preload_entry]); // SAFETY: envp holds the CStrings it points into, and it outlives this // call. The array is null-terminated. let result = unsafe { in_session(&ctx, envp.as_ptr()) }; assert!(!result); } #[test] fn test_in_session_returns_false_when_preload_missing() { let ctx = create_test_ctx(); let state_json = state_to_json(&ctx.state); let intercept_entry = format!("{}={}", KEY_INTERCEPT_STATE, state_json); let envp = TestEnvp::new(&["PATH=/usr/bin", &intercept_entry]); // SAFETY: envp holds the CStrings it points into, and it outlives this // call. The array is null-terminated. let result = unsafe { in_session(&ctx, envp.as_ptr()) }; assert!(!result); } #[test] fn test_in_session_returns_false_when_state_differs() { let ctx = create_test_ctx(); let envp = { let different_destination = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 54321); let different_state = PreloadState { destination: different_destination, library: PathBuf::from(TEST_LIBRARY_PATH), }; let state_json = state_to_json(&different_state); let intercept_entry = format!("{}={}", KEY_INTERCEPT_STATE, state_json); let preload_entry = format!("{}={}", PRELOAD_KEY, TEST_LIBRARY_PATH); TestEnvp::new(&[&intercept_entry, &preload_entry]) }; // SAFETY: envp holds the CStrings it points into, and it outlives this // call. The array is null-terminated. let result = unsafe { in_session(&ctx, envp.as_ptr()) }; assert!(!result); } #[test] fn test_in_session_returns_false_when_library_not_first_in_preload() { let ctx = create_test_ctx(); let envp = { let state_json = state_to_json(&ctx.state); let intercept_entry = format!("{}={}", KEY_INTERCEPT_STATE, state_json); // Library is second in the preload path let preload_entry = format!("{}=/other/lib.so:{}", PRELOAD_KEY, TEST_LIBRARY_PATH); TestEnvp::new(&[&intercept_entry, &preload_entry]) }; // SAFETY: envp holds the CStrings it points into, and it outlives this // call. The array is null-terminated. let result = unsafe { in_session(&ctx, envp.as_ptr()) }; assert!(!result); } #[test] fn test_in_session_returns_true_when_all_matches() { let ctx = create_test_ctx(); let envp = { let state_json = state_to_json(&ctx.state); let intercept_entry = format!("{}={}", KEY_INTERCEPT_STATE, state_json); let preload_entry = format!("{}={}", PRELOAD_KEY, TEST_LIBRARY_PATH); TestEnvp::new(&["PATH=/usr/bin", &intercept_entry, &preload_entry]) }; // SAFETY: envp holds the CStrings it points into, and it outlives this // call. The array is null-terminated. let result = unsafe { in_session(&ctx, envp.as_ptr()) }; assert!(result); } #[test] fn test_in_session_returns_true_with_library_first_among_multiple() { let ctx = create_test_ctx(); let envp = { let state_json = state_to_json(&ctx.state); let intercept_entry = format!("{}={}", KEY_INTERCEPT_STATE, state_json); // Library is first but there are other libraries too let preload_entry = format!("{}={}:/other/lib.so:/another/lib.so", PRELOAD_KEY, TEST_LIBRARY_PATH); TestEnvp::new(&[&intercept_entry, &preload_entry]) }; // SAFETY: envp holds the CStrings it points into, and it outlives this // call. The array is null-terminated. let result = unsafe { in_session(&ctx, envp.as_ptr()) }; assert!(result); } #[test] fn test_in_session_returns_false_for_empty_environment() { let ctx = create_test_ctx(); let envp = TestEnvp::new(&[]); // SAFETY: envp holds the CStrings it points into, and it outlives this // call. The array is null-terminated. let result = unsafe { in_session(&ctx, envp.as_ptr()) }; assert!(!result); } #[test] fn test_doctored_environment_from_null_envp() { let ctx = create_test_ctx(); let result = DoctoredEnvironment::from_envp(&ctx, std::ptr::null()); assert!(result.is_ok()); let doctored = result.unwrap(); // Should have at least the intercept state and preload entries assert!(!doctored.ptrs.is_empty()); // Last pointer should be null assert!(doctored.ptrs.last().unwrap().is_null()); } #[test] fn test_doctored_environment_preserves_other_variables() { let ctx = create_test_ctx(); let envp = TestEnvp::new(&["PATH=/usr/bin", "HOME=/home/user", "SHELL=/bin/sh"]); let result = DoctoredEnvironment::from_envp(&ctx, envp.as_ptr()); assert!(result.is_ok()); let doctored = result.unwrap(); let env_strings: Vec = doctored.strings.iter().map(|s| s.to_string_lossy().to_string()).collect(); // Check that original variables are preserved assert!(env_strings.iter().any(|s| s == "PATH=/usr/bin")); assert!(env_strings.iter().any(|s| s == "HOME=/home/user")); assert!(env_strings.iter().any(|s| s == "SHELL=/bin/sh")); } #[test] fn test_doctored_environment_adds_intercept_state() { let ctx = create_test_ctx(); let envp = TestEnvp::new(&["PATH=/usr/bin"]); let result = DoctoredEnvironment::from_envp(&ctx, envp.as_ptr()); assert!(result.is_ok()); let doctored = result.unwrap(); let env_strings: Vec = doctored.strings.iter().map(|s| s.to_string_lossy().to_string()).collect(); // Check that intercept state is added let has_intercept = env_strings.iter().any(|s| s.starts_with(&format!("{}=", KEY_INTERCEPT_STATE))); assert!(has_intercept, "Should contain BEAR_INTERCEPT variable"); } #[test] fn test_doctored_environment_adds_preload_key() { let ctx = create_test_ctx(); let envp = TestEnvp::new(&["PATH=/usr/bin"]); let result = DoctoredEnvironment::from_envp(&ctx, envp.as_ptr()); assert!(result.is_ok()); let doctored = result.unwrap(); let env_strings: Vec = doctored.strings.iter().map(|s| s.to_string_lossy().to_string()).collect(); // Check that preload key is added let has_preload = env_strings.iter().any(|s| s.starts_with(&format!("{}=", PRELOAD_KEY))); assert!(has_preload, "Should contain {} variable", PRELOAD_KEY); } #[test] fn test_doctored_environment_library_first_in_preload() { let ctx = create_test_ctx(); let envp = { let existing_preload = format!("{}=/other/lib.so:/another/lib.so", PRELOAD_KEY); TestEnvp::new(&["PATH=/usr/bin", &existing_preload]) }; let result = DoctoredEnvironment::from_envp(&ctx, envp.as_ptr()); assert!(result.is_ok()); let doctored = result.unwrap(); let env_strings: Vec = doctored.strings.iter().map(|s| s.to_string_lossy().to_string()).collect(); // Find the preload entry let preload_entry = env_strings .iter() .find(|s| s.starts_with(&format!("{}=", PRELOAD_KEY))) .expect("Should have preload entry"); // Extract the value and check first path let value = preload_entry.split_once('=').unwrap().1; let paths: Vec = std::env::split_paths(value).collect(); assert_eq!( paths.first().unwrap(), &PathBuf::from(TEST_LIBRARY_PATH), "Library should be first in preload path" ); } #[test] fn test_doctored_environment_as_ptr_returns_valid_pointer() { let ctx = create_test_ctx(); let envp = TestEnvp::new(&["PATH=/usr/bin"]); let result = DoctoredEnvironment::from_envp(&ctx, envp.as_ptr()); assert!(result.is_ok()); let doctored = result.unwrap(); let ptr = doctored.as_ptr(); // Pointer should not be null assert!(!ptr.is_null()); // Should be able to iterate through the envp let mut count = 0; let mut current = ptr; // SAFETY: doctored owns its strings and pointer array, and as_ptr ends // with a null, so the walk stays in bounds before it reaches the // terminator. unsafe { while !(*current).is_null() { count += 1; current = current.add(1); } } assert!(count > 0, "Should have at least one environment entry"); } #[test] fn test_doctored_environment_null_terminated() { let ctx = create_test_ctx(); let envp = TestEnvp::new(&["PATH=/usr/bin", "HOME=/home/user"]); let result = DoctoredEnvironment::from_envp(&ctx, envp.as_ptr()); assert!(result.is_ok()); let doctored = result.unwrap(); // The last element in ptrs should be null assert!(doctored.ptrs.last().unwrap().is_null()); } #[test] fn test_init_session_from_envp_returns_none_for_null() { // Note: We can't easily test SESSION_CTX initialization multiple times // since it's a OnceLock. We test the return value behavior. // SAFETY: a null envp is allowed and handled by returning early. let result = unsafe { init_session_from_envp(std::ptr::null()) }; assert!(result.is_none()); } #[test] fn test_init_session_from_envp_returns_none_when_not_found() { let envp = TestEnvp::new(&["PATH=/usr/bin", "HOME=/home/user"]); // SAFETY: envp holds the CStrings it points into, and it outlives this // call. The array is null-terminated. let result = unsafe { init_session_from_envp(envp.as_ptr()) }; assert!(result.is_none()); } #[test] fn test_from_envp_handles_malformed_entry_without_equals() { let state = create_test_state(); let envp = { let state_json = state_to_json(&state); let intercept_entry = format!("{}={}", KEY_INTERCEPT_STATE, state_json); // Include a malformed entry (no equals sign) TestEnvp::new(&["MALFORMED_ENTRY", &intercept_entry, "PATH=/usr/bin"]) }; // SAFETY: envp holds the CStrings it points into, and it outlives this // call. The array is null-terminated. let result = unsafe { from_envp(envp.as_ptr()) }; // Should still find the valid state assert!(result.is_some()); assert_eq!(result.unwrap(), state); } #[test] fn test_in_session_handles_malformed_entry_without_equals() { let ctx = create_test_ctx(); let envp = { let state_json = state_to_json(&ctx.state); let intercept_entry = format!("{}={}", KEY_INTERCEPT_STATE, state_json); let preload_entry = format!("{}={}", PRELOAD_KEY, TEST_LIBRARY_PATH); TestEnvp::new(&["MALFORMED", &intercept_entry, &preload_entry]) }; // SAFETY: envp holds the CStrings it points into, and it outlives this // call. The array is null-terminated. let result = unsafe { in_session(&ctx, envp.as_ptr()) }; assert!(result); } #[test] fn test_doctored_environment_handles_entry_with_multiple_equals() { let ctx = create_test_ctx(); // Entry with value containing equals signs let envp = TestEnvp::new(&["MY_VAR=value=with=equals", "PATH=/usr/bin"]); let result = DoctoredEnvironment::from_envp(&ctx, envp.as_ptr()); assert!(result.is_ok()); let doctored = result.unwrap(); let env_strings: Vec = doctored.strings.iter().map(|s| s.to_string_lossy().to_string()).collect(); // The variable with multiple equals should be preserved correctly // split_once only splits on the first '=' assert!(env_strings.iter().any(|s| s == "MY_VAR=value=with=equals")); } #[test] fn test_from_envp_with_ipv6_address() { let ipv6_addr = SocketAddr::new(IpAddr::V6(std::net::Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 8080); let state = PreloadState { destination: ipv6_addr, library: PathBuf::from(TEST_LIBRARY_PATH) }; let envp = { let state_json = state_to_json(&state); let intercept_entry = format!("{}={}", KEY_INTERCEPT_STATE, state_json); TestEnvp::new(&[&intercept_entry]) }; // SAFETY: envp holds the CStrings it points into, and it outlives this // call. The array is null-terminated. let result = unsafe { from_envp(envp.as_ptr()) }; assert!(result.is_some()); let parsed = result.unwrap(); assert_eq!(parsed, state); assert!(parsed.destination.ip().is_ipv6()); } #[test] fn test_doctored_environment_from_strings() { // Test the private from_strings method indirectly through from_envp let ctx = create_test_ctx(); let envp = TestEnvp::new(&[]); let result = DoctoredEnvironment::from_envp(&ctx, envp.as_ptr()); assert!(result.is_ok()); let doctored = result.unwrap(); // Verify strings and ptrs are consistent assert_eq!(doctored.ptrs.len(), doctored.strings.len() + 1); // +1 for null terminator } #[test] fn test_normalize_preload_deduplicates() { let result = normalize_preload("/a.so:/b.so:/a.so"); assert_eq!(result, vec![PathBuf::from("/a.so"), PathBuf::from("/b.so")]); } #[test] fn test_normalize_preload_removes_empty_segments() { let result = normalize_preload("/a.so::/b.so:"); assert_eq!(result, vec![PathBuf::from("/a.so"), PathBuf::from("/b.so")]); } #[test] fn test_normalize_preload_preserves_order() { let result = normalize_preload("/c.so:/a.so:/b.so"); assert_eq!(result, vec![PathBuf::from("/c.so"), PathBuf::from("/a.so"), PathBuf::from("/b.so")]); } #[test] fn test_desired_preload_value_with_startup_snapshot() { let ctx = SessionContext { state: create_test_state(), startup_preload: Some(vec![PathBuf::from("/other/lib.so")]), }; // When current is absent, should fall back to startup snapshot let result = desired_preload_value(&ctx, None).unwrap(); let paths: Vec = std::env::split_paths(&result).collect(); assert_eq!(paths[0], PathBuf::from(TEST_LIBRARY_PATH)); assert!(paths.contains(&PathBuf::from("/other/lib.so"))); } #[test] fn test_desired_preload_value_with_current() { let ctx = create_test_ctx(); // When current has a value, should prepend Bear's library let result = desired_preload_value(&ctx, Some("/existing/lib.so")).unwrap(); let paths: Vec = std::env::split_paths(&result).collect(); assert_eq!(paths[0], PathBuf::from(TEST_LIBRARY_PATH)); assert!(paths.contains(&PathBuf::from("/existing/lib.so"))); } #[test] fn test_desired_preload_value_empty_current_falls_back() { let ctx = SessionContext { state: create_test_state(), startup_preload: Some(vec![PathBuf::from("/snapshot/lib.so")]), }; // Empty current should fall back to startup snapshot let result = desired_preload_value(&ctx, Some("")).unwrap(); let paths: Vec = std::env::split_paths(&result).collect(); assert_eq!(paths[0], PathBuf::from(TEST_LIBRARY_PATH)); assert!(paths.contains(&PathBuf::from("/snapshot/lib.so"))); } } rizsotto-Bear-0c30b27/crates/intercept-supervisor/000077500000000000000000000000001527177720300223105ustar00rootroot00000000000000rizsotto-Bear-0c30b27/crates/intercept-supervisor/CLAUDE.md000066400000000000000000000032431527177720300235710ustar00rootroot00000000000000## intercept-supervisor The per-module `//!` docs hold the details; this file lists only the cross-cutting constraints that are easy to violate. ## Dependency boundary (load-bearing) May depend on `intercept`. Must NOT depend on `bear`, `config`, or `clap`. The preload cdylib depends on `intercept` only; keeping supervisor-only deps (`signal-hook`, `which`) out of `intercept`'s graph is what stops them from leaking into that cdylib. Adding such an edge here is fine; pushing one down into `intercept` is not. ## Sibling location is relative-path-only `installation.rs` finds `bear-wrapper` and `libexec.so` via relative paths from the current executable. Never hard-code or compute absolute install paths; `INTERCEPT_LIBDIR` is the one build-time knob. `build.rs` reads `INTERCEPT_LIBDIR` (default `lib`), trims it, rejects an empty or absolute value, accepts the literal `$LIB`, and passes it to `installation.rs` through `env!()`. `tests/integration/build.rs` reads the same variable and default and rejects an empty or absolute value. It does not trim before the absolute check and has no `$LIB` case. Keep the two in step. The user-facing side is in `INSTALL.md`. ## Process-group nesting Supervision chains nest in wrapper mode, so only the outermost supervisor creates a process group (`GroupPolicy::Leader` in `supervise.rs`); nested ones `Inherit`. Governed by requirement `cli-signal-forwarding`; rationale in `docs/rationale/supervise-a-compiler-call.md`. ## Wrapper directory The wrapper dir is the deterministic `.bear/` in the build cwd (not a temp dir) and is wiped at the start of each run (`wrapper.rs`). Rationale in `docs/rationale/wrapper-mode-design-decisions.md`. rizsotto-Bear-0c30b27/crates/intercept-supervisor/Cargo.toml000066400000000000000000000014731527177720300242450ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later [package] name = "intercept-supervisor" description = "Driver-side (supervisor) half of Bear's interception runtime (supervise, collector, build environment)." version.workspace = true authors.workspace = true repository.workspace = true homepage.workspace = true license.workspace = true edition.workspace = true rust-version.workspace = true publish = false [lib] name = "intercept_supervisor" path = "src/lib.rs" [dependencies] intercept = { path = "../intercept" } thiserror.workspace = true serde.workspace = true serde_json.workspace = true log.workspace = true signal-hook = { workspace = true, features = ["iterator"] } which.workspace = true [target.'cfg(unix)'.dependencies] libc.workspace = true [dev-dependencies] tempfile.workspace = true [lints] workspace = true rizsotto-Bear-0c30b27/crates/intercept-supervisor/build.rs000066400000000000000000000040641527177720300237610ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later /// Driver executable name (platform-dependent) #[cfg(windows)] const DRIVER_NAME: &str = "bear-driver.exe"; #[cfg(not(windows))] const DRIVER_NAME: &str = "bear-driver"; /// Wrapper executable name (platform-dependent) #[cfg(windows)] const WRAPPER_NAME: &str = "bear-wrapper.exe"; #[cfg(not(windows))] const WRAPPER_NAME: &str = "bear-wrapper"; /// Preload library name (platform-dependent) #[cfg(target_os = "macos")] const PRELOAD_NAME: &str = "libexec.dylib"; #[cfg(not(target_os = "macos"))] const PRELOAD_NAME: &str = "libexec.so"; fn main() { // `src/installation.rs` resolves the runtime install layout from these // compile-time names via `env!()` (its tests also read `DRIVER_NAME`). // `INTERCEPT_LIBDIR` is validated here and forwarded the same way // `tests/integration/build.rs` does for the integration tests. let intercept_libdir = std::env::var("INTERCEPT_LIBDIR").unwrap_or_else(|_| "lib".to_string()); validate_intercept_libdir(&intercept_libdir); println!("cargo:rustc-env=DRIVER_NAME={}", DRIVER_NAME); println!("cargo:rustc-env=WRAPPER_NAME={}", WRAPPER_NAME); println!("cargo:rustc-env=PRELOAD_NAME={}", PRELOAD_NAME); println!("cargo:rustc-env=INTERCEPT_LIBDIR={}", intercept_libdir); println!("cargo:rerun-if-env-changed=INTERCEPT_LIBDIR"); } /// Validate the `INTERCEPT_LIBDIR` environment variable. /// /// Valid values: /// - A non-empty relative path (e.g. `"lib"`, `"lib64"`, `"lib/x86_64-linux-gnu"`). /// - The literal string `"$LIB"` to defer directory selection to runtime/platform /// conventions (commonly interpreted as either `"lib"` or `"lib64"` depending on /// the system). fn validate_intercept_libdir(value: &str) { let value = value.trim(); if value.is_empty() { panic!("INTERCEPT_LIBDIR must not be empty or whitespace-only"); } if value == "$LIB" { return; } let path = std::path::Path::new(value); if path.is_absolute() { panic!("INTERCEPT_LIBDIR must be a relative path, got: {}", value); } } rizsotto-Bear-0c30b27/crates/intercept-supervisor/src/000077500000000000000000000000001527177720300230775ustar00rootroot00000000000000rizsotto-Bear-0c30b27/crates/intercept-supervisor/src/collector.rs000066400000000000000000000313671527177720300254450ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! The module contains the driver-side TCP collector. //! //! The collector is the receiver half of the interception wire protocol. It //! listens on a loopback TCP port, accepts one connection per reported //! execution, and reads each record using the shared //! [`intercept::tcp::ExecutionWireSerializer`] wire format that the reporter //! (`intercept::tcp::ReporterOnTcp`) writes. use intercept::Execution; use intercept::reporter::ReporterError; use intercept::tcp::ExecutionWireSerializer; use std::net::{SocketAddr, TcpListener, TcpStream}; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; /// Represents a TCP execution collector. pub struct CollectorOnTcp { shutdown: Arc, listener: TcpListener, } impl CollectorOnTcp { /// Creates a new TCP execution collector. /// /// The collector listens to a random port on the loopback interface. /// The address of the collector can be obtained by the `address` method. pub fn new() -> Result<(Self, SocketAddr), std::io::Error> { let shutdown = Arc::new(AtomicBool::new(false)); // Try IPv4 loopback first, fall back to IPv6 loopback if IPv4 is unavailable. let listener = TcpListener::bind("127.0.0.1:0").or_else(|_| TcpListener::bind("[::1]:0"))?; let address = listener.local_addr()?; Ok((Self { shutdown, listener }, address)) } /// Single-threaded implementation of the collector. /// /// The collector listens to the TCP port and accepts incoming connections. /// When a connection is accepted, the collector reads the execution from /// the connection and emits it. /// /// # Graceful shutdown /// /// Reporters open a fresh connection per execution and write the whole /// length-prefixed record synchronously before the intercepted process /// execs. By the time a build finishes, every such report has completed /// its TCP handshake and is sitting in the listener's accept backlog. /// /// When `shutdown` is requested, the backlog may therefore still hold /// legitimate, fully-written reports. To avoid losing them, once the /// shutdown flag is observed the iterator switches the listener to /// non-blocking mode and drains every queued connection (reading each /// real record) until the backlog is empty (`WouldBlock`), and only then /// returns `None`. The drain always terminates because the backlog is /// finite and no new connections arrive after the build has exited. /// /// The throwaway "wake" connection opened by `shutdown` writes no bytes, /// so its first read yields end-of-file; it is recognised and skipped /// rather than being reported as an error. pub fn executions(&self) -> impl Iterator> + '_ { let listener = &self.listener; let shutdown = &self.shutdown; // Once true, we are draining the backlog with non-blocking accepts. let mut draining = false; std::iter::from_fn(move || { loop { if !draining && shutdown.load(Ordering::Acquire) { // Enter drain mode: stop blocking on accept so the loop is // guaranteed to terminate, and pick up any connections the // reporters already queued before shutdown was requested. if listener.set_nonblocking(true).is_err() { return None; } draining = true; } match listener.accept() { Ok((mut connection, _)) => { // Read the record before closing. A connection that // does not deliver a complete record (the wake // connection from `shutdown`, or any aborted report) is // skipped rather than surfaced as an error. let execution = Self::read_connection(&mut connection); let _ = connection.shutdown(std::net::Shutdown::Both); match execution { Some(execution) => return Some(Ok(execution)), None => continue, } } Err(err) if draining && err.kind() == std::io::ErrorKind::WouldBlock => { // Backlog fully drained: nothing left to accept. return None; } Err(err) => return Some(Err(ReporterError::Network(err))), } } }) } /// Reads a single execution from an accepted connection, if the peer sent /// a complete record. /// /// Returns `None` when the connection does not yield a usable record. The /// empty wake connection opened by `shutdown` closes without sending any /// bytes - a clean EOF on Linux, but a reset or other platform-specific /// error on macOS/BSD - and an aborted or truncated report fails partway /// through. None of these are data events; reporting failures are /// non-fatal by contract, so such connections are skipped (logged at /// debug) rather than surfaced as collector errors. Keying on "did the /// peer deliver a complete record" instead of on a specific I/O error kind /// keeps the behavior identical across platforms. fn read_connection(connection: &mut TcpStream) -> Option { match ExecutionWireSerializer::read(connection) { Ok(execution) => Some(execution), Err(err) => { log::debug!("Skipping connection without a complete record: {err}"); None } } } /// Stops the collector by flipping the shutdown flag and connecting to the collector. /// /// The collector is stopped when the `executions` iterator sees the shutdown /// flag. To signal the collector to stop, we connect to it to unblock the /// blocking `accept` call so it can observe the flag and drain the backlog. pub fn shutdown(&self) -> Result<(), ReporterError> { self.shutdown.store(true, Ordering::Release); let address = self.listener.local_addr()?; let _ = TcpStream::connect(address).map_err(ReporterError::Network)?; Ok(()) } } #[cfg(test)] mod tests { use super::*; use intercept::reporter::Reporter; use intercept::tcp::ReporterOnTcp; use std::collections::HashMap; fn executions() -> Vec { vec![ Execution::from_strings("/usr/bin/ls", vec!["ls", "-l"], "/tmp", HashMap::new()), Execution::from_strings( "/usr/bin/cc", vec!["cc", "-c", "./file_a.c", "-o", "./file_a.o"], "/home/user", HashMap::from([("PATH", "/usr/bin:/bin"), ("CC", "gcc")]), ), Execution::from_strings( "/usr/bin/ld", vec!["ld", "-o", "./file_a", "./file_a.o"], "/opt/project", HashMap::from([("PATH", "/usr/bin:/bin"), ("LD_PRELOAD", "/usr/lib:/lib")]), ), ] } // Test that the TCP reporter and the TCP collector work together. // We create a TCP collector and a TCP reporter, then we send executions // to the reporter and check if the collector receives them. // // A channel is used so the collector thread can signal that all // executions have been received, and a timeout prevents the test from // hanging indefinitely if delivery is broken. #[test] fn tcp_reporter_and_collectors_work() { let expected = executions(); let (collector, address) = CollectorOnTcp::new().unwrap(); let collector_arc = Arc::new(collector); // Channel for the collector to signal "I've received everything" let (done_tx, done_rx) = std::sync::mpsc::sync_channel::<()>(0); // Start the collector in a separate thread using the executions iterator let collector_thread = { let tcp_collector = Arc::clone(&collector_arc); let count = expected.len(); std::thread::spawn(move || { let mut received = Vec::new(); for result in tcp_collector.executions() { match result { Ok(execution) => { received.push(execution); if received.len() == count { let _ = done_tx.send(()); break; } } Err(err) => { log::error!("cannot receive an intercepted command: {err}"); break; } } } received }) }; // Send executions to the reporter. for execution in expected.iter() { let reporter = ReporterOnTcp::new(address); let result = reporter.report(execution.clone()); assert!(result.is_ok()); } // Wait with a timeout — if delivery is broken, fail instead of hang. done_rx .recv_timeout(std::time::Duration::from_secs(5)) .expect("timed out waiting for collector to receive all executions"); // Now safe to shutdown and join. collector_arc.shutdown().unwrap(); let received = collector_thread.join().unwrap(); // Assert that we received all the executions. assert_eq!(expected.len(), received.len()); for execution in received { assert!(expected.contains(&execution)); } } // Regression test for issue #704: reports already queued in the accept // backlog must not be dropped when shutdown is requested. // // This reproduces the shutdown race deterministically without relying on // arch-specific timing: all reports are sent and fully flushed *before* // `shutdown` is called and *before* the collector starts accepting. The // reports therefore sit in the listener's accept backlog at the moment // shutdown flips the flag, exactly as the last compiler's report does at // the end of a real build. A correct collector drains the backlog and // yields every queued execution; the pre-fix collector discarded the // backlog and lost them. // // Requirements: interception-preload-mechanism #[test] fn tcp_collector_drains_backlog_on_shutdown() { let expected = executions(); let (collector, address) = CollectorOnTcp::new().unwrap(); let collector_arc = Arc::new(collector); // Send and fully flush every report up front. Because the reporter // opens a connection, writes the whole record, and closes, each report // ends up queued in the accept backlog before we ever accept. for execution in expected.iter() { let reporter = ReporterOnTcp::new(address); reporter.report(execution.clone()).expect("report should succeed"); } // Request shutdown while every report is still sitting in the backlog. collector_arc.shutdown().unwrap(); // Drain on a worker thread guarded by a timeout so a regression that // hangs (instead of dropping) is reported as a failure, not a hang. let (done_tx, done_rx) = std::sync::mpsc::sync_channel::>(0); let drain_thread = { let tcp_collector = Arc::clone(&collector_arc); std::thread::spawn(move || { let received: Vec = tcp_collector.executions().filter_map(Result::ok).collect(); let _ = done_tx.send(received); }) }; let received = done_rx .recv_timeout(std::time::Duration::from_secs(5)) .expect("timed out draining the shutdown backlog"); drain_thread.join().unwrap(); // Every queued report must survive shutdown. assert_eq!(expected.len(), received.len()); for execution in expected.iter() { assert!(received.contains(execution)); } } // Test that calling shutdown on the collector stops the executions // iterator. No data is sent — this purely tests the shutdown mechanism. #[test] fn tcp_collector_shutdown_stops_iterator() { let (collector, _address) = CollectorOnTcp::new().unwrap(); let collector_arc = Arc::new(collector); let collector_thread = { let tcp_collector = Arc::clone(&collector_arc); std::thread::spawn(move || tcp_collector.executions().count()) }; collector_arc.shutdown().unwrap(); let count = collector_thread.join().unwrap(); assert_eq!(count, 0); } } rizsotto-Bear-0c30b27/crates/intercept-supervisor/src/context.rs000066400000000000000000000260521527177720300251360ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later use intercept::environment; use intercept::environment::KEY_OS_PATH; use std::collections::HashMap; use std::env; use std::fmt; use std::path::PathBuf; /// Errors that can occur when capturing the application context. #[derive(Debug, thiserror::Error)] pub enum ContextError { #[error("cannot get the path of the running executable: {0}")] CurrentExecutable(std::io::Error), #[error("cannot get the current working directory: {0}")] CurrentDirectory(std::io::Error), } /// Application context containing runtime environment information. /// /// This struct captures all the environmental context needed by Bear at startup, /// including system information, environment variables, and file system state. /// This allows for pure validation and configuration phases without additional /// I/O operations. #[derive(Debug, Clone)] pub struct Context { /// Path to the current Bear executable pub current_executable: PathBuf, /// Current working directory when Bear was invoked pub current_directory: PathBuf, /// All environment variables at startup pub environment: HashMap, /// Whether preload-based interception is supported on this system pub preload_supported: bool, /// Default executable search path from `confstr(_CS_PATH)`. /// Used as a fallback when a process's environment lacks `PATH`. pub confstr_path: String, } impl Context { /// Capture the current application context. /// /// This function performs I/O operations to gather system state and should /// be called early in the application lifecycle, before any validation phase. pub fn capture() -> Result { let current_executable = env::current_exe().map_err(ContextError::CurrentExecutable)?; let current_directory = env::current_dir().map_err(ContextError::CurrentDirectory)?; let environment = env::vars().collect::>(); let preload_supported = is_preload_supported(); let confstr_path = get_confstr_cs_path(); Ok(Context { current_executable, current_directory, environment, preload_supported, confstr_path }) } /// Returns the PATH environment variable key and value. /// /// This is relevant for Windows where the PATH is not capitalized and the lookup /// should be case insensitive. pub fn path(&self) -> Option<(String, String)> { self.environment .iter() .find(|(key, _)| key.to_uppercase() == KEY_OS_PATH) .map(|(key, value)| (key.clone(), value.clone())) } /// Parses the PATH environment variable from context into a vector of directories. pub fn paths(&self) -> Vec { self.path().map(|(_, path)| std::env::split_paths(&path).collect()).unwrap_or_default() } } impl fmt::Display for Context { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "Application Context:")?; writeln!(f, " Current Executable: {}", self.current_executable.display())?; writeln!(f, " Current Directory: {}", self.current_directory.display())?; writeln!(f, " Preload Supported: {}", self.preload_supported)?; writeln!(f, " Total Environment Variables: {} entries", self.environment.len())?; // Display relevant environment variables by iterating directly writeln!(f, " Relevant Environment Variables:")?; for (key, value) in &self.environment { if environment::relevant_env(key) { writeln!(f, " {}={}", key, value)?; } } Ok(()) } } /// Check if preload-based interception is supported on the current platform. /// /// Returns false if: /// - Platform doesn't support LD_PRELOAD (e.g., Windows) /// - macOS with System Integrity Protection (SIP) enabled /// - Other platform-specific restrictions fn is_preload_supported() -> bool { #[cfg(windows)] { // Windows doesn't support LD_PRELOAD false } #[cfg(all(target_os = "macos", not(windows)))] { // On macOS, check for System Integrity Protection (SIP) !is_sip_enabled() } #[cfg(all(not(target_os = "macos"), not(windows)))] { // Other Unix-like systems should support preload true } } /// Check if System Integrity Protection (SIP) is enabled on macOS. /// /// SIP prevents LD_PRELOAD from working with system binaries, which can cause /// library-based interposition to fail silently. #[cfg(target_os = "macos")] fn is_sip_enabled() -> bool { use std::process::Command; match Command::new("csrutil").arg("status").output() { Ok(output) => { let stdout = String::from_utf8_lossy(&output.stdout); stdout .lines() .any(|line| line.contains("System Integrity Protection status:") && line.contains("enabled")) } Err(_) => { // If we can't run csrutil, assume SIP is disabled // This is a conservative approach - better to try preload and fail // than to unnecessarily force wrapper mode false } } } /// Returns the default search path from `confstr(_CS_PATH)`. /// /// On Unix systems, this queries the system configuration for the standard /// utility search path. Falls back to `/usr/bin:/bin` if unavailable. /// Android (bionic) has no `confstr`, so it takes the fallback. #[cfg(all(unix, not(target_os = "android")))] fn get_confstr_cs_path() -> String { use std::ffi::CStr; // SAFETY: confstr with a null buffer and length 0 is allowed; it only // returns the required size. let len = unsafe { libc::confstr(libc::_CS_PATH, std::ptr::null_mut(), 0) }; if len == 0 { return "/usr/bin:/bin".to_string(); } let mut buf = vec![0u8; len]; // SAFETY: buf is a live Vec of exactly len bytes, and confstr writes at // most len bytes (including the NUL) into it. let result = unsafe { libc::confstr(libc::_CS_PATH, buf.as_mut_ptr() as *mut libc::c_char, len) }; if result == 0 { return "/usr/bin:/bin".to_string(); } CStr::from_bytes_until_nul(&buf) .map(|s| s.to_string_lossy().into_owned()) .unwrap_or_else(|_| "/usr/bin:/bin".to_string()) } #[cfg(any(not(unix), target_os = "android"))] fn get_confstr_cs_path() -> String { "/usr/bin:/bin".to_string() } #[cfg(test)] mod tests { use super::*; #[test] fn test_context_capture() { let context = Context::capture(); assert!(context.is_ok()); let ctx = context.unwrap(); // Basic assertions that should always be true assert!(ctx.current_directory.is_absolute()); assert!(ctx.current_executable.is_absolute()); } #[test] fn test_display_format() { let context = Context::capture().unwrap(); let display_output = format!("{}", context); assert!(display_output.contains("Application Context:")); assert!(display_output.contains("Current Directory:")); assert!(display_output.contains("Current Executable:")); assert!(display_output.contains("Relevant Environment Variables:")); assert!(display_output.contains("Total Environment Variables:")); } #[test] fn test_display_includes_relevant_env_vars() { use std::collections::HashMap; // Create a test context with known environment variables let mut test_env = HashMap::new(); test_env.insert("PATH".to_string(), "/usr/bin:/bin".to_string()); test_env.insert("CC".to_string(), "gcc".to_string()); test_env.insert("IRRELEVANT_VAR".to_string(), "value".to_string()); test_env.insert("CPATH".to_string(), "/opt/include".to_string()); let context = Context { current_executable: std::env::current_exe().unwrap(), current_directory: std::env::current_dir().unwrap(), environment: test_env, preload_supported: is_preload_supported(), confstr_path: get_confstr_cs_path(), }; let display_output = format!("{}", context); // Should include relevant variables assert!(display_output.contains("PATH=/usr/bin:/bin")); assert!(display_output.contains("CC=gcc")); assert!(display_output.contains("CPATH=/opt/include")); // Should not include irrelevant variables assert!(!display_output.contains("IRRELEVANT_VAR=value")); // Should show relevant env vars section assert!(display_output.contains("Relevant Environment Variables:")); assert!(display_output.contains("Total Environment Variables: 4 entries")); } #[test] fn test_display_no_relevant_env_vars() { use std::collections::HashMap; // Create a test context with no relevant environment variables let mut test_env = HashMap::new(); test_env.insert("IRRELEVANT_VAR1".to_string(), "value1".to_string()); test_env.insert("IRRELEVANT_VAR2".to_string(), "value2".to_string()); let context = Context { current_executable: std::env::current_exe().unwrap(), current_directory: std::env::current_dir().unwrap(), environment: test_env, preload_supported: is_preload_supported(), confstr_path: get_confstr_cs_path(), }; let display_output = format!("{}", context); // Should show that there are no relevant variables assert!(display_output.contains("Relevant Environment Variables:")); assert!(display_output.contains("Total Environment Variables: 2 entries")); } #[test] fn test_preload_supported_field() { let context = Context::capture().unwrap(); // On Windows, preload should not be supported #[cfg(windows)] assert!(!context.preload_supported); // On non-Windows platforms, this depends on the actual system state #[cfg(not(windows))] { // Just verify the field exists and has a boolean value let _ = context.preload_supported; } } #[test] fn test_context_error_current_executable() { let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "not found"); let error = ContextError::CurrentExecutable(io_error); assert!(matches!(error, ContextError::CurrentExecutable(_))); assert!(error.to_string().contains("cannot get the path of the running executable")); assert!(error.to_string().contains("not found")); } #[test] fn test_context_error_current_directory() { let io_error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "permission denied"); let error = ContextError::CurrentDirectory(io_error); assert!(matches!(error, ContextError::CurrentDirectory(_))); assert!(error.to_string().contains("cannot get the current working directory")); assert!(error.to_string().contains("permission denied")); } #[cfg(target_os = "macos")] #[test] fn test_sip_detection() { // Test that SIP detection doesn't panic let _sip_enabled = is_sip_enabled(); } } rizsotto-Bear-0c30b27/crates/intercept-supervisor/src/installation.rs000066400000000000000000000165601527177720300261560ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! # Installation Layout //! //! This module encodes the expected installation layout of Bear's artifacts. //! //! ## Layout //! //! Bear is installed with the following directory structure: //! //! ```text //! / //! ├── bin/ //! │ └── bear ← shell script, calls bear-driver by absolute path //! └── / //! ├── bin/ //! │ ├── bear-driver ← current_executable //! │ └── bear-wrapper ← sibling of bear-driver //! └── / //! └── libexec.so ← one level up from bin/, then into INTERCEPT_LIBDIR/ //! ``` //! //! `bear-driver` locates its siblings using **relative paths only**: //! - `bear-wrapper` is a sibling in the same `bin/` directory. //! - `libexec.so` is reached via `..//libexec.so` relative to `bin/`. //! //! The `INTERCEPT_LIBDIR` value is set at build time (defaults to `lib`). On glibc-based //! Linux, packagers can set it to `$LIB` so the dynamic linker expands it at runtime. use std::fmt; use std::path::{Path, PathBuf}; use thiserror::Error; const WRAPPER_NAME: &str = env!("WRAPPER_NAME"); const PRELOAD_NAME: &str = env!("PRELOAD_NAME"); const INTERCEPT_LIBDIR: &str = env!("INTERCEPT_LIBDIR"); /// Errors that can occur when constructing an [`InstallationLayout`]. #[derive(Debug, Error)] pub enum LayoutError { #[error("executable path is not absolute: {}", .0.display())] NotAbsolute(PathBuf), #[error("executable path has no parent directory: {}", .0.display())] NoParent(PathBuf), } /// Represents the installation layout of Bear's artifacts. /// /// This struct is constructed from the path of the `bear-driver` executable /// and derives all other artifact locations using relative paths, matching /// the layout described in `INSTALL.md`. #[derive(Debug, Clone)] pub struct InstallationLayout { /// The directory that contains the `bear-driver` executable. /// `wrapper_path` is derived relative to this directory. bin_dir: PathBuf, /// The directory that contains the `libexec.so` file. lib_dir: PathBuf, } impl InstallationLayout { /// Returns the path to the `bear-wrapper` executable. /// /// `bear-wrapper` is a sibling of `bear-driver` in the same `bin/` directory. pub fn wrapper_path(&self) -> PathBuf { self.bin_dir.join(WRAPPER_NAME) } /// Returns the path to the preload shared library (`libexec.so` / `libexec.dylib`). /// /// The library lives one directory level above `bin/`, inside the `INTERCEPT_LIBDIR` /// subdirectory (set at build time, defaults to `lib`). pub fn preload_path(&self) -> PathBuf { self.lib_dir.join(PRELOAD_NAME) } } impl fmt::Display for InstallationLayout { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "Installation Layout:")?; writeln!(f, " Wrapper Executable: {}", self.wrapper_path().display())?; writeln!(f, " Preload Library: {}", self.preload_path().display())?; Ok(()) } } impl TryFrom<&Path> for InstallationLayout { type Error = LayoutError; /// Construct an [`InstallationLayout`] from the absolute path of the /// `bear-driver` executable. /// /// # Errors /// /// - [`LayoutError::NotAbsolute`] if `executable` is not an absolute path. /// - [`LayoutError::NoParent`] if `executable` has no usable parent directory /// (bare filename with no directory component, or sitting at the filesystem root). fn try_from(executable: &Path) -> Result { if !executable.is_absolute() { return Err(LayoutError::NotAbsolute(executable.to_path_buf())); } let bin_dir = executable.parent().ok_or_else(|| LayoutError::NoParent(executable.to_path_buf()))?.to_path_buf(); let parent_dir = bin_dir.parent().ok_or_else(|| LayoutError::NoParent(executable.to_path_buf()))?.to_path_buf(); Ok(Self { bin_dir, lib_dir: parent_dir.join(INTERCEPT_LIBDIR) }) } } #[cfg(test)] mod tests { use super::*; const DRIVER_NAME: &str = env!("DRIVER_NAME"); /// Build an absolute path from components, portable across platforms. /// /// On Unix the root is `/`, on Windows it is `C:\`. fn abs_path(components: &[&str]) -> PathBuf { let mut p = if cfg!(windows) { PathBuf::from("C:\\") } else { PathBuf::from("/") }; for c in components { p.push(c); } p } #[test] fn test_layout_from_typical_install_path() { let exe = abs_path(&["usr", "local", "share", "bear", "bin", DRIVER_NAME]); let layout = InstallationLayout::try_from(exe.as_path()).unwrap(); assert_eq!(layout.wrapper_path(), abs_path(&["usr", "local", "share", "bear", "bin", WRAPPER_NAME])); assert_eq!( layout.preload_path(), abs_path(&["usr", "local", "share", "bear", INTERCEPT_LIBDIR, PRELOAD_NAME]) ); } #[test] fn test_layout_from_minimal_path() { // Minimal valid absolute path: executable directly in a root-level directory. let exe = abs_path(&["bin", DRIVER_NAME]); let layout = InstallationLayout::try_from(exe.as_path()).unwrap(); assert_eq!(layout.wrapper_path(), abs_path(&["bin", WRAPPER_NAME])); assert_eq!(layout.preload_path(), abs_path(&[INTERCEPT_LIBDIR, PRELOAD_NAME])); } #[test] fn test_layout_rom_bare_executable_name_fails() { // A bare filename is not absolute — must be rejected. let bare = Path::new(DRIVER_NAME); let err = InstallationLayout::try_from(bare).unwrap_err(); assert!(matches!(err, LayoutError::NotAbsolute(_))); } #[test] fn test_layout_from_relative_path_fails() { let relative = PathBuf::from("bin").join(DRIVER_NAME); let err = InstallationLayout::try_from(relative.as_path()).unwrap_err(); assert!(matches!(err, LayoutError::NotAbsolute(_))); } #[test] fn test_layout_from_root_executable_fails() { // An executable sitting directly at the filesystem root has no usable parent. let root_exe = abs_path(&[DRIVER_NAME]); let err = InstallationLayout::try_from(root_exe.as_path()).unwrap_err(); assert!(matches!(err, LayoutError::NoParent(_))); } #[test] fn test_display_contains_expected_sections() { let exe = abs_path(&["usr", "local", "share", "bear", "bin", DRIVER_NAME]); let layout = InstallationLayout::try_from(exe.as_path()).unwrap(); let output = format!("{layout}"); assert!(output.contains("Installation Layout:")); assert!(output.contains("Wrapper Executable:")); assert!(output.contains("Preload Library:")); } #[test] fn test_display_contains_correct_paths() { let exe = abs_path(&["usr", "local", "share", "bear", "bin", DRIVER_NAME]); let layout = InstallationLayout::try_from(exe.as_path()).unwrap(); let output = format!("{layout}"); let expected_wrapper = abs_path(&["usr", "local", "share", "bear", "bin", WRAPPER_NAME]); let expected_preload = abs_path(&["usr", "local", "share", "bear", INTERCEPT_LIBDIR, PRELOAD_NAME]); assert!(output.contains(&expected_wrapper.display().to_string())); assert!(output.contains(&expected_preload.display().to_string())); } } rizsotto-Bear-0c30b27/crates/intercept-supervisor/src/lib.rs000066400000000000000000000022351527177720300242150ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! The driver-side (supervisor) half of the interception runtime. //! //! This crate owns the consumer side of the mechanism: it supervises the build //! process, collects the executions reported over TCP, and sets up the build //! environment that injects the interception (wrapper directory or preload //! library). It depends on `intercept` for the shared `Execution` type and the //! wire serialization, but never on `bear`, `config`, or `clap`, so the preload //! cdylib - which depends on `intercept` only - never compiles supervisor-only //! dependencies (`signal-hook`, `which`). pub mod collector; pub mod context; pub mod installation; pub mod runner; pub mod supervise; pub mod wrapper; pub use collector::CollectorOnTcp; pub use context::{Context, ContextError}; pub use installation::{InstallationLayout, LayoutError}; pub use runner::{BuildEnvironment, ConfigurationError}; pub use supervise::{GroupPolicy, SuperviseError, supervise, supervise_execution}; pub use wrapper::{ CONFIG_FILENAME, ConfigError, WrapperConfig, WrapperConfigReader, WrapperDirectory, WrapperDirectoryBuilder, WrapperDirectoryError, }; rizsotto-Bear-0c30b27/crates/intercept-supervisor/src/runner.rs000066400000000000000000001723531527177720300247710ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Driver-side build environment setup for command interception. //! //! This module builds the environment overrides that inject Bear's //! interception (a wrapper directory placed on `PATH`, or a preload library) //! and runs the build command under supervision. It works in terms of //! primitives only (`context`, `&[PathBuf]`, `SocketAddr`, an //! `impl Fn(&Path) -> bool` predicate), so it carries no `config`, `args`, or //! `clap` dependency. The thin `config`-to-primitive adapter lives in `bear`. use crate::context; use crate::installation::InstallationLayout; use crate::supervise; use crate::wrapper::{WrapperDirectory, WrapperDirectoryBuilder, WrapperDirectoryError}; #[cfg(not(target_os = "macos"))] use intercept::environment::KEY_OS_PRELOAD_PATH; use intercept::environment::{KEY_INTERCEPT_STATE, KEY_OS_PATH, insert_to_path}; #[cfg(target_os = "macos")] use intercept::environment::{KEY_OS_MACOS_FLAT_NAMESPACE, KEY_OS_MACOS_PRELOAD_PATH}; use intercept::state::PreloadState; use std::collections::HashMap; use std::env::JoinPathsError; #[cfg(windows)] use std::env::consts::EXE_EXTENSION; use std::net::SocketAddr; use std::path::{Path, PathBuf}; use std::process::ExitStatus; use thiserror::Error; /// Manages the environment setup for intercepting build commands during compilation. /// /// `BuildEnvironment` is responsible for configuring the execution environment to enable /// Bear's command interception capabilities. It supports two main interception modes: /// - **Wrapper mode**: Creates a new directory with copies of wrapper executables that /// can intercept compiler executions, and manipulates the environment variables so /// that these executables have precedence. /// - **Preload mode**: Changes the environment variables to inject a dynamic library /// for system call interception. pub struct BuildEnvironment { environment_overrides: HashMap, _wrapper_directory: Option, } impl BuildEnvironment { /// Creates a `BuildEnvironment` configured for wrapper mode interception. /// /// The wrapper mode is more complicated than preload mode. We create a `.bear/` directory /// in the current working directory and insert copies of the wrapper executable. To decide /// how to name the executables in the directory, we consider the config file, but also /// the current environment variables. Once these are set up, we alter the `PATH` environment /// variable to ensure the wrappers are executed instead of the real compilers. To instruct /// the wrappers which executables to call, we also create a JSON file. /// /// The wrapper mode reads the current environment and looks for variables that might /// instruct the build process about the compiler locations. Good candidates for such /// variables are: `CC`, `CXX`, `LD`, `CPP`, etc. /// /// An executed wrapper reports the execution and executes the intended process. To /// ensure that the wrapper calls the right process, it reads the JSON file from the /// wrapper directory, which contains a map of executable names and paths. Using the /// arguments (which contain the process name) it looks up the real executable. /// /// Because this process does not execute the compilers, but the build process does, /// we need to alter the build process to use the wrapper executables. This is achieved /// by changing the `PATH` variable. But that has limitations; we need to redefine /// some of the environment variables mentioned above (`CC`, `CXX`, `LD`, `CPP`, etc.), /// allowing the user to use these variables in the build invocations. /// /// The use of a deterministic `.bear/` directory (instead of a random temp directory) /// is essential for autotools-style builds where `./configure` caches compiler paths. /// With a random directory, subsequent `make` invocations would fail because the /// cached paths would no longer exist. /// /// # Arguments /// /// * `path` - Path to the wrapper executable /// * `executables` - List of executables to create wrappers for /// * `address` - Socket address for interceptor communication /// /// # Returns /// /// Returns a configured `BuildEnvironment` on success, or a `ConfigurationError` /// if validation fails or wrapper setup encounters errors. pub fn create_as_wrapper( context: &context::Context, executables: &[std::path::PathBuf], address: SocketAddr, is_compiler: impl Fn(&Path) -> bool, ) -> Result { let layout = InstallationLayout::try_from(context.current_executable.as_path()) .map_err(ConfigurationError::InstallationLayout)?; let wrapper_executable = layout.wrapper_path(); // Create wrapper directory builder (creates .bear/ in context's working directory) let mut wrapper_dir_builder = WrapperDirectoryBuilder::create( wrapper_executable.as_path(), &context.current_directory, address, )?; // Register executables from config for executable in executables { wrapper_dir_builder.register_executable(executable.clone())?; } // Register executables from environment variables that point to compiler programs // and immediately update the final environment with wrapper paths. Values may // include trailing flags (e.g. CC="gcc -std=c11"); those are preserved in the // rewritten override so the build still receives them. let mut environment_overrides = HashMap::new(); for (key, value) in &context.environment { if !intercept::environment::program_env(key) { continue; } let Some((program, flags)) = parse_program_env_value(value) else { log::warn!("Skipping compiler env var {}={:?}: could not parse value", key, value); continue; }; let Some(program_path) = Self::resolve_program_path(context, &program) else { log::warn!( "Skipping compiler env var {}={:?}: could not resolve {:?} to an executable on PATH", key, value, program, ); continue; }; let wrapper_path = wrapper_dir_builder.register_executable(program_path)?; let mut override_value = wrapper_path.to_string_lossy().into_owned(); // Plain space-join, never shell-quoted. Build systems consume // compiler env vars by either (a) unquoted variable expansion in // the shell (`$CC -c foo.c`), where POSIX quote removal does not // apply to the expanded text, or (b) GNU Make recipes passed to // `sh -c`, where the literal string is parsed fresh. In both // paths, introducing shell quoting on our side yields wrong // argv: (a) leaks literal quotes into the argv, (b) adds a layer // of quoting the original value did not carry. The user already // committed to whitespace-separated tokens when they set the // env var; Bear does not add or remove that promise. for flag in flags { override_value.push(' '); override_value.push_str(&flag); } environment_overrides.insert(key.clone(), override_value); } // PATH-based discovery (only if no config executables) if executables.is_empty() { for candidate in Self::compiler_candidates(context, is_compiler) { wrapper_dir_builder.register_executable(candidate)?; } } // Finalize wrapper directory (writes config file) let wrapper_dir = wrapper_dir_builder.build()?; // Update PATH environment variable if let Some((path_key, path_value)) = context.path() { let path_updated = insert_to_path(&path_value, wrapper_dir.path()).map_err(ConfigurationError::Path)?; environment_overrides.insert(path_key, path_updated); } else { environment_overrides .insert(KEY_OS_PATH.to_string(), wrapper_dir.path().to_string_lossy().to_string()); } Ok(Self { environment_overrides, _wrapper_directory: Some(wrapper_dir) }) } /// Discovers compiler executables in PATH directories using a predicate function. /// /// This function scans all directories in the PATH environment variable and applies /// the provided predicate to each executable file found. Executables that match /// the predicate are returned. Masquerade wrappers (ccache, distcc, ...) are /// filtered out so that the registered compiler is always the real one; the /// iteration continues past them to find the next candidate on PATH. fn compiler_candidates

(context: &context::Context, predicate: P) -> impl Iterator where P: Fn(&Path) -> bool, { context .paths() .into_iter() .filter(|dir| dir.exists()) .flat_map(|dir| match std::fs::read_dir(dir) { Ok(entries) => entries .filter_map(|entry| match entry { Ok(e) => Some(e.path()), Err(_) => None, }) .collect::>() .into_iter(), Err(e) => { log::debug!("Failed to read directory: {}", e); Vec::new().into_iter() } }) .filter(move |path| is_executable_file(path) && !is_masquerade_wrapper(path) && predicate(path)) } /// Resolves a program env var value to an absolute executable path. /// /// Handles three cases: /// - Absolute path: returned as-is (not canonicalized); if the path is a /// masquerade wrapper, falls back to resolving the basename via PATH. /// - Relative path (contains directory component): joined with cwd; same /// masquerade fallback applies. /// - Bare name: resolved via PATH, skipping masquerade wrapper /// directories (ccache, distcc, icecc, colorgcc, buildcache). /// /// Returns `None` if the program cannot be found or every resolution /// lands on a masquerade wrapper with no real compiler past it. fn resolve_program_path(context: &context::Context, value: &str) -> Option { let name = value.trim(); if name.is_empty() { return None; } let path = PathBuf::from(name); let search_path = context.path().map(|(_, p)| p).unwrap_or_else(|| context.confstr_path.clone()); // Absolute path, or relative path with a directory component. We do // not canonicalize here because that resolves symlinks and can change // the filename (e.g. /usr/bin/gcc -> /usr/bin/x86_64-linux-gnu-gcc-13), // breaking wrapper name matching. If the supplied path IS a masquerade // wrapper, fall back to resolving the basename via PATH so we do not // register a wrapper that loops (see interception-wrapper-recursion). let supplied: Option = if path.is_absolute() { Some(path) } else if path.parent().is_some_and(|p| !p.as_os_str().is_empty()) { Some(context.current_directory.join(&path)) } else { None }; if let Some(candidate) = supplied { if !is_masquerade_wrapper(&candidate) { return Some(candidate); } let basename = candidate.file_name().and_then(|n| n.to_str())?; log::info!( "resolve: {} is a masquerade wrapper; retrying basename '{}' via PATH", candidate.display(), basename, ); return resolve_past_masquerade_wrappers(basename, &search_path, &context.current_directory); } // Bare name: resolve via PATH, skipping masquerade wrappers. resolve_past_masquerade_wrappers(name, &search_path, &context.current_directory) } /// Creates a `BuildEnvironment` configured for preload mode interception. /// /// The preload mode requires altering one of the environment variables that the OS /// dynamic linker reads and loads the shared library into the memory of the executed /// process. This will result in all dynamically linked executables reporting back, but /// post processing will filter the relevant compiler executions. /// /// The interceptor will report executions via TCP sockets to the specified address, /// which is advertised via a specific environment variable. /// /// # Arguments /// /// * `path` - Path to the preload library /// * `address` - Socket address for interceptor communication /// /// # Returns /// /// Returns a configured `BuildEnvironment` on success, or a `ConfigurationError` /// if validation fails or the preload library path is invalid. pub fn create_as_preload( context: &context::Context, address: SocketAddr, ) -> Result { // Check if preload is supported on this system if !context.preload_supported { return Err(ConfigurationError::UnsupportedInterceptMode( "Preload-based interception is not supported on this system. \ This may be due to platform restrictions (e.g., Windows) or \ security features (e.g., macOS System Integrity Protection). \ Consider using wrapper mode instead.", )); } let layout = InstallationLayout::try_from(context.current_executable.as_path()) .map_err(ConfigurationError::InstallationLayout)?; let library = layout.preload_path(); let mut environment_overrides = HashMap::new(); // Platform-specific preload configuration #[cfg(target_os = "macos")] { // macOS uses DYLD_INSERT_LIBRARIES and DYLD_FORCE_FLAT_NAMESPACE let preload_original = context.environment.get(KEY_OS_MACOS_PRELOAD_PATH).cloned().unwrap_or_default(); let preload_updated = insert_to_path(&preload_original, library.as_path()).map_err(ConfigurationError::Path)?; environment_overrides.insert(KEY_OS_MACOS_PRELOAD_PATH.to_string(), preload_updated); environment_overrides.insert(KEY_OS_MACOS_FLAT_NAMESPACE.to_string(), "1".to_string()); } #[cfg(not(target_os = "macos"))] { // Linux and other Unix-like systems use LD_PRELOAD let preload_original = context.environment.get(KEY_OS_PRELOAD_PATH).cloned().unwrap_or_default(); let preload_updated = insert_to_path(&preload_original, library.as_path()).map_err(ConfigurationError::Path)?; environment_overrides.insert(KEY_OS_PRELOAD_PATH.to_string(), preload_updated); } // Make the current state available as a single environment variable let state: String = PreloadState { destination: address, library } .try_into() .map_err(|_| ConfigurationError::PathNotFound)?; environment_overrides.insert(KEY_INTERCEPT_STATE.to_string(), state); Ok(Self { environment_overrides, _wrapper_directory: None }) } /// Executes a build command within the configured interception environment. /// /// This is the main entry point for running build commands with Bear's interception /// capabilities enabled. The method sets up the command with the proper environment /// and delegates execution to the supervision system for monitoring and control. /// /// # Arguments /// /// * `arguments` - The build command (executable followed by its arguments) to /// execute with interception enabled /// /// # Returns /// /// Returns `Ok(ExitStatus)` containing the exit status of the build command, /// or `Err(SuperviseError)` if the command execution fails or cannot be supervised. pub fn run_build(&self, arguments: &[String]) -> Result { log::info!("Build command to run: {arguments:?}"); let [executable, args @ ..] = arguments else { // Safe to presume that the build command was already checked. panic!("BuildCommand arguments cannot be empty"); }; let mut command = std::process::Command::new(executable); command.args(args); // Set only the environment variables we need to override for (key, value) in &self.environment_overrides { log::info!("Build command environment override: {key}={value}"); command.env(key, value); } supervise::supervise(&mut command, supervise::GroupPolicy::Leader) } } /// Error types that can occur during build environment configuration. /// /// This enum represents the various errors that can happen when setting up /// the build environment for command interception. Each variant provides /// specific context about what went wrong during the configuration process. #[derive(Error, Debug)] pub enum ConfigurationError { #[error("Invalid characters in path to join: {0}")] Path(#[from] JoinPathsError), #[error("Wrapper directory error: {0}")] WrapperDirectory(#[from] WrapperDirectoryError), #[error("Could not find PATH variable")] PathNotFound, #[error("Unsupported intercept mode: {0}")] UnsupportedInterceptMode(&'static str), #[error("Invalid installation layout: {0}")] InstallationLayout(#[from] crate::installation::LayoutError), } /// Splits a compiler env var value into `(program, flags)` on whitespace. /// Returns `None` for empty or whitespace-only values; the caller logs a /// warning and skips. Anything more elaborate than whitespace-separated /// tokens (shell quoting, metacharacters, command substitutions) belongs /// in `CFLAGS`/`CXXFLAGS` -- see `interception-cc-env-var-flags`. fn parse_program_env_value(value: &str) -> Option<(String, Vec)> { let mut tokens = value.split_whitespace().map(str::to_string); let program = tokens.next()?; Some((program, tokens.collect())) } /// Known masquerade wrappers. A directory full of symlinks named after /// compilers, where each symlink resolves to one of these binaries, is a /// masquerade directory; Bear skips such directories when resolving the real /// compiler. See `interception-wrapper-recursion`. const MASQUERADE_WRAPPERS: &[&str] = &["ccache", "distcc", "icecc", "colorgcc", "buildcache"]; /// Checks whether `path` is a symlink whose ultimate target's filename is one /// of the known masquerade wrappers. /// /// Short-circuits on non-symlinks so iterating every executable in a PATH /// directory stays cheap. For symlinks, uses `canonicalize` to follow the /// chain because only the basename of the final target is inspected; the /// canonicalised path itself is discarded. Callers must not use this helper /// when registering a compiler -- canonicalisation would change the name /// (e.g. `/usr/bin/gcc` -> `/usr/bin/gcc-13`) and break wrapper lookup. fn is_masquerade_wrapper(path: &Path) -> bool { let Ok(meta) = std::fs::symlink_metadata(path) else { return false }; if !meta.file_type().is_symlink() { return false; } let Ok(target) = std::fs::canonicalize(path) else { return false }; let Some(name) = target.file_name().and_then(|n| n.to_str()) else { return false }; let stem = name.strip_suffix(".exe").or_else(|| name.strip_suffix(".EXE")).unwrap_or(name); MASQUERADE_WRAPPERS.iter().any(|w| w.eq_ignore_ascii_case(stem)) } /// Resolves a bare program name via PATH, transparently skipping masquerade /// wrapper directories. If the first match on PATH is a masquerade wrapper /// (e.g. `/usr/lib64/ccache/gcc` -> `/usr/bin/ccache`), the containing /// directory is excluded and the search is retried. The process repeats until /// a non-masquerade compiler is found or PATH is exhausted. /// /// Returns `None` if no real compiler is reachable past the masquerade /// directories. In that case the caller must not register a wrapper; doing so /// would re-create the recursion the filtering is designed to prevent. fn resolve_past_masquerade_wrappers(name: &str, search_path: &str, cwd: &Path) -> Option { let mut excluded: Vec = Vec::new(); loop { let current = filter_out_paths(search_path, &excluded); let found = match which::which_in(name, Some(current.as_str()), cwd) { Ok(path) => path, Err(_) => { if !excluded.is_empty() { let dirs = excluded.iter().map(|p| p.display().to_string()).collect::>().join(", "); log::warn!("resolve: no real '{}' on PATH past masquerade dir(s): {}", name, dirs,); } return None; } }; if !is_masquerade_wrapper(&found) { return Some(found); } let parent = found.parent()?.to_path_buf(); if excluded.contains(&parent) { // Defensive: the excluded dir came back, which would loop forever. log::warn!( "resolve: masquerade dir {} already excluded but returned again for '{}'", parent.display(), name, ); return None; } log::info!( "resolve: masquerade wrapper at {}; re-resolving '{}' past {}", found.display(), name, parent.display(), ); excluded.push(parent); } } /// Joins a path-separated string, removing any entries that match one of the /// excluded paths. Trailing path separators are stripped on both sides before /// comparison so that a PATH entry written as `/usr/lib64/ccache/` still /// matches an excluded dir derived from `PathBuf::parent()` (which has no /// trailing slash). No further canonicalisation: matching is otherwise by /// byte-equal path. fn filter_out_paths(original: &str, excluded: &[PathBuf]) -> String { let excluded_normalised: Vec = excluded.iter().map(|p| normalise_path(p)).collect(); let kept: Vec = std::env::split_paths(original) .filter(|p| { let candidate = normalise_path(p); !excluded_normalised.iter().any(|e| e == &candidate) }) .collect(); std::env::join_paths(kept).map(|os| os.into_string().unwrap_or_default()).unwrap_or_default() } /// Strips trailing path separators from a path (except when the path is just /// a root, e.g. `/` on Unix or `C:\` on Windows). Returns the input unchanged /// for non-UTF-8 paths. fn normalise_path(path: &Path) -> PathBuf { let Some(s) = path.to_str() else { return path.to_path_buf() }; let trimmed = s.trim_end_matches(std::path::MAIN_SEPARATOR); if trimmed.is_empty() { path.to_path_buf() } else { PathBuf::from(trimmed) } } /// Checks if a path represents an executable file. fn is_executable_file(path: &Path) -> bool { if !path.is_file() { return false; } // Check if file is executable #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; if let Ok(metadata) = path.metadata() { metadata.permissions().mode() & 0o111 != 0 } else { false } } #[cfg(not(unix))] { // On Windows, assume .exe files are executable, others might be scripts path.extension().map_or(false, |ext| ext == EXE_EXTENSION) } } #[cfg(test)] mod test { use super::*; use std::path::PathBuf; /// Helper function to assert that the first entry in a path-like string equals the expected value. /// This can be used to assert PATH, LD_PRELOAD, or any other path-separated environment variable. fn assert_first_path_entry(expected: &str, path_like: &str) { let path_entries: Vec = std::env::split_paths(path_like).map(|p| p.to_string_lossy().to_string()).collect(); let first_entry = path_entries.first().expect("Path-like string should not be empty"); assert_eq!( first_entry, expected, "First path entry should match expected. First entry: {}, expected: {}", first_entry, expected ); } /// Helper function to get the expected wrapper directory path from a TempDir. fn get_wrapper_dir_path(temp_dir: &tempfile::TempDir) -> String { temp_dir.path().join(".bear").to_string_lossy().to_string() } mod fixture { //! Shared fixture builder for `BuildEnvironment` wrapper-mode tests. //! //! Each test needs roughly the same on-disk scaffolding: a //! `current_dir` for the `.bear/` directory, an `install_dir` //! containing `bin/` that `register_executable` //! copies/hard-links, and some number of fake compiler binaries //! on PATH or at known locations. Assembling that inline burns //! ~20 lines per test and buries the intent. The `Fixture` //! builder below owns the backing `TempDir`s, assembles the //! `Context`, and exposes just the knobs the tests care about. //! //! The fixture cannot mock the filesystem away: `create_as_wrapper` //! really creates a directory, `register_executable` really copies //! files, and `which::which_in` really stats PATH entries. The //! fixture minimises the ceremony, not the I/O. use super::*; use std::collections::HashMap; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::{Path, PathBuf}; use tempfile::TempDir; /// Standard test socket address. pub fn test_address() -> SocketAddr { SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080) } /// Platform-appropriate fake-compiler basename. pub fn compiler_basename() -> &'static str { if cfg!(windows) { "fake-cc.exe" } else { "fake-cc" } } /// Writes a file and marks it executable on Unix. pub fn write_executable(path: &Path) { std::fs::write(path, "#!/usr/bin/true").unwrap(); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).unwrap(); } } /// Builder-style fixture. Owns every `TempDir` it creates so /// paths handed out stay valid for the test's lifetime. Drop /// cleans everything up. pub struct Fixture { current_dir: TempDir, install_dir: TempDir, tool_dirs: Vec, environment: HashMap, preload_supported: bool, confstr_path: String, } impl Fixture { /// Fresh fixture: empty `current_dir`, `install_dir` with a /// fake wrapper binary under `bin/`, empty /// environment, `preload_supported=false`, `confstr_path=""`. pub fn new() -> Self { let install_dir = TempDir::new().unwrap(); let bin_dir = install_dir.path().join("bin"); std::fs::create_dir(&bin_dir).unwrap(); write_executable(&bin_dir.join(env!("WRAPPER_NAME"))); Self { current_dir: TempDir::new().unwrap(), install_dir, tool_dirs: Vec::new(), environment: HashMap::new(), preload_supported: false, confstr_path: String::new(), } } /// Overwrites `PATH` with the string produced by joining /// the tool_dirs registered via `add_compiler_on_path` / /// `add_compilers_on_path`. Callers normally do not need to /// invoke this directly. fn refresh_path(&mut self) { let joined = std::env::join_paths(self.tool_dirs.iter().map(|d| d.path().to_owned())) .expect("join PATH"); self.environment.insert(KEY_OS_PATH.to_string(), joined.into_string().expect("utf-8 PATH")); } /// Creates a new tool directory, writes a fake compiler /// named `name`, adds the directory to `PATH`, returns the /// compiler's absolute path. pub fn add_compiler_on_path(&mut self, name: &str) -> PathBuf { let paths = self.add_compilers_on_path(&[name]); paths.into_iter().next().unwrap() } /// Creates one tool directory, writes one fake binary per /// name, adds the directory to `PATH`, returns their paths /// in the order passed. pub fn add_compilers_on_path(&mut self, names: &[&str]) -> Vec { let dir = TempDir::new().unwrap(); let paths: Vec = names .iter() .map(|n| { let p = dir.path().join(n); write_executable(&p); p }) .collect(); self.tool_dirs.push(dir); self.refresh_path(); paths } /// Creates a fake compiler in a new tool directory that is /// NOT added to `PATH`. Use for tests that set `CC` to an /// absolute path directly. pub fn add_compiler_off_path(&mut self, name: &str) -> PathBuf { let dir = TempDir::new().unwrap(); let path = dir.path().join(name); write_executable(&path); self.tool_dirs.push(dir); path } /// Writes a fake compiler at `current_dir//`. /// Used for relative-path tests. pub fn add_compiler_in_cwd_subdir(&mut self, subdir: &str, name: &str) -> PathBuf { let dir = self.current_dir.path().join(subdir); std::fs::create_dir_all(&dir).unwrap(); let path = dir.join(name); write_executable(&path); path } pub fn with_env(mut self, key: &str, value: &str) -> Self { self.environment.insert(key.into(), value.into()); self } /// Overrides PATH with the given string. Use only for tests /// that want a specific, non-tool-dir PATH (e.g. to force /// PATH resolution failure). pub fn with_path_string(mut self, value: &str) -> Self { self.environment.insert(KEY_OS_PATH.to_string(), value.into()); self } pub fn with_preload_supported(mut self, v: bool) -> Self { self.preload_supported = v; self } #[cfg(unix)] pub fn current_dir(&self) -> &Path { self.current_dir.path() } /// The `.bear` directory path that `create_as_wrapper` will /// populate. pub fn wrapper_dir(&self) -> PathBuf { self.current_dir.path().join(".bear") } /// Full path of a named wrapper inside `.bear/`. pub fn wrapper_path_for(&self, name: &str) -> PathBuf { self.wrapper_dir().join(name) } /// Borrows the underlying `current_dir` TempDir, for /// compatibility with helpers like `get_wrapper_dir_path`. pub fn current_dir_tempdir(&self) -> &TempDir { &self.current_dir } pub fn context(&self) -> context::Context { context::Context { current_executable: self.install_dir.path().join("bin").join("bear-driver"), current_directory: self.current_dir.path().to_path_buf(), environment: self.environment.clone(), preload_supported: self.preload_supported, confstr_path: self.confstr_path.clone(), } } } } #[cfg(unix)] #[test] fn test_build_environment_create_preload() { use std::collections::HashMap; use std::net::{IpAddr, Ipv4Addr}; let sut = { let context = { let environment = { let mut builder = HashMap::new(); builder.insert(KEY_OS_PATH.to_string(), "/usr/bin:/bin".to_string()); builder.insert("CC".to_string(), "/usr/bin/gcc".to_string()); builder.insert("CXX".to_string(), "/usr/bin/g++".to_string()); builder }; context::Context { current_executable: PathBuf::from("/usr/bin/bear"), current_directory: PathBuf::from("/tmp"), environment, preload_supported: true, confstr_path: String::from("/usr/bin:/bin"), } }; let address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080); BuildEnvironment::create_as_preload(&context, address).unwrap() }; let libdir = env!("INTERCEPT_LIBDIR"); let preload_name = env!("PRELOAD_NAME"); let expected_library = format!("/usr/{libdir}/{preload_name}"); let expected_state = format!(r#"{{"destination":"127.0.0.1:8080","library":"{expected_library}"}}"#); // Check platform-specific preload configuration #[cfg(target_os = "macos")] { assert_eq!(sut.environment_overrides.get(KEY_INTERCEPT_STATE), Some(&expected_state)); let dyld_preload = sut.environment_overrides.get(KEY_OS_MACOS_PRELOAD_PATH).unwrap(); assert_first_path_entry(&expected_library, dyld_preload); assert_eq!(sut.environment_overrides.get(KEY_OS_MACOS_FLAT_NAMESPACE), Some(&"1".to_string())); } #[cfg(not(target_os = "macos"))] { assert_eq!(sut.environment_overrides.get(KEY_INTERCEPT_STATE), Some(&expected_state)); let ld_preload = sut.environment_overrides.get(KEY_OS_PRELOAD_PATH).unwrap(); assert_first_path_entry(&expected_library, ld_preload); } assert!(sut._wrapper_directory.is_none()); } // Requirements: interception-mechanism-selection #[test] fn test_create_preload_rejected_when_preload_unsupported() { // The fixture defaults to preload_supported=false, modelling a host // where library injection is unavailable (Windows, macOS with SIP). let fx = fixture::Fixture::new(); let sut = BuildEnvironment::create_as_preload(&fx.context(), fixture::test_address()); let Err(error) = sut else { panic!("forced preload must be rejected when unsupported"); }; assert!( matches!(error, ConfigurationError::UnsupportedInterceptMode(_)), "expected UnsupportedInterceptMode, got: {error:?}", ); assert!( error.to_string().contains("wrapper mode"), "error must name wrapper mode as the alternative: {error}", ); } #[test] fn test_wrapper_environment_path_setting_when_it_was_empty_before() { let fx = fixture::Fixture::new(); let sut = BuildEnvironment::create_as_wrapper(&fx.context(), &[], fixture::test_address(), |_| true) .unwrap(); let path = sut.environment_overrides.get("PATH").unwrap(); let expected = get_wrapper_dir_path(fx.current_dir_tempdir()); assert_first_path_entry(&expected, path); } // Sets `CC`/`CXX` to raw absolute paths from a TempDir, which materialise // as backslash-separated strings on Windows. That input pattern is not a // realistic Windows scenario: MSYS2/Git Bash, where `make` actually runs // on Windows, use forward-slash paths. The equivalent Windows coverage // lives in // `env_with_flags::forward_slash_absolute_cc_registers_wrapper_on_windows`. #[cfg(unix)] #[test] fn test_build_environment_create_wrapper() { let mut fx = fixture::Fixture::new().with_preload_supported(true); let paths = fx.add_compilers_on_path(&["cc", "clang", "gcc", "g++"]); let [cc_path, clang_path, gcc_path, gxx_path] = [&paths[0], &paths[1], &paths[2], &paths[3]]; let fx = fx.with_env("CC", &gcc_path.to_string_lossy()).with_env("CXX", &gxx_path.to_string_lossy()); let executables = vec![cc_path.clone(), clang_path.clone()]; let address = fixture::test_address(); let sut = BuildEnvironment::create_as_wrapper(&fx.context(), &executables, address, |_| true).unwrap(); let path = sut.environment_overrides.get("PATH").unwrap(); assert_first_path_entry(&get_wrapper_dir_path(fx.current_dir_tempdir()), path); let wrapper_dir = sut._wrapper_directory.as_ref().expect("wrapper directory"); let wrapper_config = wrapper_dir.config(); assert_eq!(wrapper_config.collector_address, address); assert_eq!(wrapper_config.get_executable("cc").unwrap(), cc_path); assert_eq!(wrapper_config.get_executable("clang").unwrap(), clang_path); assert_eq!(wrapper_config.get_executable("gcc").unwrap(), gcc_path); assert_eq!(wrapper_config.get_executable("g++").unwrap(), gxx_path); assert!(sut.environment_overrides.get("CC").unwrap().contains(".bear")); assert!(sut.environment_overrides.get("CXX").unwrap().contains(".bear")); } // Requirements: interception-wrapper-mechanism #[test] fn test_path_discovery_with_empty_executables() { #[cfg(unix)] let compilers = ["gcc", "g++", "clang", "notacompiler"]; #[cfg(not(unix))] let compilers = ["gcc.exe", "g++.exe", "clang.exe", "notacompiler"]; let mut fx = fixture::Fixture::new().with_preload_supported(true); fx.add_compilers_on_path(&compilers); let known_compilers = ["gcc", "g++", "clang", "gcc.exe", "g++.exe", "clang.exe"]; let build_env = BuildEnvironment::create_as_wrapper( &fx.context(), &[], // Empty executables - should trigger PATH discovery fixture::test_address(), |path| { path.file_name() .and_then(|n| n.to_str()) .map(|n| known_compilers.contains(&n)) .unwrap_or(false) }, ) .expect("PATH discovery should succeed"); let path = build_env.environment_overrides.get("PATH").unwrap(); assert_first_path_entry(&get_wrapper_dir_path(fx.current_dir_tempdir()), path); let wrapper_config = build_env._wrapper_directory.as_ref().unwrap().config(); #[cfg(unix)] for name in ["gcc", "g++", "clang"] { assert!(wrapper_config.get_executable(name).is_some(), "{name} should be discovered"); } #[cfg(not(unix))] for name in ["gcc.exe", "g++.exe", "clang.exe"] { assert!(wrapper_config.get_executable(name).is_some(), "{name} should be discovered"); } assert!(wrapper_config.get_executable("notacompiler").is_none()); } // Configured executables replace the PATH scan rather than adding to it. // This is the contract, not an oversight: see // `interception-wrapper-mechanism`. // Requirements: interception-wrapper-mechanism #[test] fn test_path_discovery_skipped_with_executables() { let compiler = if cfg!(unix) { "gcc" } else { "gcc.exe" }; let mut fx = fixture::Fixture::new().with_preload_supported(true); fx.add_compiler_on_path(compiler); let custom_compiler = PathBuf::from("/usr/bin/custom-gcc"); let build_env = BuildEnvironment::create_as_wrapper( &fx.context(), std::slice::from_ref(&custom_compiler), fixture::test_address(), |_| true, ) .expect("should succeed with explicit executables"); let wrapper_config = build_env._wrapper_directory.as_ref().unwrap().config(); assert!(wrapper_config.get_executable("custom-gcc").is_some()); assert!( wrapper_config.get_executable(compiler).is_none(), "PATH discovery should be skipped when explicit executables are provided", ); } // U1: resolve_program_path finds bare name via PATH #[test] fn test_env_var_bare_name_resolved_via_path() { let basename = fixture::compiler_basename(); let mut fx = fixture::Fixture::new(); let compiler_path = fx.add_compiler_on_path(basename); let fx = fx.with_env("CC", "fake-cc"); let sut = BuildEnvironment::create_as_wrapper(&fx.context(), &[], fixture::test_address(), |_| false) .unwrap(); let wrapper_config = sut._wrapper_directory.as_ref().unwrap().config(); assert_eq!(wrapper_config.get_executable(basename).unwrap(), &compiler_path); assert!(sut.environment_overrides.get("CC").unwrap().contains(".bear")); } // U2: resolve_program_path returns absolute path as-is. // Unix-only: the TempDir path is backslash-separated on Windows, which is // not a realistic `CC` value on that platform. See the Windows-specific // counterpart `env_with_flags::forward_slash_absolute_cc_registers_wrapper_on_windows`. #[cfg(unix)] #[test] fn test_env_var_absolute_path_passed_through() { let compiler_name = "my-gcc"; let mut fx = fixture::Fixture::new().with_path_string("/usr/bin"); let compiler_path = fx.add_compiler_off_path(compiler_name); let fx = fx.with_env("CC", &compiler_path.to_string_lossy()); let sut = BuildEnvironment::create_as_wrapper(&fx.context(), &[], fixture::test_address(), |_| false) .unwrap(); assert!( sut._wrapper_directory.as_ref().unwrap().config().get_executable(compiler_name).is_some(), "absolute CC path should be registered as {compiler_name}", ); } // U4: resolve_program_path returns None for unresolvable name #[test] fn test_env_var_unresolvable_name_skipped() { let fx = fixture::Fixture::new().with_path_string("/usr/bin").with_env("CC", "nonexistent-compiler-xyz"); let sut = BuildEnvironment::create_as_wrapper(&fx.context(), &[], fixture::test_address(), |_| false) .unwrap(); assert!(!sut.environment_overrides.contains_key("CC")); } // U3: resolve_program_path resolves relative path against cwd #[test] fn test_env_var_relative_path_resolved_against_cwd() { let compiler_name = if cfg!(windows) { "my-cc.exe" } else { "my-cc" }; let mut fx = fixture::Fixture::new().with_path_string("/usr/bin"); fx.add_compiler_in_cwd_subdir("tools", compiler_name); let fx = fx.with_env("CC", &format!("tools/{compiler_name}")); let sut = BuildEnvironment::create_as_wrapper(&fx.context(), &[], fixture::test_address(), |_| false) .unwrap(); let resolved = sut ._wrapper_directory .as_ref() .unwrap() .config() .get_executable(compiler_name) .unwrap_or_else(|| panic!("relative CC=tools/{compiler_name} should resolve against cwd")); assert!(resolved.is_absolute(), "resolved path should be absolute, got: {}", resolved.display()); assert!(sut.environment_overrides.get("CC").unwrap().contains(".bear")); } #[cfg(unix)] mod masquerade { use super::super::{filter_out_paths, is_masquerade_wrapper, resolve_past_masquerade_wrappers}; use std::os::unix::fs::PermissionsExt; use std::path::PathBuf; use tempfile::TempDir; fn write_executable(path: &std::path::Path, content: &str) { std::fs::write(path, content).unwrap(); std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).unwrap(); } // Requirements: interception-wrapper-recursion #[test] fn detects_symlink_to_ccache() { let dir = TempDir::new().unwrap(); let fake_ccache = dir.path().join("ccache"); write_executable(&fake_ccache, "#!/bin/sh\n"); let gcc_symlink = dir.path().join("masq").join("gcc"); std::fs::create_dir_all(gcc_symlink.parent().unwrap()).unwrap(); std::os::unix::fs::symlink(&fake_ccache, &gcc_symlink).unwrap(); assert!(is_masquerade_wrapper(&gcc_symlink)); } // Requirements: interception-wrapper-recursion #[test] fn detects_all_known_wrapper_names() { let dir = TempDir::new().unwrap(); for name in ["distcc", "icecc", "colorgcc", "buildcache"] { let target = dir.path().join(name); write_executable(&target, "#!/bin/sh\n"); let link = dir.path().join(format!("{name}-gcc-link")); std::os::unix::fs::symlink(&target, &link).unwrap(); assert!(is_masquerade_wrapper(&link), "{name} target should be detected"); } } // Requirements: interception-wrapper-recursion #[test] fn ignores_real_compiler_and_non_wrapper_symlinks() { let dir = TempDir::new().unwrap(); let real_gcc = dir.path().join("gcc-13"); write_executable(&real_gcc, "#!/bin/sh\n"); assert!(!is_masquerade_wrapper(&real_gcc)); let gcc_symlink = dir.path().join("gcc"); std::os::unix::fs::symlink(&real_gcc, &gcc_symlink).unwrap(); assert!(!is_masquerade_wrapper(&gcc_symlink)); } // Requirements: interception-wrapper-recursion #[test] fn ignores_broken_and_missing_paths() { let dir = TempDir::new().unwrap(); let broken = dir.path().join("broken-link"); std::os::unix::fs::symlink(dir.path().join("does-not-exist"), &broken).unwrap(); assert!(!is_masquerade_wrapper(&broken)); assert!(!is_masquerade_wrapper(&dir.path().join("does-not-exist"))); } // Requirements: interception-wrapper-recursion #[test] fn filter_out_paths_drops_matching_entries_only() { let a = PathBuf::from("/a"); let b = PathBuf::from("/b"); let c = PathBuf::from("/c"); let original = std::env::join_paths([&a, &b, &c]).unwrap().into_string().unwrap_or_default(); let kept = filter_out_paths(&original, std::slice::from_ref(&b)); let entries: Vec = std::env::split_paths(&kept).collect(); assert_eq!(entries, vec![a, c]); } /// A PATH entry written with a trailing separator must still match an /// excluded dir derived from `Path::parent()` (which never has one). // Requirements: interception-wrapper-recursion #[test] fn filter_out_paths_matches_across_trailing_separator() { let sep = std::path::MAIN_SEPARATOR; let original = format!("{sep}a{sep}:{sep}b{sep}:{sep}c"); let excluded = PathBuf::from(format!("{sep}b")); let kept = filter_out_paths(&original, std::slice::from_ref(&excluded)); let entries: Vec = std::env::split_paths(&kept).collect(); assert_eq!( entries, vec![PathBuf::from(format!("{sep}a{sep}")), PathBuf::from(format!("{sep}c"))], "trailing-slash PATH entry was not filtered", ); } // Requirements: interception-wrapper-recursion #[test] fn resolver_returns_real_compiler_when_no_masquerade() { let dir = TempDir::new().unwrap(); let real = dir.path().join("gcc"); write_executable(&real, "#!/bin/sh\n"); let path = std::env::join_paths([dir.path()]).unwrap().into_string().unwrap_or_default(); let found = resolve_past_masquerade_wrappers("gcc", &path, dir.path()).unwrap(); assert_eq!(found, real); } // Requirements: interception-wrapper-recursion #[test] fn resolver_skips_masquerade_and_returns_next_real_compiler() { let dir = TempDir::new().unwrap(); let ccache_bin = dir.path().join("bin").join("ccache"); std::fs::create_dir_all(ccache_bin.parent().unwrap()).unwrap(); write_executable(&ccache_bin, "#!/bin/sh\n"); let masq_dir = dir.path().join("ccache_dir"); std::fs::create_dir_all(&masq_dir).unwrap(); let masq_gcc = masq_dir.join("gcc"); std::os::unix::fs::symlink(&ccache_bin, &masq_gcc).unwrap(); let real_dir = dir.path().join("real"); std::fs::create_dir_all(&real_dir).unwrap(); let real_gcc = real_dir.join("gcc"); write_executable(&real_gcc, "#!/bin/sh\n"); let path = std::env::join_paths([&masq_dir, &real_dir]).unwrap().into_string().unwrap_or_default(); let found = resolve_past_masquerade_wrappers("gcc", &path, dir.path()).unwrap(); assert_eq!(found, real_gcc); } // Requirements: interception-wrapper-recursion #[test] fn resolver_returns_none_when_only_masquerade_is_reachable() { let dir = TempDir::new().unwrap(); let ccache_bin = dir.path().join("ccache"); write_executable(&ccache_bin, "#!/bin/sh\n"); let masq_dir = dir.path().join("masq"); std::fs::create_dir_all(&masq_dir).unwrap(); let masq_gcc = masq_dir.join("gcc"); std::os::unix::fs::symlink(&ccache_bin, &masq_gcc).unwrap(); let path = std::env::join_paths([&masq_dir]).unwrap().into_string().unwrap_or_default(); assert!(resolve_past_masquerade_wrappers("gcc", &path, dir.path()).is_none()); } // Requirements: interception-wrapper-recursion #[test] fn resolver_skips_multiple_masquerade_layers() { let dir = TempDir::new().unwrap(); let ccache_bin = dir.path().join("ccache"); let distcc_bin = dir.path().join("distcc"); write_executable(&ccache_bin, "#!/bin/sh\n"); write_executable(&distcc_bin, "#!/bin/sh\n"); let masq1 = dir.path().join("m1"); let masq2 = dir.path().join("m2"); let real = dir.path().join("real"); std::fs::create_dir_all(&masq1).unwrap(); std::fs::create_dir_all(&masq2).unwrap(); std::fs::create_dir_all(&real).unwrap(); std::os::unix::fs::symlink(&ccache_bin, masq1.join("gcc")).unwrap(); std::os::unix::fs::symlink(&distcc_bin, masq2.join("gcc")).unwrap(); let real_gcc = real.join("gcc"); write_executable(&real_gcc, "#!/bin/sh\n"); let path = std::env::join_paths([&masq1, &masq2, &real]).unwrap().into_string().unwrap_or_default(); let found = resolve_past_masquerade_wrappers("gcc", &path, dir.path()).unwrap(); assert_eq!(found, real_gcc); } } /// An explicit CC=/path/to/ccache-masquerade/gcc must not be stored /// verbatim in the wrapper config -- that would recreate the recursion /// the masquerade filter is meant to prevent. // Requirements: interception-wrapper-recursion #[cfg(unix)] #[test] fn resolve_program_path_falls_back_past_masquerade_for_absolute_cc() { use std::collections::HashMap; use std::os::unix::fs::PermissionsExt; use tempfile::TempDir; let dir = TempDir::new().unwrap(); let ccache_bin = dir.path().join("ccache"); std::fs::write(&ccache_bin, "#!/bin/sh\n").unwrap(); std::fs::set_permissions(&ccache_bin, std::fs::Permissions::from_mode(0o755)).unwrap(); let masq_dir = dir.path().join("masq"); std::fs::create_dir_all(&masq_dir).unwrap(); let masq_gcc = masq_dir.join("gcc"); std::os::unix::fs::symlink(&ccache_bin, &masq_gcc).unwrap(); let real_dir = dir.path().join("real"); std::fs::create_dir_all(&real_dir).unwrap(); let real_gcc = real_dir.join("gcc"); std::fs::write(&real_gcc, "#!/bin/sh\n").unwrap(); std::fs::set_permissions(&real_gcc, std::fs::Permissions::from_mode(0o755)).unwrap(); let mut environment = HashMap::new(); environment.insert( KEY_OS_PATH.to_string(), std::env::join_paths([&masq_dir, &real_dir]).unwrap().into_string().unwrap(), ); let context = context::Context { current_executable: PathBuf::from("/unused"), current_directory: dir.path().to_path_buf(), environment, preload_supported: true, confstr_path: String::new(), }; let resolved = BuildEnvironment::resolve_program_path(&context, masq_gcc.to_str().unwrap()); assert_eq!( resolved.as_deref(), Some(real_gcc.as_path()), "absolute CC pointing at a masquerade symlink must fall back to the real compiler on PATH", ); } mod env_with_flags { //! Requirements: interception-cc-env-var-flags use super::super::parse_program_env_value; use super::*; #[test] fn parser_skips_empty_and_whitespace() { assert_eq!(parse_program_env_value(""), None); assert_eq!(parse_program_env_value(" "), None); assert_eq!(parse_program_env_value("\t\n"), None); } #[test] fn parser_splits_bare_name_with_flags() { let (program, flags) = parse_program_env_value("gcc -std=c11 -Wall").unwrap(); assert_eq!(program, "gcc"); assert_eq!(flags, vec!["-std=c11", "-Wall"]); } #[test] fn parser_returns_program_alone_when_no_flags() { let (program, flags) = parse_program_env_value("gcc").unwrap(); assert_eq!(program, "gcc"); assert!(flags.is_empty()); } /// Regression guard: when the env var has no flags, the override must /// be the wrapper path as a bare string, byte-identical to how it was /// emitted before this requirement. /// /// Unix-only: the fixture builds `CC` from a TempDir path, which is /// backslash-separated on Windows -- not a realistic `CC` value in /// MSYS2/Git Bash (the shells where `bear -- make` runs on Windows). /// Windows coverage of absolute `CC` paths lives in /// `forward_slash_absolute_cc_registers_wrapper_on_windows`. #[cfg(unix)] #[test] fn no_flag_override_is_byte_identical_to_wrapper_path() { let basename = fixture::compiler_basename(); let mut fx = fixture::Fixture::new(); let compiler_path = fx.add_compiler_on_path(basename); let fx = fx.with_env("CC", &compiler_path.to_string_lossy()); let sut = BuildEnvironment::create_as_wrapper(&fx.context(), &[], fixture::test_address(), |_| false) .unwrap(); assert_eq!( sut.environment_overrides.get("CC").unwrap(), &fx.wrapper_path_for(basename).to_string_lossy().into_owned(), "no-flag override must be the raw wrapper path, no shell quoting", ); } #[test] fn bare_cc_with_flags_resolves_program_and_preserves_flags() { let basename = fixture::compiler_basename(); let mut fx = fixture::Fixture::new(); let compiler_path = fx.add_compiler_on_path(basename); let fx = fx.with_env("CC", &format!("{basename} -std=c11 -Wall")); let sut = BuildEnvironment::create_as_wrapper(&fx.context(), &[], fixture::test_address(), |_| false) .unwrap(); assert_eq!( sut._wrapper_directory.as_ref().unwrap().config().get_executable(basename).unwrap(), &compiler_path ); let expected = format!("{} -std=c11 -Wall", fx.wrapper_path_for(basename).to_string_lossy()); assert_eq!(sut.environment_overrides.get("CC").unwrap(), &expected); } // Unix-only for the same reason as // `no_flag_override_is_byte_identical_to_wrapper_path`: backslash- // separated TempDir paths are not a realistic Windows `CC` shape. #[cfg(unix)] #[test] fn absolute_cc_with_flags_resolves_program_and_preserves_flags() { let basename = fixture::compiler_basename(); let mut fx = fixture::Fixture::new().with_path_string("/usr/bin"); let compiler_path = fx.add_compiler_off_path(basename); let fx = fx.with_env("CC", &format!("{} -m32 -DX=1", compiler_path.to_string_lossy())); let sut = BuildEnvironment::create_as_wrapper(&fx.context(), &[], fixture::test_address(), |_| false) .unwrap(); let expected = format!("{} -m32 -DX=1", fx.wrapper_path_for(basename).to_string_lossy()); assert_eq!(sut.environment_overrides.get("CC").unwrap(), &expected); } #[test] fn empty_or_whitespace_only_cc_is_not_rewritten() { for value in ["", " ", "\t\n"] { let fx = fixture::Fixture::new().with_path_string("/usr/bin").with_env("CC", value); let sut = BuildEnvironment::create_as_wrapper(&fx.context(), &[], fixture::test_address(), |_| { false }) .unwrap(); assert!(!sut.environment_overrides.contains_key("CC"), "CC={value:?} must not be rewritten"); } } /// Covers the intersection of `interception-wrapper-recursion` and /// this requirement: a ccache masquerade dir first on PATH, with a /// real compiler past it, must resolve past the masquerade and still /// preserve the flags from the env var value. // Requirements: interception-cc-env-var-flags, interception-wrapper-recursion #[cfg(unix)] #[test] fn masquerade_with_flags_resolves_real_compiler_and_preserves_flags() { use std::os::unix::fs::PermissionsExt; let fx = fixture::Fixture::new(); let dir = fx.current_dir(); // ccache binary and a masq dir with a symlink to it. Bear's // resolver must step past the masq dir and land on `real/gcc`. let ccache_bin = dir.join("ccache"); std::fs::write(&ccache_bin, "#!/bin/sh\n").unwrap(); std::fs::set_permissions(&ccache_bin, std::fs::Permissions::from_mode(0o755)).unwrap(); let masq_dir = dir.join("masq"); std::fs::create_dir_all(&masq_dir).unwrap(); std::os::unix::fs::symlink(&ccache_bin, masq_dir.join("gcc")).unwrap(); let real_dir = dir.join("real"); std::fs::create_dir_all(&real_dir).unwrap(); let real_gcc = real_dir.join("gcc"); std::fs::write(&real_gcc, "#!/bin/sh\n").unwrap(); std::fs::set_permissions(&real_gcc, std::fs::Permissions::from_mode(0o755)).unwrap(); let path_value = std::env::join_paths([&masq_dir, &real_dir]).unwrap().into_string().unwrap(); let fx = fx.with_path_string(&path_value).with_env("CC", "gcc -std=c11"); let sut = BuildEnvironment::create_as_wrapper(&fx.context(), &[], fixture::test_address(), |_| false) .unwrap(); assert_eq!( sut._wrapper_directory.as_ref().unwrap().config().get_executable("gcc").unwrap(), &real_gcc, "masquerade symlink must be resolved past, just like without flags", ); let expected = format!("{} -std=c11", fx.wrapper_path_for("gcc").to_string_lossy()); assert_eq!(sut.environment_overrides.get("CC").unwrap(), &expected); } /// Windows coverage of `CC= -flag` using the path /// style MSYS2/Git Bash users actually produce: forward slashes /// with a drive letter (e.g. `C:/tools/gcc.exe`). Backslash- /// separated paths are shell escapes in those environments, so /// they are not a realistic `CC` value and are not exercised here. #[cfg(windows)] #[test] fn forward_slash_absolute_cc_registers_wrapper_on_windows() { let basename = fixture::compiler_basename(); let mut fx = fixture::Fixture::new().with_path_string("C:/Windows/System32"); let compiler_path = fx.add_compiler_off_path(basename); let forward_slash_path = compiler_path.to_string_lossy().replace('\\', "/"); let fx = fx.with_env("CC", &format!("{forward_slash_path} -DBEAR_TEST=1")); let sut = BuildEnvironment::create_as_wrapper(&fx.context(), &[], fixture::test_address(), |_| false) .unwrap(); assert!( sut._wrapper_directory.as_ref().unwrap().config().get_executable(basename).is_some(), "forward-slash absolute CC path must register a wrapper for {basename}", ); let cc_value = sut.environment_overrides.get("CC").unwrap(); assert!( cc_value.ends_with(" -DBEAR_TEST=1"), "override must preserve -DBEAR_TEST=1: {cc_value:?}" ); assert!(cc_value.contains(".bear"), "override must point at the .bear wrapper: {cc_value:?}"); } } } rizsotto-Bear-0c30b27/crates/intercept-supervisor/src/supervise.rs000066400000000000000000000574741527177720300255130ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Requirements: cli-signal-forwarding use intercept::Execution; use std::path::PathBuf; use std::process::ExitStatus; use thiserror::Error; /// Whether this supervisor owns the build's process group or merely inherits it. /// /// In wrapper mode the supervision chain nests /// (`bear-driver` -> `make` -> `bear-wrapper` -> real `cc`); only the /// outermost supervisor may create a process group. If every level created /// its own group, a top-level group kill would miss the deeper processes. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GroupPolicy { /// The outermost supervisor (the driver). Creates a new process group with /// the child as leader and owns the authoritative escalation: forward the /// received signal to the whole group, grant a grace window, then escalate /// to `SIGKILL`. Leader, /// A nested supervisor (the wrapper). Stays in the inherited group and only /// relays the received signal to its direct child; the driver's group kill /// is the authoritative teardown, so it runs no grace/`SIGKILL` timer. Inherit, } /// This method supervises the execution of a command. /// /// It starts the command and waits for its completion. While waiting it /// forwards termination signals it receives to the build so the build stops /// too, and propagates the build's exit status back to the caller. /// /// On Unix a `Leader` supervisor places the child in a new process group and, /// on a termination signal, forwards the real signal to the whole group, grants /// a short grace window, then escalates to `SIGKILL` - so the entire process /// tree is torn down, not just the immediate child. An `Inherit` supervisor /// relays the signal to its direct child only and lets the leader's group kill /// reap the rest. pub fn supervise( command: &mut std::process::Command, policy: GroupPolicy, ) -> Result { platform::supervise(command, policy) } /// This function supervises the execution of a command represented by the `Execution` struct. pub fn supervise_execution(execution: Execution, policy: GroupPolicy) -> Result { let mut child = command_from_execution(execution)?; supervise(&mut child, policy) } /// Builds a [`std::process::Command`] from an [`Execution`]. /// /// This is a free function rather than a `TryFrom` impl because the orphan /// rule forbids implementing the foreign `TryFrom`/`Command` for the foreign /// `Execution` from this crate. fn command_from_execution(val: Execution) -> Result { let mut command = match val.arguments.as_slice() { [] => return Err(SuperviseError::EmptyArguments), [_] => std::process::Command::new(val.executable), [_, arguments @ ..] => { let mut cmd = std::process::Command::new(val.executable); cmd.args(arguments); cmd } }; command.envs(val.environment); command.current_dir(val.working_dir); Ok(command) } #[cfg(unix)] mod platform { use super::cgroup; use super::{GroupPolicy, SuperviseError}; use signal_hook::consts::signal::SIGCHLD; use signal_hook::iterator::Signals; use std::path::{Path, PathBuf}; use std::process::{Child, ExitStatus}; use std::time::{Duration, Instant}; /// How long the build's process tree is allowed to wind down on its own /// after the real termination signal before the `Leader` escalates to /// `SIGKILL`. A cooperating tree ends the window early by exiting, so /// only a build that ignores the signal pays the full wait; sized to /// the largest value that keeps that case inside the requirement's /// sub-one-second teardown budget, because the headroom is what keeps /// a trap on a starved CI runner from being SIGKILLed mid-flight /// (issue #713). const GRACE: Duration = Duration::from_millis(800); pub(super) fn supervise( command: &mut std::process::Command, policy: GroupPolicy, ) -> Result { use std::os::unix::process::CommandExt; let executable = PathBuf::from(command.get_program()); // The leader owns the build's process group so a single killpg reaps // the whole tree. process_group(0) is safe std: the child becomes a // new group leader (pgid == child pid) before exec. if policy == GroupPolicy::Leader { command.process_group(0); } // On Linux, additionally place the build in a fresh cgroup so a // descendant that re-`setsid`s out of the process group is still // reaped on escalation. `create_and_attach` returns `None` (and // teardown falls back to the process-group `SIGKILL`) where cgroup v2 // is unavailable or its directory is not writable/delegated. Only the // leader owns a cgroup; nested wrappers inherit it through the child. let cgroup = if policy == GroupPolicy::Leader { cgroup::create_and_attach(command) } else { None }; // Watch the termination signals plus SIGCHLD so the wait below blocks // until either the build wants to be torn down or the child exits. let mut watched: Vec = signal_hook::consts::TERM_SIGNALS.to_vec(); watched.push(SIGCHLD); let mut signals = Signals::new(&watched).map_err(|err| SuperviseError::SignalRegistration { executable: executable.clone(), source: err, })?; let mut child = command .spawn() .map_err(|err| SuperviseError::ProcessSpawn { executable: executable.clone(), source: err })?; let child_pid = child.id() as libc::pid_t; // Close the child-exited-early race: the child may already be gone // before we block on the signal iterator. if let Some(status) = try_wait(&mut child, &executable)? { return Ok(status); } // Phase 1: wait for either SIGCHLD (child reaped) or a term signal // (the teardown trigger). signal-hook's self-pipe persists a signal // delivered after registration, so a signal racing this loop is not // lost. let mut received: Option = None; for info in &mut signals { if info == SIGCHLD { if let Some(status) = try_wait(&mut child, &executable)? { return Ok(status); } } else { received = Some(info); break; } } // The loop exits only via the `break` above: we never close the signal // handle, and `Signals::forever()` ends only on a closed handle. If // signal-hook ever closes it out from under us, there is nothing left to // forward, so fall back to blocking until the child is reaped. let Some(signal) = received else { return child.wait().map_err(|err| SuperviseError::ProcessWait { executable, source: err }); }; match policy { GroupPolicy::Leader => { leader_teardown(&mut child, child_pid, signal, &executable, cgroup.as_ref()) } GroupPolicy::Inherit => inherit_forward(&mut child, child_pid, signal, &executable, &mut signals), } } /// Leader: deliver the real signal to the whole group, grant a grace /// window, then force whatever is still alive. The leader runs the single /// authoritative escalation timer for the supervision chain. fn leader_teardown( child: &mut Child, pgid: libc::pid_t, signal: libc::c_int, executable: &Path, cgroup: Option<&cgroup::Cgroup>, ) -> Result { log::debug!("Received signal {signal}, forwarding to process group {pgid}"); send(SendTarget::Group(pgid), signal); // Let the tree wind down on its own (run traps, drain in-flight work) // before forcing it. The direct child exiting ends the grace early, // but a cgroup is still swept below: a detached descendant can outlive // the child that exited cleanly. let deadline = Instant::now() + GRACE; let mut exited = None; loop { if let Some(status) = try_wait(child, executable)? { exited = Some(status); break; } if Instant::now() >= deadline { break; } std::thread::sleep(Duration::from_millis(10)); } if exited.is_none() { // One stable line across both escalation paths (cgroup and // process-group): tests and triage key on it to distinguish a // starved grace window from a build that exited in time. log::debug!("Build did not exit within the grace window ({GRACE:?})"); } // Force anything still alive. A cgroup kill reaps the whole cgroup, // including a descendant that left the process group (a daemon that // called `setsid`), so it runs even when the direct child already // exited. Without a cgroup, fall back to forcing the process group - // but only when something is still there to force. match cgroup { Some(cgroup) => { log::debug!("Killing build cgroup to reap the whole tree"); cgroup.kill(); } None if exited.is_none() => { log::debug!("Grace window elapsed, sending SIGKILL to process group {pgid}"); send(SendTarget::Group(pgid), libc::SIGKILL); } None => {} } if let Some(status) = exited { return Ok(status); } // Nothing left to forward; block until the direct child is reaped. A // blocking wait (not a poll loop) avoids burning CPU and does not hang // any worse than the kernel itself: if SIGKILL cannot collect the child // (e.g. uninterruptible sleep), no caller could have done better. child .wait() .map_err(|err| SuperviseError::ProcessWait { executable: executable.to_path_buf(), source: err }) } /// Inherit: relay the real signal to the direct child only and wait for it /// to be reaped. The leader's group kill is authoritative, so no grace or /// `SIGKILL` timer runs here. fn inherit_forward( child: &mut Child, child_pid: libc::pid_t, signal: libc::c_int, executable: &Path, signals: &mut Signals, ) -> Result { log::debug!("Received signal {signal}, forwarding to child {child_pid}"); send(SendTarget::Process(child_pid), signal); if let Some(status) = try_wait(child, executable)? { return Ok(status); } for info in signals { if info == SIGCHLD && let Some(status) = try_wait(child, executable)? { return Ok(status); } } // We never close the signal handle, so the loop above only ends if // signal-hook closes it itself; in that case block until the child is // reaped (the leader's group kill remains the authoritative teardown). child .wait() .map_err(|err| SuperviseError::ProcessWait { executable: executable.to_path_buf(), source: err }) } fn try_wait(child: &mut Child, executable: &Path) -> Result, SuperviseError> { match child.try_wait() { Ok(Some(status)) => { log::debug!("Child process exited: {status:?}"); Ok(Some(status)) } Ok(None) => Ok(None), Err(err) => { log::error!("Error waiting for child process: {err}"); Err(SuperviseError::ProcessWait { executable: executable.to_path_buf(), source: err }) } } } enum SendTarget { Group(libc::pid_t), Process(libc::pid_t), } /// Best-effort delivery of a signal to a process or process group. /// /// Signal forwarding is best effort: a failure to deliver must not turn /// into a hard supervision error (only spawn/wait failures are fatal). /// `ESRCH` - the target already exited, the normal exit-then-signal race - /// is benign and logged at debug; any other errno is logged at error. fn send(target: SendTarget, signal: libc::c_int) { // SAFETY: kill()/killpg() are async-signal-safe libc calls taking // plain integers; they cannot violate memory safety. We inspect errno // only on the documented -1 return. let rc = unsafe { match target { SendTarget::Group(pgid) => libc::killpg(pgid, signal), SendTarget::Process(pid) => libc::kill(pid, signal), } }; if rc == -1 { let err = std::io::Error::last_os_error(); if err.raw_os_error() == Some(libc::ESRCH) { log::debug!("Signal target already gone (ESRCH), nothing to forward"); } else { log::error!("Failed to forward signal {signal}: {err}"); } } } } /// Linux cgroup v2 teardown: closes the one hole process groups leave open - /// a descendant that calls `setsid` escapes the group but cannot leave the /// cgroup unprivileged, so killing the cgroup reaps it too. #[cfg(all(unix, target_os = "linux"))] mod cgroup { use std::fs; use std::io; use std::os::fd::{AsRawFd, OwnedFd, RawFd}; use std::os::unix::fs::OpenOptionsExt; use std::os::unix::process::CommandExt; use std::path::{Path, PathBuf}; use std::process::Command; use std::time::Duration; /// A freshly created cgroup v2 directory owning one supervised build. /// Dropping it removes the directory, so a normal build leaves nothing /// behind. pub(crate) struct Cgroup { path: PathBuf, } /// Create a child cgroup for `command` and arrange the spawned process to /// join it before `exec`. Best effort: returns `None` (caller falls back /// to process-group teardown) when cgroup v2 is unavailable, not writable, /// or too old to expose `cgroup.kill`. pub(crate) fn create_and_attach(command: &mut Command) -> Option { let cgroup = Cgroup::create()?; if let Err(err) = cgroup.attach(command) { log::debug!("cgroup attach failed ({err}); using process-group teardown"); return None; // `cgroup` drops here and removes the directory } Some(cgroup) } impl Cgroup { fn create() -> Option { let relative = own_v2_path()?; let path = Path::new("/sys/fs/cgroup") .join(relative.trim_start_matches('/')) .join(format!("bear-{}", std::process::id())); if let Err(err) = fs::create_dir(&path) { log::debug!("cgroup unavailable ({}: {err}); using process-group teardown", path.display()); return None; } let cgroup = Cgroup { path }; // `cgroup.kill` exists in every cgroup since kernel 5.14; its // absence means the kernel is too old for this path. if !cgroup.path.join("cgroup.kill").exists() { log::debug!("cgroup.kill missing (kernel too old); using process-group teardown"); return None; // `cgroup` drops here and removes the directory } Some(cgroup) } /// Move the about-to-be-spawned child into this cgroup from a /// `pre_exec` hook, so its whole tree starts inside the cgroup with no /// race window. The `cgroup.procs` file is opened in the parent and /// only written (an async-signal-safe operation) in the child. fn attach(&self, command: &mut Command) -> io::Result<()> { let file = fs::OpenOptions::new() .write(true) .custom_flags(libc::O_CLOEXEC) .open(self.path.join("cgroup.procs"))?; let fd: OwnedFd = file.into(); // SAFETY: the closure runs in the child between fork and exec and // calls only async-signal-safe libc (getpid, write). unsafe { command.pre_exec(move || { let pid = libc::getpid(); write_pid(fd.as_raw_fd(), pid) }); } Ok(()) } /// Kill every process in the cgroup with a single write. Best effort, /// like signal forwarding: a failure is logged, not surfaced. pub(crate) fn kill(&self) { if let Err(err) = fs::write(self.path.join("cgroup.kill"), b"1") { log::error!("failed to kill cgroup {}: {err}", self.path.display()); } } } impl Drop for Cgroup { fn drop(&mut self) { // A cgroup directory is removable only once empty; killed // processes leave it as init reaps them, which can lag under load, // so retry up to ~500ms before giving up and leaving the dir. for _ in 0..100 { match fs::remove_dir(&self.path) { Ok(()) => return, Err(err) if err.raw_os_error() == Some(libc::EBUSY) => { std::thread::sleep(Duration::from_millis(5)); } Err(err) => { log::debug!("could not remove cgroup {}: {err}", self.path.display()); return; } } } log::debug!("cgroup {} still busy after retries; leaving it", self.path.display()); } } /// Our own cgroup v2 path, read from the `0::` line of /// `/proc/self/cgroup`. `None` on a host without a unified hierarchy. fn own_v2_path() -> Option { let content = fs::read_to_string("/proc/self/cgroup").ok()?; content.lines().find_map(|line| line.strip_prefix("0::").map(str::to_string)) } /// Write a pid as decimal bytes to `fd` without allocating, so it is safe /// to call from the post-fork child. fn write_pid(fd: RawFd, pid: libc::pid_t) -> io::Result<()> { let mut buf = [0u8; 20]; let mut at = buf.len(); let mut value = pid as u64; if value == 0 { at -= 1; buf[at] = b'0'; } while value > 0 { at -= 1; buf[at] = b'0' + (value % 10) as u8; value /= 10; } let bytes = &buf[at..]; let mut written = 0; while written < bytes.len() { // SAFETY: async-signal-safe write to a valid, inherited fd. let rc = unsafe { libc::write(fd, bytes[written..].as_ptr().cast(), bytes.len() - written) }; if rc < 0 { let err = io::Error::last_os_error(); // A signal interrupting the write must not fail the spawn: // returning Err here aborts the whole supervised run. if err.raw_os_error() == Some(libc::EINTR) { continue; } return Err(err); } written += rc as usize; } Ok(()) } } /// Non-Linux unix has no cgroups; teardown stays process-group only. The type /// is uninhabited so the leader-teardown signature is uniform across unix. #[cfg(all(unix, not(target_os = "linux")))] mod cgroup { pub(crate) enum Cgroup {} impl Cgroup { pub(crate) fn kill(&self) { match *self {} } } pub(crate) fn create_and_attach(_command: &mut std::process::Command) -> Option { None } } #[cfg(not(unix))] mod platform { use super::{GroupPolicy, SuperviseError}; use std::path::PathBuf; use std::process::ExitStatus; use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; use std::thread; use std::time::Duration; /// A blocking `wait()` cannot be interrupted by a signal on this platform /// (no `SIGCHLD`), so the term-signal flag is observed by polling. Process /// groups are out of scope here, so `GroupPolicy` is ignored and only the /// direct child is signalled. pub(super) fn supervise( command: &mut std::process::Command, _policy: GroupPolicy, ) -> Result { let executable = PathBuf::from(command.get_program()); let signaled = Arc::new(AtomicUsize::new(0)); for signal in signal_hook::consts::TERM_SIGNALS { signal_hook::flag::register_usize(*signal, Arc::clone(&signaled), *signal as usize).map_err( |err| SuperviseError::SignalRegistration { executable: executable.clone(), source: err }, )?; } let mut child = command .spawn() .map_err(|err| SuperviseError::ProcessSpawn { executable: executable.clone(), source: err })?; loop { if signaled.swap(0usize, Ordering::SeqCst) != 0 { log::debug!("Received signal, forwarding to child process"); child.kill().map_err(|err| SuperviseError::ProcessKill { executable: executable.clone(), source: err, })?; } match child.try_wait() { Ok(Some(exit_status)) => { log::debug!("Child process exited: {exit_status:?}"); return Ok(exit_status); } Ok(None) => { thread::sleep(Duration::from_millis(100)); } Err(err) => { log::error!("Error waiting for child process: {err}"); return Err(SuperviseError::ProcessWait { executable: executable.clone(), source: err }); } } } } } /// Errors that can occur during process supervision. #[derive(Error, Debug)] pub enum SuperviseError { #[error("cannot register signal handler for '{executable}': {source}", executable = executable.display())] SignalRegistration { executable: PathBuf, #[source] source: std::io::Error, }, #[error("cannot run '{executable}': {source}", executable = executable.display())] ProcessSpawn { executable: PathBuf, #[source] source: std::io::Error, }, #[error("cannot kill '{executable}': {source}", executable = executable.display())] ProcessKill { executable: PathBuf, #[source] source: std::io::Error, }, #[error("cannot wait for '{executable}': {source}", executable = executable.display())] ProcessWait { executable: PathBuf, #[source] source: std::io::Error, }, #[error("Execution arguments cannot be empty")] EmptyArguments, } #[cfg(all(test, unix))] mod tests { // Requirements: cli-signal-forwarding // // Only the normal-exit path is unit-tested here. The signal-forwarding // paths (real-signal forward, grace window, SIGKILL escalation, whole-tree // teardown) are verified end to end against the real driver in the // integration suite (tests/integration, exit_codes.rs): driving supervise() // in-process is unsafe, because signal-hook handlers are process-global and // would bleed across parallel unit tests. use super::*; use std::process::Command; #[test] fn normal_exit_propagates_status() { // arrange / act / assert per case: (exit code, expected success) let cases = [(0, true), (7, false)]; for (code, expect_success) in cases { let mut command = Command::new("sh"); command.arg("-c").arg(format!("exit {code}")); let sut = supervise(&mut command, GroupPolicy::Leader).expect("supervise failed"); assert_eq!(sut.success(), expect_success, "exit {code} success mismatch"); assert_eq!(sut.code(), Some(code), "exit {code} code mismatch"); } } } rizsotto-Bear-0c30b27/crates/intercept-supervisor/src/wrapper.rs000066400000000000000000000577401527177720300251420ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Wrapper configuration management for Bear interception. //! //! This module provides structures and utilities for managing wrapper executable //! configurations, including serialization and deserialization of executable mappings. /// The filename used for wrapper configuration files. pub const CONFIG_FILENAME: &str = "wrappers.cfg"; /// The directory name used for wrapper executables in the current working directory. pub(crate) const WRAPPER_DIR_NAME: &str = ".bear"; use std::collections::HashMap; use std::fmt::Display; use std::io::{BufReader, BufWriter, Read, Write}; use std::net::SocketAddr; use std::path::{Path, PathBuf}; use thiserror::Error; /// A managed directory that is created in the current working directory /// and cleaned up when dropped. /// /// Unlike `TempDir`, this uses a deterministic path (`.bear/` in cwd) which /// is essential for autotools-style builds where `configure` caches compiler /// paths that must remain valid across multiple Bear invocations. struct ManagedDirectory { path: PathBuf, } impl ManagedDirectory { /// Creates a new managed directory at `.bear/` in the specified working directory. /// /// If the directory already exists, it is deleted first to ensure a clean state. /// This is important because we need fresh wrapper executables for each Bear run. /// /// # Arguments /// /// * `working_dir` - The directory where `.bear/` will be created (typically from context) pub fn create(working_dir: &Path) -> Result { let path = working_dir.join(WRAPPER_DIR_NAME); // Remove existing directory if present (clean slate for each run) if path.exists() { std::fs::remove_dir_all(&path)?; } // Create fresh directory std::fs::create_dir_all(&path)?; Ok(Self { path }) } /// Returns the path to the managed directory. pub fn path(&self) -> &Path { &self.path } } impl Drop for ManagedDirectory { fn drop(&mut self) { // Best-effort cleanup - ignore errors if self.path.exists() { let _ = std::fs::remove_dir_all(&self.path); } } } /// Configuration structure for wrapper executables. /// /// This structure contains the mapping from wrapper executable names to their /// corresponding real executable paths, as well as the address of the collector /// where intercepted events should be reported. #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct WrapperConfig { /// Map from wrapper executable name to the real executable path pub executables: HashMap, /// The address of the collector to report intercepted events to pub collector_address: SocketAddr, } impl WrapperConfig { /// Creates a new wrapper configuration with the specified collector address. pub fn new(collector_address: SocketAddr) -> Self { Self { executables: HashMap::new(), collector_address } } /// Adds an executable mapping to the configuration. /// /// On Windows, the key is normalized to lowercase with `.exe` stripped, /// so that lookup is case-insensitive and extension-agnostic. pub(crate) fn add_executable(&mut self, name: String, path: PathBuf) { self.executables.insert(normalize_executable_key(&name), path); } /// Gets the real executable path for a given wrapper name. /// /// The lookup key is normalized the same way as in `add_executable`, /// so on Windows "cl", "cl.exe", and "CL.EXE" all match. pub fn get_executable(&self, name: &str) -> Option<&PathBuf> { self.executables.get(&normalize_executable_key(name)) } } /// Normalizes an executable name for use as a HashMap key. /// /// On Windows, executable lookup is case-insensitive and the `.exe` extension /// is optional. This function lowercases the name and strips `.exe` so that /// "cl", "cl.exe", and "CL.EXE" all produce the same key. /// /// On non-Windows platforms, the name is returned unchanged. fn normalize_executable_key(name: &str) -> String { #[cfg(windows)] { let lower = name.to_ascii_lowercase(); lower.strip_suffix(".exe").unwrap_or(&lower).to_string() } #[cfg(not(windows))] { name.to_string() } } impl Display for WrapperConfig { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { writeln!(f, "Collector address: {}", self.collector_address)?; if self.executables.is_empty() { write!(f, "No executables configured") } else { for (name, path) in self.executables.iter() { writeln!(f, " {} -> {}", name, path.display())?; } Ok(()) } } } /// Reader for wrapper configurations. /// /// Handles deserialization of wrapper configurations from various input sources. pub struct WrapperConfigReader; impl WrapperConfigReader { /// Reads wrapper configuration from any source implementing `Read`. pub fn read(reader: R) -> Result { let buf_reader = BufReader::new(reader); serde_json::from_reader(buf_reader).map_err(ConfigError::Json) } /// Reads wrapper configuration from a file path. pub fn read_from_file>(path: P) -> Result { let file = std::fs::File::open(path.as_ref()).map_err(ConfigError::Io)?; Self::read(file) } } /// Writer for wrapper configurations. /// /// Handles serialization of wrapper configurations to various output destinations. struct WrapperConfigWriter; impl WrapperConfigWriter { /// Writes wrapper configuration to any destination implementing `Write`. pub fn write(config: &WrapperConfig, writer: W) -> Result<(), ConfigError> { let buf_writer = BufWriter::new(writer); serde_json::to_writer_pretty(buf_writer, config).map_err(ConfigError::Json) } /// Writes wrapper configuration to a file path. pub fn write_to_file>(config: &WrapperConfig, path: P) -> Result<(), ConfigError> { let file = std::fs::File::create(path.as_ref()).map_err(ConfigError::Io)?; Self::write(config, file) } } /// Builder for creating wrapper directories with executable links and configuration. /// /// This type acts as a builder that collects executable mappings and creates /// the necessary directory structure, hard links (or file copies on Windows), and configuration file. /// It owns and manages the wrapper directory (`.bear/` in the current working directory). /// /// The use of a deterministic directory path is essential for autotools-style builds /// where `./configure` caches compiler paths. Using a random temporary directory /// would break subsequent `make` invocations because the cached paths would no longer exist. pub struct WrapperDirectoryBuilder { wrapper_executable_path: PathBuf, wrapper_dir: ManagedDirectory, config: WrapperConfig, } impl WrapperDirectoryBuilder { /// Creates a wrapper directory at `.bear/` in the specified working directory. /// /// This creates a deterministic directory path that persists across Bear invocations, /// which is essential for autotools-style builds. The directory is cleaned up when /// Bear exits, but the path remains consistent so cached compiler paths remain valid. /// /// # Arguments /// /// * `wrapper_executable_path` - Path to the wrapper executable to link/copy /// * `working_dir` - The directory where `.bear/` will be created (typically `context.current_directory`) /// * `collector_address` - The address of the collector to report intercepted events to pub fn create( wrapper_executable_path: &Path, working_dir: &Path, collector_address: SocketAddr, ) -> Result { let wrapper_dir = ManagedDirectory::create(working_dir).map_err(WrapperDirectoryError::DirCreation)?; Ok(Self { wrapper_executable_path: wrapper_executable_path.to_path_buf(), wrapper_dir, config: WrapperConfig::new(collector_address), }) } /// Registers an executable to be wrapped. /// /// This method checks for filename uniqueness and creates the hard link (or file copy on Windows) immediately. /// Returns the path to the wrapper executable on success, or an error if link creation fails. pub fn register_executable( &mut self, executable_path: PathBuf, ) -> Result { let executable_name = executable_path .file_name() .ok_or_else(|| WrapperDirectoryError::InvalidExecutablePath(executable_path.clone()))? .to_string_lossy() .to_string(); // Check for filename uniqueness (uses normalized key on Windows) if self.config.get_executable(&executable_name).is_some() { // Same executable is already registered, return existing wrapper path. // Use the original executable_name (not normalized) to match the file on disk. return Ok(self.wrapper_dir.path().join(&executable_name)); } // Create hard link immediately (or copy on Windows/when hard link fails) let wrapper_path = self.wrapper_dir.path().join(&executable_name); #[cfg(windows)] std::fs::copy(&self.wrapper_executable_path, &wrapper_path) .map(|_| ()) .map_err(WrapperDirectoryError::LinkCreation)?; #[cfg(not(windows))] // Try hard link first, fall back to copy if it fails (e.g., in containers with overlay fs) if let Err(hard_link_error) = std::fs::hard_link(&self.wrapper_executable_path, &wrapper_path) { log::debug!("Hard link failed ({}), falling back to copy", hard_link_error); std::fs::copy(&self.wrapper_executable_path, &wrapper_path) .map(|_| ()) .map_err(WrapperDirectoryError::LinkCreation)?; } // Register the mapping self.config.add_executable(executable_name.clone(), executable_path); Ok(wrapper_path) } /// Finalizes the wrapper directory by writing the configuration file and returns an immutable WrapperDirectory. pub fn build(self) -> Result { let config_path = self.wrapper_dir.path().join(CONFIG_FILENAME); WrapperConfigWriter::write_to_file(&self.config, &config_path) .map_err(WrapperDirectoryError::ConfigWrite)?; let wrapper_directory = WrapperDirectory { wrapper_dir: self.wrapper_dir, _config: self.config }; log::info!("{}", wrapper_directory); Ok(wrapper_directory) } } /// Immutable wrapper directory after build. pub struct WrapperDirectory { wrapper_dir: ManagedDirectory, _config: WrapperConfig, } impl WrapperDirectory { /// Gets the path to the wrapper directory. pub fn path(&self) -> &Path { self.wrapper_dir.path() } /// Gets the wrapper config. /// /// Used by tests in this crate and in `bear`. Not `#[cfg(test)]`-gated /// because the gate would only apply when `intercept` itself is compiled /// for test, leaving it unavailable to `bear`'s tests across the crate /// boundary. pub fn config(&self) -> &WrapperConfig { &self._config } } impl Display for WrapperDirectory { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { writeln!(f, "Set up wrapper directory: {}", self.wrapper_dir.path().display())?; if self._config.executables.is_empty() { write!(f, " No wrappers configured") } else { writeln!(f, " Set up wrappers as:")?; for (name, path) in self._config.executables.iter() { writeln!(f, " {} -> {}", name, path.display())?; } Ok(()) } } } /// Errors that can occur during wrapper directory operations. #[derive(Error, Debug)] pub enum WrapperDirectoryError { #[error("Invalid executable path: {0}")] InvalidExecutablePath(PathBuf), #[error("Failed to create wrapper executable: {0}")] LinkCreation(#[from] std::io::Error), #[error("Failed to write configuration file: {0}")] ConfigWrite(#[from] ConfigError), #[error("Failed to create wrapper directory: {0}")] DirCreation(std::io::Error), } /// Errors that can occur during wrapper configuration operations. #[derive(Error, Debug)] pub enum ConfigError { #[error("I/O error: {0}")] Io(#[from] std::io::Error), #[error("JSON error: {0}")] Json(#[from] serde_json::Error), } #[cfg(test)] mod tests { use super::*; use std::io::Cursor; use std::net::{IpAddr, Ipv4Addr}; use tempfile::TempDir; fn address() -> SocketAddr { SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080) } #[test] fn test_wrapper_config_new() { let config = WrapperConfig::new(address()); assert!(config.executables.is_empty()); assert_eq!(config.collector_address, address()); } #[test] fn test_wrapper_config_add_executable() { let config = { let mut builder = WrapperConfig::new(address()); builder.add_executable("gcc".to_string(), PathBuf::from("/usr/bin/gcc")); builder }; assert_eq!(config.executables.len(), 1); assert_eq!(config.get_executable("gcc"), Some(&PathBuf::from("/usr/bin/gcc"))); } #[test] fn test_wrapper_config_get_executable() { let config = { let mut builder = WrapperConfig::new(address()); builder.add_executable("gcc".to_string(), PathBuf::from("/usr/bin/gcc")); builder.add_executable("g++".to_string(), PathBuf::from("/usr/bin/g++")); builder }; assert_eq!(config.get_executable("gcc"), Some(&PathBuf::from("/usr/bin/gcc"))); assert_eq!(config.get_executable("g++"), Some(&PathBuf::from("/usr/bin/g++"))); assert_eq!(config.get_executable("clang"), None); } // U5: normalize_key lowercases and strips .exe on Windows // Requirements: interception-wrapper-mechanism #[cfg(windows)] #[test] fn test_wrapper_config_get_executable_windows_case_insensitive() { let config = { let mut builder = WrapperConfig::new(address()); builder.add_executable("cl.exe".to_string(), PathBuf::from(r"C:\VS\cl.exe")); builder }; // All casing/extension variants should resolve to the same entry assert_eq!(config.get_executable("cl"), Some(&PathBuf::from(r"C:\VS\cl.exe"))); assert_eq!(config.get_executable("cl.exe"), Some(&PathBuf::from(r"C:\VS\cl.exe"))); assert_eq!(config.get_executable("cl.EXE"), Some(&PathBuf::from(r"C:\VS\cl.exe"))); assert_eq!(config.get_executable("CL.EXE"), Some(&PathBuf::from(r"C:\VS\cl.exe"))); } // U6: normalize_key is identity on non-Windows (exact match only) #[cfg(not(windows))] #[test] fn test_wrapper_config_get_executable_unix_exact_match_only() { let config = { let mut builder = WrapperConfig::new(address()); builder.add_executable("gcc".to_string(), PathBuf::from("/usr/bin/gcc")); builder }; assert_eq!(config.get_executable("gcc"), Some(&PathBuf::from("/usr/bin/gcc"))); assert_eq!(config.get_executable("GCC"), None); assert_eq!(config.get_executable("gcc.exe"), None); } // U10: Duplicate detection works across normalized keys on Windows #[cfg(windows)] #[test] fn test_wrapper_config_duplicate_detection_across_normalized_keys() { let temp_dir = TempDir::new().unwrap(); let wrapper_path = { let file = temp_dir.path().join("wrapper.exe"); std::fs::write(&file, "wrapper").unwrap(); file }; let mut builder = WrapperDirectoryBuilder::create(&wrapper_path, temp_dir.path(), address()).unwrap(); // Register "cl.exe" builder.register_executable(PathBuf::from(r"C:\VS\cl.exe")).unwrap(); // Register "cl.EXE" - should be treated as duplicate after normalization builder.register_executable(PathBuf::from(r"C:\Other\cl.EXE")).unwrap(); // Only one entry should exist in the config (de-duped via normalized key) let config = builder.build().unwrap(); assert_eq!(config.config().executables.len(), 1, "cl.EXE should be de-duped against cl.exe"); // The stored path should be from the first registration assert_eq!(config.config().get_executable("cl"), Some(&PathBuf::from(r"C:\VS\cl.exe"))); } #[test] fn test_wrapper_config_reader_write_roundtrip() { let original_config = { let mut builder = WrapperConfig::new(address()); builder.add_executable("gcc".to_string(), PathBuf::from("/usr/bin/gcc")); builder.add_executable("g++".to_string(), PathBuf::from("/usr/bin/g++")); builder }; // Write to memory buffer let buffer = { let mut writer = Vec::new(); WrapperConfigWriter::write(&original_config, &mut writer).unwrap(); writer }; // Read back from memory buffer let read_config = { let cursor = Cursor::new(buffer); WrapperConfigReader::read(cursor).unwrap() }; assert_eq!(original_config, read_config); } #[test] fn test_wrapper_config_file_operations() { let original_config = { let mut builder = WrapperConfig::new(address()); builder.add_executable("gcc".to_string(), PathBuf::from("/usr/bin/gcc")); builder.add_executable("clang".to_string(), PathBuf::from("/usr/bin/clang")); builder }; let temp_dir = TempDir::new().unwrap(); let config_path = temp_dir.path().join(CONFIG_FILENAME); // Write to file WrapperConfigWriter::write_to_file(&original_config, &config_path).unwrap(); assert!(config_path.exists()); // Read from file let read_config = WrapperConfigReader::read_from_file(&config_path).unwrap(); assert_eq!(original_config, read_config); } #[test] fn test_wrapper_config_reader_invalid_json() { let invalid_json = b"{ invalid json }"; let cursor = Cursor::new(invalid_json); let result = WrapperConfigReader::read(cursor); assert!(result.is_err()); assert!(matches!(result.unwrap_err(), ConfigError::Json(_))); } #[test] fn test_wrapper_config_reader_missing_file() { let temp_dir = TempDir::new().unwrap(); let nonexistent_path = temp_dir.path().join("nonexistent.json"); let result = WrapperConfigReader::read_from_file(&nonexistent_path); assert!(result.is_err()); assert!(matches!(result.unwrap_err(), ConfigError::Io(_))); } #[test] fn test_managed_directory_creation() { let temp_dir = TempDir::new().unwrap(); let managed_dir = ManagedDirectory::create(temp_dir.path()).unwrap(); // Verify directory exists assert!(managed_dir.path().exists()); assert!(managed_dir.path().ends_with(WRAPPER_DIR_NAME)); } #[test] fn test_managed_directory_cleanup_on_drop() { let temp_dir = TempDir::new().unwrap(); let path; { let managed_dir = ManagedDirectory::create(temp_dir.path()).unwrap(); path = managed_dir.path().to_path_buf(); assert!(path.exists()); } // After drop, directory should be cleaned up assert!(!path.exists()); } #[test] fn test_managed_directory_replaces_existing() { let temp_dir = TempDir::new().unwrap(); // Create first managed directory and add a file let managed_dir1 = ManagedDirectory::create(temp_dir.path()).unwrap(); let test_file = managed_dir1.path().join("test_file.txt"); std::fs::write(&test_file, "test content").unwrap(); assert!(test_file.exists()); // Drop the first directory (but pretend we didn't clean up) let path = managed_dir1.path().to_path_buf(); std::mem::forget(managed_dir1); // Simulate crash - don't run drop // Verify the directory and file still exist (since we skipped drop) assert!(path.exists()); assert!(test_file.exists()); // Create second managed directory in the SAME working directory - should replace existing let managed_dir2 = ManagedDirectory::create(temp_dir.path()).unwrap(); assert_eq!(managed_dir2.path(), path); // Old file should be gone (directory was replaced with a fresh one) assert!(!test_file.exists()); // But the directory itself should exist (freshly created) assert!(managed_dir2.path().exists()); } #[test] fn test_wrapper_directory_builder() { let temp_dir = TempDir::new().unwrap(); let wrapper_dir = { let wrapper_path = { let file = temp_dir.path().join("wrapper"); std::fs::write(&file, "#!/bin/sh\necho wrapper").unwrap(); file }; let mut builder = WrapperDirectoryBuilder::create(&wrapper_path, temp_dir.path(), address()).unwrap(); builder.register_executable(PathBuf::from("/usr/bin/gcc")).unwrap(); builder.register_executable(PathBuf::from("/usr/bin/g++")).unwrap(); builder.build().unwrap() }; // Verify links were created assert!(wrapper_dir.path().join("gcc").exists()); assert!(wrapper_dir.path().join("g++").exists()); // Verify wrapper directory is in .bear assert!(wrapper_dir.path().ends_with(WRAPPER_DIR_NAME)); // Finalize and check config file let config_path = wrapper_dir.path().join(CONFIG_FILENAME); assert!(config_path.exists(), "Config file should exist at {:?}", config_path); assert_eq!(wrapper_dir.config().executables.len(), 2); assert!(wrapper_dir.config().get_executable("gcc").is_some()); assert!(wrapper_dir.config().get_executable("g++").is_some()); // Reading back the config file content, is matching with the config attribute let wrapper_config = WrapperConfigReader::read_from_file(&config_path).unwrap(); assert_eq!(wrapper_config, wrapper_dir.config().clone()); } #[test] fn test_wrapper_directory_duplicate_names() { let temp_dir = TempDir::new().unwrap(); let wrapper_path = { let file = temp_dir.path().join("wrapper"); std::fs::write(&file, "#!/bin/sh\necho wrapper").unwrap(); file }; let mut builder = WrapperDirectoryBuilder::create(&wrapper_path, temp_dir.path(), address()).unwrap(); // Register first executable let gcc_wrapper = builder.register_executable(PathBuf::from("/usr/bin/gcc")).unwrap(); // Try to register different executable with same name - should fail let result = builder.register_executable(PathBuf::from("/usr/local/bin/gcc")); assert!(result.is_ok()); assert_eq!(result.unwrap(), gcc_wrapper); } #[test] fn test_wrapper_directory_same_executable_twice() { let temp_dir = TempDir::new().unwrap(); let wrapper_path = { let file = temp_dir.path().join("wrapper"); std::fs::write(&file, "#!/bin/sh\necho wrapper").unwrap(); file }; let mut builder = WrapperDirectoryBuilder::create(&wrapper_path, temp_dir.path(), address()).unwrap(); // Register same executable twice - should be OK let gcc_path = PathBuf::from("/usr/bin/gcc"); let wrapper_path1 = builder.register_executable(gcc_path.clone()).unwrap(); let wrapper_path2 = builder.register_executable(gcc_path).unwrap(); // Should not error assert_eq!(wrapper_path1, wrapper_path2); // Should return same wrapper path let wrapper_dir = builder.build().unwrap(); assert_eq!(wrapper_dir.config().executables.len(), 1); // Should only have one entry } } rizsotto-Bear-0c30b27/crates/intercept/000077500000000000000000000000001527177720300200715ustar00rootroot00000000000000rizsotto-Bear-0c30b27/crates/intercept/Cargo.toml000066400000000000000000000014171527177720300220240ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later [package] name = "intercept" description = "Shared interception runtime for Bear (reporter, collector, wrapper, environment)." version.workspace = true authors.workspace = true repository.workspace = true homepage.workspace = true license.workspace = true edition.workspace = true rust-version.workspace = true publish = false [lib] name = "intercept" path = "src/lib.rs" [dependencies] thiserror.workspace = true serde.workspace = true serde_json.workspace = true log.workspace = true env_logger.workspace = true [target.'cfg(unix)'.dependencies] libc.workspace = true [build-dependencies] compilers-codegen = { path = "../../build-support/compilers-codegen" } [dev-dependencies] tempfile.workspace = true [lints] workspace = true rizsotto-Bear-0c30b27/crates/intercept/build.rs000066400000000000000000000017341527177720300215430ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later fn main() { // The compiler-specific environment variable names (`COMPILER_ENV_KEYS`) // are generated from the same `compilers/*.yaml` descriptions the // `semantic` crate uses. The agent-side environment filtering in // `src/environment.rs` includes the result via // `include!(OUT_DIR/env_keys.rs)`. let manifest_dir = std::path::PathBuf::from( std::env::var("CARGO_MANIFEST_DIR").expect("cargo sets CARGO_MANIFEST_DIR for build scripts"), ); let flags_dir = manifest_dir.join("../semantic/compilers"); let out_dir: std::path::PathBuf = std::env::var("OUT_DIR").expect("cargo sets OUT_DIR for build scripts").into(); if let Err(e) = compilers_codegen::generate_env_keys_only(&flags_dir, &out_dir) { eprintln!("error: env-key codegen failed"); for cause in e.chain() { eprintln!(" caused by: {}", cause); } std::process::exit(1); } } rizsotto-Bear-0c30b27/crates/intercept/src/000077500000000000000000000000001527177720300206605ustar00rootroot00000000000000rizsotto-Bear-0c30b27/crates/intercept/src/environment.rs000066400000000000000000000243031527177720300235740ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later use std::env::JoinPathsError; use std::path::{Path, PathBuf}; pub const KEY_INTERCEPT_STATE: &str = "BEAR_INTERCEPT"; // man page for `ld.so` (Linux dynamic linker/loader) pub const KEY_OS_PRELOAD_PATH: &str = "LD_PRELOAD"; // man page for `dyld` (macOS dynamic linker) pub const KEY_OS_MACOS_PRELOAD_PATH: &str = "DYLD_INSERT_LIBRARIES"; pub const KEY_OS_MACOS_FLAT_NAMESPACE: &str = "DYLD_FORCE_FLAT_NAMESPACE"; // man page for `exec` (Linux system call) pub const KEY_OS_PATH: &str = "PATH"; // Compiler-specific environment variable names, generated from compilers/*.yaml. include!(concat!(env!("OUT_DIR"), "/env_keys.rs")); // These run in every intercepted process, the preload library included, so // they must not allocate. // https://www.gnu.org/software/make/manual/html_node/Implicit-Variables.html // Only the program variables. The `*FLAGS` variables are not kept: compilers // never read them, make expands them into the command line. const fn make_program_key(key: &str) -> bool { matches!( key.as_bytes(), b"CC" | b"CXX" | b"CPP" | b"FC" | b"AR" | b"AS" | b"M2C" | b"PC" | b"LEX" | b"YACC" | b"LINT" ) } // https://cmake.org/cmake/help/latest/manual/cmake-env-variables.7.html#environment-variables-for-languages // https://cmake.org/cmake/help/latest/envvar/ASM_DIALECT.html const fn cmake_program_key(key: &str) -> bool { matches!( key.as_bytes(), b"CUDACXX" | b"CUDAHOSTCXX" | b"HIPCXX" | b"HIPHOSTCXX" | b"ISPC" | b"OBJC" | b"OBJCXX" | b"RC" | b"SWIFTC" | b"SYCLCXX" | b"ASM" | b"ASM_NASM" | b"ASM_MASM" | b"ASM_MARMASM" | b"ASM_POASM" | b"ASM-ATT" ) } // Windows PATH variable is case sensitive and not always capitalized const fn path_key(key: &str) -> bool { key.eq_ignore_ascii_case(KEY_OS_PATH) } pub fn relevant_env(key: &str) -> bool { matches!( key, KEY_INTERCEPT_STATE | KEY_OS_PRELOAD_PATH | KEY_OS_MACOS_PRELOAD_PATH | KEY_OS_MACOS_FLAT_NAMESPACE ) || program_env(key) || COMPILER_ENV_KEYS.contains(&key) || path_key(key) } pub const fn program_env(key: &str) -> bool { make_program_key(key) || cmake_program_key(key) } /// Manipulates a `PATH`-like environment variable by inserting a path at the beginning. /// /// This function ensures that the specified path appears first in a colon-separated /// list of paths (like `PATH` or `LD_PRELOAD`). If the path already exists elsewhere /// in the list, it is removed from its current position and moved to the front. /// This guarantees that the specified path takes precedence over other paths. /// /// # Arguments /// /// * `original` - The original PATH-like environment variable value /// * `first` - The path to insert at the beginning of the path list /// /// # Returns /// /// Returns `Ok(String)` containing the updated path list, or `Err(JoinPathsError)` /// if path manipulation fails due to invalid characters or platform limitations. /// /// # Behavior /// /// - If `original` is empty, returns just the `first` path /// - If `first` already exists in `original`, it's moved to the front /// - If `first` doesn't exist, it's prepended to the existing paths /// - Uses platform-appropriate path separators and handles path encoding pub fn insert_to_path>(original: &str, first: P) -> Result { let first_path = first.as_ref(); if original.is_empty() { return Ok(first_path.to_string_lossy().to_string()); } let mut paths: Vec = std::env::split_paths(original).filter(|path| path.as_path() != first_path).collect(); paths.insert(0, first_path.to_owned()); std::env::join_paths(paths).map(|os_string| os_string.into_string().unwrap_or_default()) } #[cfg(test)] mod tests { use super::*; use std::path::PathBuf; /// Assert that the first entry in a path-like string equals the expected value. /// Works for PATH, LD_PRELOAD, or any path-separated environment variable. fn assert_first_path_entry(expected: &str, path_like: &str) { let path_entries: Vec = std::env::split_paths(path_like).map(|p| p.to_string_lossy().to_string()).collect(); let first_entry = path_entries.first().expect("Path-like string should not be empty"); assert_eq!( first_entry, expected, "First path entry should match expected. First entry: {}, expected: {}", first_entry, expected ); } fn assert_path_entry(expected: &str, path_like: &str) { let path_entries: Vec = std::env::split_paths(path_like).map(|p| p.to_string_lossy().to_string()).collect(); assert!( path_entries.contains(&expected.to_string()), "Path entry should contain expected. Path entries: {:?}, expected: {}", path_entries, expected ); } // Requirements: interception-wrapper-mechanism #[test] fn program_env_accepts_only_make_and_cmake_program_keys() { let cases = [ ("CC", true), ("CXX", true), ("LINT", true), ("CUDACXX", true), ("OBJCXX", true), ("SWIFTC", true), ("ASM", true), ("ASM_NASM", true), ("ASM-ATT", true), ("CFLAGS", false), ("LDFLAGS", false), ("CUDAFLAGS", false), ("CMAKE_C_COMPILER", false), ("CARGO", false), ("RUSTC", false), ("RUSTC_WRAPPER", false), ("RUSTFLAGS", false), ("cc", false), ("PATH", false), ]; for (key, expected) in cases { let sut = program_env(key); assert_eq!(sut, expected, "program_env({key:?})"); } } // Requirements: interception-wrapper-mechanism #[test] fn relevant_env_keeps_program_path_and_compiler_keys_only() { let cases = [ ("CC", true), ("RC", true), ("ASM_MARMASM", true), ("BEAR_INTERCEPT", true), ("LD_PRELOAD", true), ("CPATH", true), ("PATH", true), ("Path", true), ("path", true), ("CFLAGS", false), ("CXXFLAGS", false), ("LDLIBS", false), ("CARGO", false), ("RUSTC", false), ("RUSTC_WRAPPER", false), ("RUSTFLAGS", false), ("HOME", false), ]; for (key, expected) in cases { let sut = relevant_env(key); assert_eq!(sut, expected, "relevant_env({key:?})"); } } #[test] fn test_insert_to_path_empty_original() { let original = ""; let first = PathBuf::from("/usr/local/bin"); let result = insert_to_path(original, first.clone()).unwrap(); // For empty path case, we just return the path as a string assert_first_path_entry(&first.to_string_lossy(), &result); } #[test] fn test_insert_to_path_prepend_new() { let bin = PathBuf::from("/bin"); let usr_bin = PathBuf::from("/usr/bin"); let usr_local_bin = PathBuf::from("/usr/local/bin"); // Join the original paths using platform-specific separator let original = std::env::join_paths([usr_bin.clone(), bin.clone()]).unwrap().to_string_lossy().to_string(); // Apply our function let result = insert_to_path(&original, usr_local_bin.clone()).unwrap(); // Check that the new path is first assert_first_path_entry(&usr_local_bin.to_string_lossy(), &result); assert_path_entry(&bin.to_string_lossy(), &result); assert_path_entry(&usr_bin.to_string_lossy(), &result); } #[test] fn test_insert_to_path_move_existing_to_front() { let bin = PathBuf::from("/bin"); let usr_bin = PathBuf::from("/usr/bin"); let usr_local_bin = PathBuf::from("/usr/local/bin"); // Join the original paths using platform-specific separator let original = std::env::join_paths([usr_bin.clone(), usr_local_bin.clone(), bin.clone()]) .unwrap() .to_string_lossy() .to_string(); // Apply our function let result = insert_to_path(&original, usr_local_bin.clone()).unwrap(); // Check that the existing path was moved to front assert_first_path_entry(&usr_local_bin.to_string_lossy(), &result); assert_path_entry(&bin.to_string_lossy(), &result); assert_path_entry(&usr_bin.to_string_lossy(), &result); } #[test] fn test_insert_to_path_already_first() { let bin = PathBuf::from("/bin"); let usr_bin = PathBuf::from("/usr/bin"); let usr_local_bin = PathBuf::from("/usr/local/bin"); // Join the original paths using platform-specific separator let original = std::env::join_paths([usr_local_bin.clone(), usr_bin.clone(), bin.clone()]) .unwrap() .to_string_lossy() .to_string(); // Apply our function let result = insert_to_path(&original, usr_local_bin.clone()).unwrap(); // Check that the path is still first (no change needed) assert_first_path_entry(&usr_local_bin.to_string_lossy(), &result); assert_path_entry(&bin.to_string_lossy(), &result); assert_path_entry(&usr_bin.to_string_lossy(), &result); } #[cfg(target_os = "windows")] #[test] fn test_insert_to_path_windows_mingw_preservation() { // Test the exact Windows CI failure scenario - MinGW PATH preservation let original = "C:\\mingw64\\bin;C:\\Windows\\System32;C:\\Program Files\\Git\\bin"; let wrapper_dir = "C:\\Users\\RUNNER~1\\AppData\\Local\\Temp\\bear-xyz"; let first = PathBuf::from(wrapper_dir); let result = insert_to_path(original, first).unwrap(); // Wrapper should be first in PATH assert_first_path_entry(wrapper_dir, &result); assert_path_entry("C:\\mingw64\\bin", &result); assert_path_entry("C:\\Windows\\System32", &result); assert_path_entry("C:\\Program Files\\Git\\bin", &result); } } rizsotto-Bear-0c30b27/crates/intercept/src/lib.rs000066400000000000000000000164221527177720300220010ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! The module contains the intercept reporting and collecting functionality. //! //! When a command execution is intercepted, the interceptor sends the event to the collector. //! This happens in two different processes, requiring a communication channel between these //! processes. //! //! The module provides abstractions for the reporter and the collector. And it also defines //! the data structures that are used to represent the events. It also hosts the shared //! logging initializer (`logging`) that every Bear binary uses for a consistent diagnostic //! format. //! //! # How the executable path is captured //! //! The `Execution.executable` field contains the path to the intercepted executable. //! Its value depends on the interception mechanism and the exec-family function used: //! //! ## Wrapper mode //! //! The wrapper binary discovers the real compiler path at build start (via `which`) and //! stores it in a config file. When invoked, it replaces its own path with the real //! compiler's absolute path before reporting. **Result:** always an absolute path. //! //! ## Preload mode (libexec.so) //! //! The preload library intercepts exec-family calls and reports the path argument as-is: //! //! | Function | Parameter | Typical value | Searches PATH? | //! |-------------|-----------|-----------------|----------------| //! | `execve` | `path` | `/usr/bin/gcc` | No | //! | `execv` | `path` | `/usr/bin/gcc` | No | //! | `execvpe` | `file` | `gcc` | Yes | //! | `execvp` | `file` | `gcc` | Yes | //! | `execlp` | `file` | `gcc` | Yes | //! | `posix_spawn` | `path` | `/usr/bin/gcc` | No | //! | `posix_spawnp` | `file` | `gcc` | Yes | //! //! The `p`-variant functions accept bare filenames; the preload shim reports them //! as-is (not resolved). **Result:** absolute path or bare filename. //! //! ## Normalization //! //! None. The executable travels through semantic analysis and into the //! compilation database exactly as reported here; bare filenames stay bare, //! and their resolution is left to the consumer (see the //! `output-json-compilation-database` requirement). pub mod environment; pub mod logging; pub mod reporter; pub mod state; pub mod tcp; use crate::environment::relevant_env; use std::collections::HashMap; use std::fmt; use std::path::{Path, PathBuf}; use thiserror::Error; /// Execution is a representation of a process execution. /// /// It does not contain information about the outcome of the execution, /// like the exit code or the duration of the execution. It only contains /// the information that is necessary to reproduce the execution. /// /// # Fields /// - `executable`: The path to the executable that was run. /// - `arguments`: The command line arguments that were passed to the executable. /// Includes the executable itself as the first argument. /// - `working_dir`: The current working directory of the process. /// - `environment`: The environment variables that were set for the process. #[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] pub struct Execution { pub executable: PathBuf, pub arguments: Vec, pub working_dir: PathBuf, pub environment: HashMap, } impl Execution { /// Captures the execution information of the current process. /// /// This method retrieves the executable path, command-line arguments, /// current working directory, and environment variables of the process. /// /// **Security Note**: This method captures ALL environment variables from /// the current process, which may include sensitive information. Consider /// using the `trim()` method to filter to only relevant environment variables. pub fn capture() -> Result { let executable = std::env::current_exe().map_err(CaptureError::CurrentExecutable)?; let arguments = std::env::args().collect(); let working_dir = std::env::current_dir().map_err(CaptureError::CurrentDirectory)?; let environment = std::env::vars().collect(); Ok(Self { executable, arguments, working_dir, environment }) } pub fn with_executable(self, executable: &Path) -> Self { Self { executable: executable.to_path_buf(), ..self } } /// Trims the execution information to only contain relevant environment variables. pub fn trim(self) -> Self { let environment = self.environment.into_iter().filter(|(k, _)| relevant_env(k)).collect(); Self { environment, ..self } } /// Builds an `Execution` from string slices. /// /// A convenience constructor used by tests in this crate and in `bear`. /// It is not `#[cfg(test)]`-gated because the gate would only apply when /// `intercept` itself is compiled for test, leaving it unavailable to /// `bear`'s tests across the crate boundary. pub fn from_strings( executable: &str, arguments: Vec<&str>, working_dir: &str, environment: HashMap<&str, &str>, ) -> Self { Self { executable: PathBuf::from(executable), arguments: arguments.iter().map(|s| s.to_string()).collect(), working_dir: PathBuf::from(working_dir), environment: environment.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(), } } } impl fmt::Display for Execution { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Execution path={}, args=[{}]", self.executable.display(), self.arguments.join(",")) } } /// Represents errors that can occur while capturing the execution information. #[derive(Error, Debug)] pub enum CaptureError { #[error("Failed to capture execution: {0}")] CurrentExecutable(std::io::Error), #[error("Failed to capture current directory: {0}")] CurrentDirectory(std::io::Error), } #[cfg(test)] mod tests { use super::*; #[test] fn test_execution_trim() { // Create an execution with both relevant and irrelevant environment variables let environment = { let mut builder = HashMap::new(); builder.insert("PATH".to_string(), "/usr/bin:/bin".to_string()); builder.insert("CC".to_string(), "gcc".to_string()); builder.insert("IRRELEVANT_VAR".to_string(), "value".to_string()); builder.insert("HOME".to_string(), "/home/user".to_string()); builder }; let execution = Execution { executable: PathBuf::from("/usr/bin/gcc"), arguments: vec!["/usr/bin/gcc".to_string(), "-c".to_string(), "test.c".to_string()], working_dir: PathBuf::from("/tmp"), environment, }; let trimmed = execution.trim(); // All environment variables in the trimmed execution should be relevant for key in trimmed.environment.keys() { assert!(relevant_env(key), "Non-relevant env var found after trim: {}", key); } // Other fields should remain unchanged assert_eq!(trimmed.executable, PathBuf::from("/usr/bin/gcc")); assert_eq!(trimmed.arguments, vec!["/usr/bin/gcc", "-c", "test.c"]); assert_eq!(trimmed.working_dir, PathBuf::from("/tmp")); } } rizsotto-Bear-0c30b27/crates/intercept/src/logging.rs000066400000000000000000000151131527177720300226550ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Shared logging initialization for Bear's binaries. //! //! Bear's log output has two audiences, so this initializer picks a format //! from the environment: //! //! - **User** (default, `RUST_LOG` unset): UNIX-style `prog: message` //! diagnostics on stderr, matching `cc`, `git`, and `make`. The default //! filter is `warn`, so a user sees only warnings and errors, prefixed //! `prog: warning:` / `prog: error:`. //! - **Developer** (`RUST_LOG` set): a rich trace carrying timestamp, level, //! the **process** identity (name + pid), and the source module, so lines //! from the driver, wrapper, and preload processes can be told apart even //! when they interleave on the same terminal. //! //! `program` is the process identity: `bear`, `wrapper`, `preload`. All //! binaries route through [`init`]; initialization is idempotent so the //! preload library may call it more than once without panicking. use std::fmt; use std::io; use std::time::{SystemTime, UNIX_EPOCH}; /// Initialize the process-wide logger. /// /// `program` names the process for diagnostics (see the module docs). /// Idempotent: a second call is a no-op rather than a panic. pub fn init(program: &str) { // An empty `RUST_LOG` counts as unset: it would otherwise select the // developer view while `env_logger` parses "" down to an `error` // threshold, i.e. quieter than the user view's `warn` default. let developer = std::env::var_os("RUST_LOG").is_some_and(|value| !value.is_empty()); let program = program.to_string(); let pid = std::process::id(); let mut builder = env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")); if developer { builder.format(move |buf, record| { write_developer(buf, ×tamp(), &program, pid, record.level(), record.target(), record.args()) }); } else { builder.format(move |buf, record| write_user(buf, &program, record.level(), record.args())); } // `try_init`, not `init`: this runs from a library constructor inside // processes Bear does not control (any intercepted compiler), so a second // registration must be a silent no-op rather than a panic. let _ = builder.try_init(); } /// User format: `prog: message`, with a severity qualifier for the two levels /// a user sees by default. fn write_user( out: &mut impl io::Write, program: &str, level: log::Level, message: impl fmt::Display, ) -> io::Result<()> { match level { log::Level::Error => writeln!(out, "{program}: error: {message}"), log::Level::Warn => writeln!(out, "{program}: warning: {message}"), _ => writeln!(out, "{program}: {message}"), } } /// Developer format: timestamp, level, process identity (name + pid), source /// module, then the message. fn write_developer( out: &mut impl io::Write, timestamp: &str, program: &str, pid: u32, level: log::Level, target: &str, message: impl fmt::Display, ) -> io::Result<()> { writeln!(out, "[{timestamp} {level:5} {program}[{pid}] {target}] {message}") } /// UTC `HH:MM:SS.mmm` for the developer format. /// /// `env_logger`'s timestamp support lives behind its `humantime` feature, /// which the workspace disables (`default-features = false`), so the time of /// day is derived here from the epoch directly. It is UTC, not local time; /// this is developer-only diagnostic output where an absolute wall clock is /// not needed. fn timestamp() -> String { let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default(); let secs = now.as_secs(); let ms = now.subsec_millis(); let (h, m, s) = ((secs / 3600) % 24, (secs / 60) % 60, secs % 60); format!("{h:02}:{m:02}:{s:02}.{ms:03}") } #[cfg(test)] mod tests { use super::*; // Requirements: cli-diagnostic-format #[test] fn user_format_prefixes_program_and_qualifies_severity() { // arrange: one message, every level, expected line per level let cases = [ (log::Level::Error, "bear: error: hi\n"), (log::Level::Warn, "bear: warning: hi\n"), (log::Level::Info, "bear: hi\n"), (log::Level::Debug, "bear: hi\n"), (log::Level::Trace, "bear: hi\n"), ]; for (level, expected) in cases { // act let mut buf = Vec::new(); write_user(&mut buf, "bear", level, "hi").unwrap(); let sut = String::from_utf8(buf).unwrap(); // assert assert_eq!(sut, expected, "level {level}"); } } // Requirements: cli-diagnostic-format #[test] fn user_format_carries_the_process_identity() { // arrange / act: a warning from the wrapper process let mut buf = Vec::new(); write_user(&mut buf, "wrapper", log::Level::Warn, "report failed").unwrap(); let sut = String::from_utf8(buf).unwrap(); // assert: the emitting process is named, not the crate assert_eq!(sut, "wrapper: warning: report failed\n"); } // Requirements: cli-diagnostic-format #[test] fn developer_format_tags_process_identity_pid_and_source() { // arrange / act let mut buf = Vec::new(); write_developer( &mut buf, "01:02:03.004", "preload", 4242, log::Level::Debug, "intercept::tcp", "sent", ) .unwrap(); let sut = String::from_utf8(buf).unwrap(); // assert assert_eq!(sut, "[01:02:03.004 DEBUG preload[4242] intercept::tcp] sent\n"); } // Requirements: cli-diagnostic-format #[test] fn developer_format_pads_level_for_column_alignment() { // arrange / act: the shorter level names pad to five columns let mut buf = Vec::new(); write_developer(&mut buf, "01:02:03.004", "bear", 7, log::Level::Info, "bear::modes", "cfg").unwrap(); let sut = String::from_utf8(buf).unwrap(); // assert: "INFO" padded to width 5, then the field separator assert_eq!(sut, "[01:02:03.004 INFO bear[7] bear::modes] cfg\n"); } // Requirements: cli-diagnostic-format #[test] fn format_accepts_deferred_arguments() { // arrange / act: the real call path passes `record.args()`, i.e. // `fmt::Arguments`; make sure that type is accepted, not just `&str` let mut buf = Vec::new(); write_user(&mut buf, "bear", log::Level::Error, format_args!("code {}", 42)).unwrap(); let sut = String::from_utf8(buf).unwrap(); // assert assert_eq!(sut, "bear: error: code 42\n"); } } rizsotto-Bear-0c30b27/crates/intercept/src/reporter.rs000066400000000000000000000032261527177720300230730ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Reporter module for command interception layer. //! //! This module provides abstractions and implementations for reporting intercepted events //! to a remote collector. It defines error types for initialization and reporting failures, //! a trait for reporting events, and a factory for creating reporter instances. //! //! The main responsibilities include: //! - Defining the `Reporter` trait for sending events. //! - Providing error types for initialization and reporting. //! - Implementing a factory to create TCP-based reporters. use crate::{Execution, tcp}; use std::net::SocketAddr; use thiserror::Error; /// Trait for reporting intercepted executions to a remote collector. pub trait Reporter { /// Sends an execution to the remote collector. /// /// The execution is serialized and sent to the remote collector. /// The TCP connection is opened and closed for each execution. fn report(&self, execution: Execution) -> Result<(), ReporterError>; } /// Errors that can occur while reporting events. #[derive(Error, Debug)] pub enum ReporterError { #[error("Serialization error: {0}")] Serialization(#[from] serde_json::Error), #[error("Network error: {0}")] Network(#[from] std::io::Error), } /// Factory for creating reporter instances. pub struct ReporterFactory; impl ReporterFactory { /// Creates a new TCP-based reporter using the destination from the environment. /// /// The created reporter is not connected yet; it only stores the destination address. pub fn create(address: SocketAddr) -> tcp::ReporterOnTcp { tcp::ReporterOnTcp::new(address) } } rizsotto-Bear-0c30b27/crates/intercept/src/state.rs000066400000000000000000000024201527177720300223440ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Preload interception state passed from the supervisor to the preload library. use std::net::SocketAddr; use std::path::PathBuf; /// Represents the state information needed for preload-based interception. /// /// This struct is serialized to JSON and passed to the preloaded library via /// an environment variable. It contains all the information the library needs /// to report execution events back to the Bear process. #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct PreloadState { /// The socket address where execution events should be reported pub destination: SocketAddr, /// The path to the preload library itself pub library: PathBuf, } impl TryInto for PreloadState { type Error = serde_json::Error; fn try_into(self) -> Result { serde_json::to_string(&self) } } impl TryFrom<&str> for PreloadState { type Error = serde_json::Error; fn try_from(value: &str) -> Result { serde_json::from_str(value) } } impl TryFrom for PreloadState { type Error = serde_json::Error; fn try_from(value: String) -> Result { serde_json::from_str(&value) } } rizsotto-Bear-0c30b27/crates/intercept/src/tcp.rs000066400000000000000000000123731527177720300220220ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! The module contains the implementation of the TCP collector and reporter. use crate::Execution; use crate::reporter::{Reporter, ReporterError}; use std::io::{Read, Write}; use std::net::{SocketAddr, TcpStream}; /// The serializer for executions to transmit over the network. /// /// The executions are serialized using LV (Length-Value) format. /// The length is a 4-byte big-endian integer, and the value is the JSON /// representation of the execution. /// /// Both the reporter (sender, in this crate) and the collector (receiver, in /// `intercept-supervisor`) share this wire format, so the type is `pub`. pub struct ExecutionWireSerializer; impl ExecutionWireSerializer { /// Read an execution from a reader using LV format. pub fn read(reader: &mut impl Read) -> Result { let mut length_bytes = [0; 4]; reader.read_exact(&mut length_bytes)?; let length = u32::from_be_bytes(length_bytes) as usize; let mut buffer = vec![0; length]; reader.read_exact(&mut buffer)?; let execution = serde_json::from_slice(buffer.as_ref())?; Ok(execution) } /// Write an execution to a writer using LV format. pub fn write(writer: &mut impl Write, execution: Execution) -> Result { let serialized = serde_json::to_string(&execution)?; let bytes = serialized.into_bytes(); let length = bytes.len() as u32; writer.write_all(&length.to_be_bytes())?; writer.write_all(&bytes)?; Ok(length) } } /// Represents a TCP execution reporter. pub struct ReporterOnTcp { destination: SocketAddr, } impl ReporterOnTcp { /// Creates a new TCP reporter instance. /// /// It does not open the TCP connection yet. Stores the destination /// address and creates a unique reporter id. pub fn new(destination: SocketAddr) -> Self { Self { destination } } } impl Reporter for ReporterOnTcp { /// Sends an execution to the remote collector. /// /// The execution's environment is trimmed to the variables relevant for /// compilation database generation before serialization. /// The TCP connection is opened and closed for each execution. fn report(&self, execution: Execution) -> Result<(), ReporterError> { let execution = execution.trim(); log::debug!("Execution report: {execution:?}"); let mut socket = TcpStream::connect(self.destination).map_err(ReporterError::Network)?; ExecutionWireSerializer::write(&mut socket, execution)?; Ok(()) } } #[cfg(test)] mod tests { use super::*; use std::collections::HashMap; use std::io::Cursor; fn executions() -> Vec { vec![ Execution::from_strings("/usr/bin/ls", vec!["ls", "-l"], "/tmp", HashMap::new()), Execution::from_strings( "/usr/bin/cc", vec!["cc", "-c", "./file_a.c", "-o", "./file_a.o"], "/home/user", HashMap::from([("PATH", "/usr/bin:/bin"), ("CC", "gcc")]), ), Execution::from_strings( "/usr/bin/ld", vec!["ld", "-o", "./file_a", "./file_a.o"], "/opt/project", HashMap::from([("PATH", "/usr/bin:/bin"), ("LD_PRELOAD", "/usr/lib:/lib")]), ), ] } // Test that the serialization and deserialization works. We write the // executions to a buffer and read them back to check if the deserialized // values match the originals. #[test] fn read_write_works() { let executions = executions(); let mut writer = Cursor::new(vec![0; 1024]); for execution in executions.iter() { let result = ExecutionWireSerializer::write(&mut writer, execution.clone()); assert!(result.is_ok()); } let mut reader = Cursor::new(writer.get_ref()); for execution in executions.iter() { let result = ExecutionWireSerializer::read(&mut reader); assert!(result.is_ok()); assert_eq!(result.unwrap(), execution.clone()); } } // Test that the reporter writes a record the wire serializer can read back. // A raw listener stands in for the collector (which lives in // `intercept-supervisor`); the end-to-end reporter/collector test lives // there. #[test] fn reporter_writes_a_readable_record() { let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); let address = listener.local_addr().unwrap(); let sender = std::thread::spawn(move || { let reporter = ReporterOnTcp::new(address); reporter.report(Execution::from_strings( "/usr/bin/cc", vec!["cc", "-c", "file.c"], "/tmp", HashMap::from([("PATH", "/usr/bin:/bin")]), )) }); let (mut connection, _) = listener.accept().unwrap(); let received = ExecutionWireSerializer::read(&mut connection).unwrap(); sender.join().unwrap().expect("report should succeed"); assert_eq!(received.executable, std::path::PathBuf::from("/usr/bin/cc")); assert_eq!(received.arguments, vec!["cc", "-c", "file.c"]); } } rizsotto-Bear-0c30b27/crates/semantic/000077500000000000000000000000001527177720300176775ustar00rootroot00000000000000rizsotto-Bear-0c30b27/crates/semantic/CLAUDE.md000066400000000000000000000036201527177720300211570ustar00rootroot00000000000000## semantic The per-module `//!` docs hold the details; this file lists only the cross-cutting constraints that are easy to violate. ## Dependency boundary (load-bearing) May depend on `intercept`. Must NOT depend on `bear`, and must not grow a dependency on the application layer in any form: no configuration types, no `clap`, no command-line arguments, no serde. A caller's configuration reaches this crate as plain values: a path, an optional `as:` spelling, a boolean. Rationale in `docs/rationale/workspace-crate-layout.md`. Resolution of a configured `as:` spelling onto a `CompilerType` belongs here, next to the generated id data it validates against, not in the caller's schema layer. ## The public API is deliberate `src/lib.rs` states exactly what dependents may touch; everything else is crate-private, including `CompilerType`, `CompilerId`, `CompilerInterpreter`, the interpreter combinators, and `IgnoreByPath`. Widening that surface is a design change, not a mechanical fix: say in the commit body why the dependent cannot be served by what is already exported. Composition of the interpreter chain is a semantic concern and stays behind `interpreters::create`. ## The `testing` feature The `testing` module is gated on `any(test, feature = "testing")`, not on `cfg(test)`. Enable the feature from a dependent's `[dev-dependencies]` only. Do not copy a factory into the dependent. Rationale in `docs/rationale/workspace-crate-layout.md`. ## Compiler definitions are data Recognition patterns, flag tables, the family registry, and the accepted `as:` spellings are all generated from `compilers/*.yaml` by `build.rs`. Read `compilers/CLAUDE.md` before editing YAML (it owns the steps to add a compiler or launcher), and `build-support/compilers-codegen/CLAUDE.md` before changing what is generated. Hand-writing a compiler id as a Rust literal is guarded by `compiler_id_literals_are_known`; keep it that way. rizsotto-Bear-0c30b27/crates/semantic/Cargo.toml000066400000000000000000000020451527177720300216300ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later [package] name = "semantic" description = "Compiler recognition and command line flag parsing for Bear." keywords = ["clang", "clang-tooling", "compilation-database"] version.workspace = true authors.workspace = true repository.workspace = true homepage.workspace = true license.workspace = true edition.workspace = true rust-version.workspace = true publish = false [lib] name = "semantic" path = "src/lib.rs" [features] # Compiles the `testing` module (command factories and comparison helpers) # into the library, so dependents can build their own tests against it. # Off by default, so release builds carry no test factories. testing = [] [dependencies] intercept = { path = "../intercept" } thiserror.workspace = true log.workspace = true shell-words.workspace = true regex-lite.workspace = true libc.workspace = true [build-dependencies] compilers-codegen = { path = "../../build-support/compilers-codegen" } [dev-dependencies] tempfile.workspace = true mockall.workspace = true [lints] workspace = true rizsotto-Bear-0c30b27/crates/semantic/build.rs000066400000000000000000000007521527177720300213500ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later fn main() { let flags_dir = std::path::Path::new("compilers"); let out_dir: std::path::PathBuf = std::env::var("OUT_DIR").expect("cargo sets OUT_DIR for build scripts").into(); if let Err(e) = compilers_codegen::generate(flags_dir, &out_dir) { eprintln!("error: flag codegen failed"); for cause in e.chain() { eprintln!(" caused by: {}", cause); } std::process::exit(1); } } rizsotto-Bear-0c30b27/crates/semantic/compilers/000077500000000000000000000000001527177720300216745ustar00rootroot00000000000000rizsotto-Bear-0c30b27/crates/semantic/compilers/CLAUDE.md000066400000000000000000000115541527177720300231610ustar00rootroot00000000000000## Compiler interpreter definitions Read `README.md` in this directory for full schema documentation (pattern syntax, result values, inheritance, environment variables). ## Two schemas live side by side here `type:` is a closed kind enum. Compiler files (`type: compiler`) declare a family identity in a nested `compiler:` block (`id:`, optional `extends:`). Wrapper files (`type: wrapper` -- the four compiler launchers: ccache, distcc, sccache, icecc) carry no identity block. Codegen discovers both kinds by scanning this directory and peeking `type:` -- there is no hand-maintained file list. See `README.md` for the full schema of both. ## Rules for modifying YAML files - Every compiler-kind YAML file maps to one compiler or compiler family; every wrapper-kind file maps to one compiler launcher - `compilers-codegen` reads these at build time (via `crates/semantic/build.rs`) and generates static Rust arrays - After any edit: `cargo build && cargo test` to validate ## Adding a new compiler Adding a family is a YAML file plus accepting snapshots -- no Rust edit. The directory scan picks up the file, derives every generated name from its stem, and emits the recognition row, the `KNOWN_IDS` entry (so `as: ` is accepted), and the interpreter registration on its own. 1. Create `mycompiler.yaml` in this directory 2. Add `type: compiler`, a `compiler:` block (`id:`, optionally `extends:`), `recognize:`, `flags:` entries (optionally `ignore_when:`, `environment:`, `slash_prefix:`, `source_mode:`, `response_file_syntax:`). `id:` must be unique across this directory (checked at codegen) and is the only accepted config `as:` spelling for this family -- no aliases. 3. Run `cargo build && cargo test`, then accept the snapshot diff (`cargo insta accept`), and regenerate `site/src/reference/supported-compilers.md` with `scripts/generate-supported-compilers.py` 4. When the compiler requires extra plumbing for interception, write the family's page under `site/src/guides/recipes/` in the same release, and list it in `site/src/SUMMARY.md` and `site/src/guides/recipes/index.md`. Otherwise the generated `site/src/reference/supported-compilers.md` already names it, and a page that only says "put it on `PATH` and run Bear" adds nothing. See `site/CLAUDE.md` for the authoring rules, and use the family's `id:` when the page shows an `as:` value. ## Adding a new wrapper 1. Create `mywrapper.yaml` with `type: wrapper`, a `recognize:` entry, and optionally `options:` (exact-token launcher flags only) 2. No Rust change needed: codegen discovers it by kind, the unwrap logic is generic over generated data, and the basename is emitted into `WRAPPER_AS_NAMES` so `as: mywrapper` is accepted -- see `README.md` 3. Run `cargo build && cargo test`, then accept the snapshot diff ## Removing a compiler Delete the YAML file, then delete its now-orphaned per-family flag snapshot (`build-support/compilers-codegen/tests/snapshots/snapshots__snapshot_flags_.snap`) by hand: the looped snapshot test names snapshots per discovered stem, so a removed family's stored snapshot is simply no longer referenced, not reported as stale. ## `recognize:` entries must be documented and cited Every `recognize:` entry is mandatory `description` (a short human label, e.g. `"GCC"`) and a mandatory `references` (a non-empty list of http(s) documentation URLs). Both are validated at codegen time -- the build fails if either is missing, blank, or `references` holds a non-http(s) entry. `description` is emitted into the generated `RECOGNITION_PATTERNS` table and shown by `bear semantic --print-compilers`; `references` is validate-only and is never emitted into generated code. ## Adding a new flag to an existing compiler 1. Find the correct YAML file 2. Add entry under `flags:` with `match` pattern and `result` 3. `cargo build` regenerates tables automatically 4. `cargo test` validates sorting and invariants ## Per-family selectors: source_mode and response_file_syntax Two selectors are consumed outside the flag classifier (`source_mode` at the converter, `response_file_syntax` at response-file tokenization and environment expansion). Their semantics are code; the per-family choice is data -- optional top-level YAML keys, not inherited through `extends`. Defaults: `source_mode: per-source-stripped` (vala is `combined`, swift is `per-source-full`) and `response_file_syntax: gnu` (msvc and clang_cl are `msvc`). See `README.md` for the full description of each value. ## Common mistakes - Forgetting to run `cargo build` after YAML edits (stale generated code) - Using wrong pattern syntax (see README.md pattern table) - Adding flags to wrong file when inheritance (`extends:`) would cover it - Not considering cross-platform implications (`slash_prefix` for MSVC-style compilers) ## Regression protection Compiler interpreter changes must be covered by integration tests. See `tests/integration/CLAUDE.md` for how to write them. rizsotto-Bear-0c30b27/crates/semantic/compilers/README.md000066400000000000000000000406141527177720300231600ustar00rootroot00000000000000# Compiler Definitions This directory contains YAML files that define how Bear recognizes compiler executables, categorizes their command-line flags, and filters internal invocations. Each file corresponds to one compiler (or compiler family). At build time, `crates/semantic/build.rs` reads these files and generates static Rust arrays for flag tables, ignore filters, and recognition patterns. The generated code is included in the interpreter and recognition modules via `include!()`. ## File structure Every file in this directory declares two things up front: `type:`, a closed kind enum naming which of the two schemas the rest of the file follows (`compiler` or `wrapper`), and, for `type: compiler`, a nested `compiler:` block holding the family's identity. `type: wrapper` files (compiler launchers such as ccache) are a different, simpler schema -- see "Compiler-launcher (wrapper) files" below. ```yaml # Required: closed kind enum. This file follows the compiler schema. type: compiler # Required for type: compiler -- the family's identity. compiler: id: gcc # Required, unique across every file in this directory # (checked at codegen). This is the `extends:` target, # the value emitted into RECOGNITION_PATTERNS, and the # ONLY accepted config `as:` spelling for this family # -- no aliases. extends: # Optional: another file's compiler.id to inherit # flags, ignore filters, slash_prefix, and environment # entries from. References an id, not a filename -- # file names are pure packaging. By convention a # file's stem still matches its own id (see the real # files in this directory), but nothing enforces that. # Executable names this compiler is known by recognize: - description: "GCC" # required: short human label, shown by --print-compilers references: # required: non-empty list of http(s) doc URLs (validate-only) - "https://gcc.gnu.org/onlinedocs/gcc/Option-Summary.html" executables: ["gcc", "g++", "gfortran"] versioned: true # match with version suffix (e.g., gcc-11, gcc11) cross_compilation: true # match with cross-compilation prefix (e.g., arm-linux-gnu-gcc) # Optional: treat '/'-prefixed arguments as flags (default: false) # When true, arguments like /Fo, /c, /I are treated as compiler flags. # When false (default), only '-'-prefixed arguments are treated as flags. # Inherited from base file via `extends` if not specified. slash_prefix: false # Optional: how this family's sources map to database entries (default: # per-source-stripped). See "Source mode and response-file syntax" below. # Not inherited via `extends`; each family states its own. source_mode: per-source-stripped # Optional: response-file (@file) tokenization convention (default: gnu; # msvc for MSVC-style families). Not inherited via `extends`. response_file_syntax: gnu # Optional: conditions under which a recognized invocation should be ignored ignore_when: # Ignore if the executable filename matches any of these executables: ["cc1", "cc1plus", "f951"] # Ignore if any argument matches any of these flags flags: ["-cc1"] flags: - match: {pattern: "-o{ }*"} result: output - match: {pattern: "-c"} result: stops_at_compiling - match: {pattern: "-I{ }*"} result: configures_preprocessing ``` ## Pattern syntax The `pattern` string encodes both the flag name and how it consumes arguments: | Syntax | Example | Meaning | |---------------|-----------------|------------------------------------------------| | `-flag` | `-c` | Exact match, no additional arguments | | `-flag` + count | `-x` count: 1 | Exact match with N separate arguments | | `-flag*` | `-W*` | Prefix match (anything starting with `-W`) | | `-flag*` + count | `-Xarch*` count: 1 | Prefix match with N separate arguments | | `-flag{ }*` | `-D{ }*` | Exact match, value glued or as separate arg | | `-flag=*` | `-specs=*` | Exact match, value after `=` | | `-flag{=}*` | `--std{=}*` | Exact match, value after `=` or as separate arg| | `-flag:*` | `/std:*` | Exact match, value after `:` | | `-flag{:}*` | `/Fe{:}*` | Exact match, value after `:` or as separate arg| The `{}` pair means the separator is optional: - `{ }` -- the space between flag and value is optional (value can be glued: `-Dfoo` or separate: `-D foo`) - `{=}` -- the `=` between flag and value is optional (value can follow `=`: `--std=c99` or be separate: `--std c99`) - `{:}` -- the `:` between flag and value is optional (value can follow `:`: `/std:c++20` or be separate: `/std c++20`) ## Result values The `result` field describes what the flag means semantically: | Value | Meaning | |-----------------------------|---------------------------------------------| | `output` | Output file specification | | `configures_preprocessing` | Affects the preprocessing pass | | `configures_compiling` | Affects the compilation pass | | `configures_assembling` | Affects the assembly pass | | `configures_linking` | Affects the linking pass | | `stops_at_preprocessing` | Stop compilation after preprocessing | | `stops_at_compiling` | Stop compilation after compiling | | `stops_at_assembling` | Stop compilation after assembling | | `info_and_exit` | Print info and exit (e.g. `--version`) | | `driver_option` | Driver/toolchain behavior flag | | `pass_through` | Stop parsing; remaining args go to linker | | `none` | No specific semantic effect | ## Ignore filters The optional `ignore_when` section specifies conditions under which a recognized compiler invocation should be treated as an internal/ignored command rather than a user-facing compilation: - `executables` -- list of executable filenames (not paths). If the invoked executable's filename matches any entry, the command is ignored. Used by GCC to skip internal executables like `cc1`, `collect2`, etc. - `flags` -- list of argument strings. If any argument in the invocation matches any entry, the command is ignored. Used by Clang to skip `-cc1` frontend invocations. Both fields are optional and default to empty. When a file's `compiler.extends` is set, the ignore filters are inherited from the base file only if the extending file does not define its own list for that field (i.e., own values take precedence per field, not per entry). ## Inheritance Files with `compiler.extends: gcc` inherit all GCC flags and (unless overridden) ignore filters. The build script concatenates own flags before base flags, then sorts all entries by flag name length (longest first) so more specific flags match before shorter prefixes. The sort is stable, so own flags take priority over base flags of the same length. ## Recognition patterns The `recognize` section defines which executable names this compiler is known by. Each entry specifies: - `executables` -- list of base executable names (e.g., `["gcc", "g++"]`) - `cross_compilation` -- if `true`, also matches names with a cross-compilation prefix (e.g., `arm-linux-gnueabihf-gcc`) - `versioned` -- if `true`, also matches names with a version suffix (e.g., `gcc-11`, `gcc11`, `gcc-11.2`) - `description` -- required short human label (e.g., `"GCC"`), emitted into the generated recognition table and shown by `bear semantic --print-compilers` - `references` -- required non-empty list of http(s) documentation URLs; validated at codegen time but never emitted into generated code Both `description` and `references` are mandatory: the build fails if either is missing, `description` is blank, or `references` holds a non-http(s) entry. All patterns automatically handle `.exe` extensions on Windows. Executables listed in `ignore_when.executables` are automatically added as recognition entries with `cross_compilation: false, versioned: false`. This ensures the recognizer routes them to the right compiler type, where the interpreter then ignores them. You do not need to list them under `recognize`. ## Compiler-launcher (wrapper) files A compiler launcher (`ccache`, `distcc`, `sccache`, `icecc`) is not a compiler: it carries the real compiler in its own argv (`ccache gcc -c main.c`), and Bear records the invocation as the real compiler's, not the launcher's. Dispatch for every launcher is uniform (one runtime `CompilerType::Wrapper`), so a launcher file needs no family identity of its own -- no `compiler:` block, no `id`, no `extends:`, no `flags:`, `ignore_when:`, `slash_prefix:`, or `environment:`. Its only job is to say what the launcher is (`recognize:`) and, optionally, which of its own options precede the inner compiler in argv (`options:`): ```yaml type: wrapper recognize: - description: "Compiler cache" # same description/references contract as compiler recognize references: - "https://ccache.dev/manual/latest.html" executables: ["ccache"] # versioned and cross_compilation must both be false (or omitted): a # launcher basename is matched exactly, never version-suffixed or # cross-compilation prefixed. options: # optional; only distcc.yaml has this today - match: {pattern: "-j", count: 1} - match: {pattern: "-v"} ``` `options:` reuses the `match`/`count` sub-language from "Pattern syntax" above, but only its exact-token form: no `*` prefix matching and no `{ }`/`{=}`/`{:}` glued-or-separate forms. `WrapperTable::validate` (in `build-support/compilers-codegen/src/yaml_types.rs`) rejects any pattern containing `*` or `{` at codegen time, because the unwrap loop that consumes this data only compares argv tokens for equality -- it does not implement prefix or glued-value matching. `count` is how many following argv tokens the option's value consumes, so the skip loop advances `1 + count` slots per matched option. The *absence* of `options:` is itself the contract: "argv[1] is the real compiler" (true for ccache, sccache, and icecc; distcc is the one launcher whose own flags can precede the compiler). ## Environment variables The optional `environment` section declares environment variables that the compiler binary reads and how their values map to command-line arguments. ```yaml environment: - variable: CPATH effect: configures_preprocessing mapping: flag: "-I" separator: path - variable: CL effect: configures_compiling mapping: expand: prepend separator: space ``` Each entry has: - `variable` -- the environment variable name (must match `[A-Za-z_][A-Za-z0-9_]*`) - `effect` -- semantic effect (same vocabulary as `result` in flags) - `mapping` -- how the value translates to arguments ### Mapping types | Type | Fields | Behavior | |------|--------|----------| | Flag | `flag` + `separator` | Split value by separator, emit ` ` per element | | Expand | `expand` + `separator: space` | Split value with the family's `response_file_syntax` rules, insert as raw arguments | ### Separators | Value | Meaning | |-------|---------| | `path` | Platform path separator (`:` on Unix, `;` on Windows) | | `";"` | Fixed semicolon separator | | `space` | Word splitting by the family's `response_file_syntax` (shell words for `gnu`, Windows command-line rules for `msvc`; used with `expand`) | ### Expand positions | Value | Meaning | |-------|---------| | `prepend` | Insert before command-line arguments (e.g., MSVC `CL`) | | `append` | Insert after command-line arguments (e.g., MSVC `_CL_`) | ### Documentary entries Variables the compiler reads but Bear cannot parse (e.g., config file paths) can be listed with `effect: none`: ```yaml - variable: ICXCFG effect: none note: "Config file - not parsed" mapping: separator: space ``` These are skipped during code generation but document the variable for future contributors. ### Environment inheritance Environment variables follow the `extends` chain transitively. If `armclang.yaml` extends `clang.yaml` which extends `gcc.yaml`, armclang inherits all GCC and Clang environment entries. Own entries override inherited ones matched by variable name. Compilers that do not read GCC variables (e.g., NVIDIA HPC SDK) must not extend GCC and will have an empty environment table. ## Source mode and response-file syntax Two per-family selectors are consumed outside the flag classifier -- one after parsing (at the converter), one before it (at response-file tokenization and environment expansion). Their *semantics* are code; the per-family *choice* is data, declared in these YAML files. Neither is inherited through `extends`: each family states its own (so `clang_cl` carries `response_file_syntax: msvc` itself, it does not inherit it from `msvc`). `source_mode` -- how an invocation's sources map to compilation database entries. Consumed at the converter (post-parse). Values: - `per-source-stripped` (default) -- for GCC/Clang and most families: each source is a separable translation unit, and the converter emits one entry per source, stripping sibling sources from each entry's arguments. - `combined` -- for a single-translation-unit compiler like `valac`, which compiles all of a target's sources together and produces one output: the converter emits exactly one combined entry per invocation (`file` is the first source, every source retained). - `per-source-full` -- for a whole-module compiler like `swiftc`: every source is analyzed together, but per-file consuming tooling (SourceKit-LSP) looks up a compile command by file path, so the converter emits one entry per source while every entry keeps the complete invocation (no sibling stripping). `response_file_syntax` -- how `@file` response files are tokenized before flag classification (only relevant with `format.arguments.from_response_files`). The same rules split the value of an `expand` environment variable into words. Values: `gnu` (default) for GCC/Clang whitespace-and-quote rules; `msvc` for the Windows `CommandLineToArgv` rules used by `msvc` and `clang-cl`. ## Adding a new compiler Adding a compiler family is a YAML file plus accepting snapshots -- no Rust edit. Codegen discovers the file by scanning this directory and peeking its `type:`, derives every generated name from the file stem, and emits the recognition row, the `KNOWN_IDS` entry (so `as: ` is accepted), and the family's interpreter registration automatically. 1. Create a new YAML file in this directory (e.g., `mycompiler.yaml`). 2. Add `type: compiler`, a `compiler:` block (`id:`, and optionally `extends:`), `recognize:`, and `flags:` entries; optionally `ignore_when:`, `environment:`, `slash_prefix:`, `source_mode:`, `response_file_syntax:`. `id:` must be unique across every YAML file in this directory (checked at codegen); it is also the `extends:` target and the ONLY accepted config `as:` spelling for this family -- there is no alias step. 3. Run `cargo build`, then `cargo test`. The snapshot tests will show a diff for the new family (its flag table, the recognition table, and the family registry); run `cargo insta accept` (or update the snapshots) to accept it. ## Adding a new wrapper 1. Create `mywrapper.yaml` in this directory with `type: wrapper`, a `recognize:` entry (description + references + executables, with `versioned`/`cross_compilation` false or omitted), and an optional `options:` list of exact-token flags the launcher accepts before its inner compiler. 2. Run `cargo build && cargo test`, and accept the snapshots. That is the whole change: codegen discovers `type: wrapper` files by kind (`load_wrapper_tables`), the unwrap logic (`extract_real_compiler`) is generic over the generated `WRAPPER_NAMES`/`WRAPPER_OPTIONS`, and the launcher basename is emitted into `WRAPPER_AS_NAMES` so `as: mywrapper` is accepted -- no wrapper-specific Rust code. ## Adding a new flag 1. Find the right YAML file for the compiler 2. Add an entry under `flags:` with the appropriate `match` pattern and `result` 3. Run `cargo build` -- the build script regenerates the flag tables automatically 4. Run `cargo test` -- invariant tests verify sorting, no invalid kinds, etc. rizsotto-Bear-0c30b27/crates/semantic/compilers/arm_flang.yaml000066400000000000000000000005511527177720300245070ustar00rootroot00000000000000type: compiler compiler: id: arm_flang extends: flang recognize: - description: "Arm Compiler for Linux Fortran" references: - "https://learn.arm.com/install-guides/acfl/" - "https://support.arm.com/documentation/101380/latest/" executables: ["armflang"] versioned: false cross_compilation: false slash_prefix: false flags: [] rizsotto-Bear-0c30b27/crates/semantic/compilers/armclang.yaml000066400000000000000000000020341527177720300243430ustar00rootroot00000000000000type: compiler compiler: id: armclang extends: clang recognize: - description: "Arm Compiler 6" references: - "https://developer.arm.com/documentation/100748/latest/" executables: ["armclang", "armclang++"] versioned: true cross_compilation: false slash_prefix: false flags: - match: {pattern: "--library_type{=}*"} result: configures_linking - match: {pattern: "--depend{ }*"} result: configures_compiling - match: {pattern: "--depend_format{=}*"} result: configures_compiling - match: {pattern: "--depend_target{ }*"} result: configures_compiling - match: {pattern: "--depend_single_line"} result: configures_compiling - match: {pattern: "--depend_system_headers"} result: configures_compiling - match: {pattern: "--no_depend_system_headers"} result: configures_compiling - match: {pattern: "--apcs{=}*"} result: configures_compiling environment: - variable: ARMCOMPILER6_CLANGOPT effect: configures_compiling mapping: expand: prepend separator: space rizsotto-Bear-0c30b27/crates/semantic/compilers/ccache.yaml000066400000000000000000000002271527177720300237670ustar00rootroot00000000000000type: wrapper recognize: - description: "Compiler cache" references: - "https://ccache.dev/manual/latest.html" executables: ["ccache"] rizsotto-Bear-0c30b27/crates/semantic/compilers/clang.yaml000066400000000000000000000201571527177720300236510ustar00rootroot00000000000000type: compiler compiler: id: clang extends: gcc recognize: - description: "Clang" references: - "https://clang.llvm.org/docs/ClangCommandLineReference.html" executables: ["clang", "clang++"] versioned: true cross_compilation: true - description: "AMD ROCm Clang/HIP" references: - "https://rocm.docs.amd.com/en/latest/" executables: ["amdclang", "amdclang++", "hipcc"] versioned: false cross_compilation: false # Emscripten drivers (emcc, em++, plus the .py-suffixed spellings some # installs expose) and TI's tiarmclang. tiarmclang is a single token with # no hyphen, so the -clang cross rule does not catch it; it must # be listed explicitly. Emscripten's binutils companions (emar, emranlib) # are deliberately absent -- they are not compiler drivers. - description: "Emscripten" references: - "https://emscripten.org/docs/tools_reference/emcc.html" executables: ["emcc", "em++", "emcc.py", "em++.py"] versioned: false cross_compilation: false - description: "Texas Instruments Arm Clang" references: - "https://www.ti.com/tool/ARM-CGT" executables: ["tiarmclang"] versioned: false cross_compilation: false ignore_when: flags: ["-cc1"] slash_prefix: false flags: - match: {pattern: "--prefix=*"} result: configures_preprocessing - match: {pattern: "-F{ }*"} result: configures_compiling - match: {pattern: "-ObjC"} result: none - match: {pattern: "-ObjC++"} result: none - match: {pattern: "-Xarch*", count: 1} result: none - match: {pattern: "-Xcuda*", count: 1} result: none - match: {pattern: "-Xopenmp-target", count: 1} result: none - match: {pattern: "-Xopenmp-target=*", count: 1} result: none - match: {pattern: "-Z", count: 1} result: configures_compiling - match: {pattern: "-a*"} result: configures_compiling - match: {pattern: "--profile-blocks"} result: configures_compiling - match: {pattern: "-all_load"} result: configures_linking - match: {pattern: "-allowable_client", count: 1} result: configures_linking - match: {pattern: "--analyze"} result: configures_compiling - match: {pattern: "--analyzer-output{ }*"} result: configures_compiling # C++20 module-interface precompile: a compile action, not a link - match: {pattern: "--precompile"} result: stops_at_compiling - match: {pattern: "-Xanalyzer", count: 1} result: configures_compiling - match: {pattern: "-arch", count: 1} result: configures_compiling - match: {pattern: "-arch_only", count: 1} result: none - match: {pattern: "--autocomplete=*"} result: none - match: {pattern: "-bind_at_load"} result: configures_linking - match: {pattern: "-bundle"} result: configures_linking - match: {pattern: "-bundle_loader", count: 1} result: configures_linking - match: {pattern: "-client_name*"} result: none - match: {pattern: "--config", count: 1} result: driver_option - match: {pattern: "--cuda-host-only"} result: configures_compiling - match: {pattern: "-cuid=*"} result: none - match: {pattern: "-current_version{ }*"} result: configures_linking - match: {pattern: "-dead_strip"} result: configures_linking - match: {pattern: "-dependency-dot", count: 1} result: configures_preprocessing - match: {pattern: "-dependency-file", count: 1} result: configures_preprocessing - match: {pattern: "-dsym-dir{ }*"} result: none - match: {pattern: "-dumpmachine"} result: info_and_exit - match: {pattern: "-dumpversion"} result: info_and_exit - match: {pattern: "--dyld-prefix{=}*"} result: none - match: {pattern: "-dylib_file", count: 1} result: configures_linking - match: {pattern: "-dylinker"} result: configures_linking - match: {pattern: "-dynamic"} result: configures_linking - match: {pattern: "-dynamiclib"} result: configures_linking - match: {pattern: "-emit-ast"} result: configures_compiling - match: {pattern: "-faligned-new=*"} result: configures_compiling - match: {pattern: "-force_load", count: 1} result: configures_linking - match: {pattern: "-framework", count: 1} result: configures_linking - match: {pattern: "--gcc-toolchain=*"} result: configures_compiling - match: {pattern: "-gcodeview"} result: configures_compiling - match: {pattern: "-gcodeview-ghash"} result: configures_compiling - match: {pattern: "--hip-link"} result: configures_linking - match: {pattern: "--hip-version=*"} result: configures_compiling - match: {pattern: "-ibuiltininc"} result: configures_preprocessing - match: {pattern: "-image_base", count: 1} result: configures_linking - match: {pattern: "-index-header-map"} result: configures_preprocessing - match: {pattern: "-init", count: 1} result: configures_linking - match: {pattern: "-install_name", count: 1} result: configures_linking - match: {pattern: "-lazy*", count: 1} result: configures_linking - match: {pattern: "-EB"} result: none - match: {pattern: "--migrate"} result: configures_compiling - match: {pattern: "-mllvm{=}*"} result: configures_compiling - match: {pattern: "-multiply_defined*", count: 1} result: configures_linking - match: {pattern: "--output{=}*"} result: output - match: {pattern: "-objcmt*"} result: configures_compiling - match: {pattern: "-object"} result: none - match: {pattern: "--profile"} result: configures_compiling - match: {pattern: "--pipe"} result: driver_option - match: {pattern: "-r*"} result: configures_linking - match: {pattern: "--save*"} result: driver_option - match: {pattern: "-sect*", count: 3} result: configures_linking - match: {pattern: "-seg1addr{ }*"} result: configures_linking - match: {pattern: "-seg_*", count: 1} result: configures_linking - match: {pattern: "-segaddr", count: 2} result: configures_linking - match: {pattern: "-segcreate", count: 3} result: configures_linking - match: {pattern: "-seglinkedit"} result: configures_linking - match: {pattern: "-segprot", count: 3} result: configures_linking - match: {pattern: "-single_module"} result: configures_linking - match: {pattern: "-sub_*"} result: configures_linking - match: {pattern: "--sysroot{=}*"} result: configures_preprocessing - match: {pattern: "--target{=}*"} result: configures_compiling - match: {pattern: "-target", count: 1} result: configures_compiling - match: {pattern: "-time"} result: driver_option - match: {pattern: "--traditional*"} result: configures_preprocessing - match: {pattern: "-traditional*"} result: configures_preprocessing - match: {pattern: "-twolevel*"} result: configures_linking - match: {pattern: "-umbrella", count: 1} result: configures_linking - match: {pattern: "-unwindlib=*"} result: configures_linking - match: {pattern: "--unwindlib=*"} result: configures_linking - match: {pattern: "--language{=}*"} result: configures_compiling - match: {pattern: "-Xassembler", count: 1} result: configures_assembling - match: {pattern: "-Xclang", count: 1} result: configures_compiling - match: {pattern: "-Xpreprocessor", count: 1} result: configures_preprocessing - match: {pattern: "-triple", count: 1} result: configures_compiling - match: {pattern: "-analyzer-config", count: 1} result: configures_compiling - match: {pattern: "-emit-llvm"} result: configures_compiling - match: {pattern: "-resource-dir", count: 1} result: configures_compiling - match: {pattern: "-MJ", count: 1} result: configures_preprocessing - match: {pattern: "--cuda-path{=}*"} result: configures_compiling - match: {pattern: "--cuda-gpu-arch{=}*"} result: configures_compiling - match: {pattern: "--hip-path{=}*"} result: configures_compiling - match: {pattern: "--gcc-install-dir{=}*"} result: configures_compiling - match: {pattern: "-load", count: 1} result: configures_compiling - match: {pattern: "-plugin", count: 1} result: configures_compiling - match: {pattern: "-plugin-arg-*"} result: configures_compiling rizsotto-Bear-0c30b27/crates/semantic/compilers/clang_cl.yaml000066400000000000000000000060031527177720300243210ustar00rootroot00000000000000type: compiler compiler: id: clang_cl extends: msvc recognize: - description: "Clang (MSVC mode)" references: - "https://clang.llvm.org/docs/UsersManual.html#clang-cl" executables: ["clang-cl"] versioned: true cross_compilation: false slash_prefix: true response_file_syntax: msvc flags: # Pass arg to clang frontend - match: {pattern: "-Xclang", count: 1} result: configures_compiling # Diagnostics - match: {pattern: "-fcolor-diagnostics"} result: none - match: {pattern: "-fno-color-diagnostics"} result: none - match: {pattern: "-fcrash-diagnostics-dir{=}*"} result: none # MSVC compatibility - match: {pattern: "-fms-compatibility-version{=}*"} result: configures_compiling - match: {pattern: "-fms-extensions"} result: configures_compiling - match: {pattern: "-fno-ms-extensions"} result: configures_compiling - match: {pattern: "-fmsc-version{=}*"} result: configures_compiling # Sanitizers - match: {pattern: "-fsanitize{=}*"} result: configures_compiling - match: {pattern: "-fno-sanitize{=}*"} result: configures_compiling - match: {pattern: "-fsanitize-blacklist{=}*"} result: configures_compiling # LTO - match: {pattern: "-flto"} result: configures_compiling - match: {pattern: "-flto=*"} result: configures_compiling - match: {pattern: "-fno-lto"} result: configures_compiling # Profiling and coverage - match: {pattern: "-fprofile-instr-generate"} result: configures_compiling - match: {pattern: "-fprofile-instr-generate=*"} result: configures_compiling - match: {pattern: "-fprofile-instr-use{=}*"} result: configures_compiling - match: {pattern: "-fcoverage-mapping"} result: configures_compiling - match: {pattern: "-fno-coverage-mapping"} result: configures_compiling # Auto variable initialization - match: {pattern: "-ftrivial-auto-var-init{=}*"} result: configures_compiling # Target - match: {pattern: "--target{=}*"} result: configures_compiling - match: {pattern: "-m32"} result: configures_compiling - match: {pattern: "-m64"} result: configures_compiling - match: {pattern: "-msse*"} result: configures_compiling - match: {pattern: "-mavx*"} result: configures_compiling # Driver options - match: {pattern: "--driver-mode{=}*"} result: driver_option - match: {pattern: "-v"} result: driver_option # Linker - match: {pattern: "-fuse-ld{=}*"} result: configures_linking # Info and exit - match: {pattern: "-###"} result: info_and_exit - match: {pattern: "--version"} result: info_and_exit # clang-cl specific slash flags - match: {pattern: "/clang:*"} result: configures_compiling - match: {pattern: "/imsvc{ }*"} result: configures_preprocessing - match: {pattern: "/showFilenames"} result: none - match: {pattern: "/vctoolsdir{ }*"} result: driver_option - match: {pattern: "/vctoolsversion{ }*"} result: driver_option - match: {pattern: "/winsysroot{ }*"} result: driver_option rizsotto-Bear-0c30b27/crates/semantic/compilers/cray_cc.yaml000066400000000000000000000015231527177720300241640ustar00rootroot00000000000000# CCE C/C++ is Clang-based, so flag parsing extends the Clang table. # # The HPE Cray PrgEnv wrapper "CC" is NOT listed here: on Cray systems "CC" # drives whichever compiler module the loaded programming environment # selects (CCE, GCC, or another vendor's compiler), so the basename alone # cannot say which compiler it is. It is classified by the ambiguous-name # version probe instead (see # docs/requirements/recognition-ambiguous-name-probe.md). type: compiler compiler: id: cray_cc extends: clang recognize: - description: "Cray C/C++" references: - "https://support.hpe.com/hpesc/public/docDisplay?docId=a00113893en_us&page=index.html" executables: ["craycc", "crayCC", "craycxx"] versioned: true cross_compilation: false slash_prefix: false flags: - match: {pattern: "-fcray-*"} result: configures_compiling rizsotto-Bear-0c30b27/crates/semantic/compilers/cray_fortran.yaml000066400000000000000000000100201527177720300252420ustar00rootroot00000000000000type: compiler compiler: id: cray_fortran recognize: - description: "Cray Fortran" references: - "https://support.hpe.com/hpesc/public/docDisplay?docId=a00115296en_us&page=Fortran_Command-line_Options.html" executables: ["crayftn", "ftn"] versioned: true cross_compilation: false slash_prefix: false flags: - match: {pattern: "-no-add-rpath-shared"} result: configures_linking - match: {pattern: "--no-custom-ld-script"} result: configures_linking - match: {pattern: "-no-add-runpath"} result: configures_linking - match: {pattern: "-add-rpath-shared"} result: configures_linking - match: {pattern: "--no-as-needed"} result: configures_linking - match: {pattern: "-no-gcc-rpath"} result: configures_linking - match: {pattern: "-no-add-rpath"} result: configures_linking - match: {pattern: "-add-runpath"} result: configures_linking - match: {pattern: "-no-as-needed"} result: configures_linking - match: {pattern: "--as-needed"} result: configures_linking - match: {pattern: "-gcc-rpath"} result: configures_linking - match: {pattern: "-add-rpath"} result: configures_linking - match: {pattern: "-as-needed"} result: configures_linking - match: {pattern: "-qno-openmp"} result: none - match: {pattern: "-noopenmp"} result: none - match: {pattern: "-Mnoopenmp"} result: none - match: {pattern: "-default64"} result: none - match: {pattern: "-openmp"} result: none - match: {pattern: "-dynamic"} result: configures_linking - match: {pattern: "-shared"} result: configures_linking - match: {pattern: "-static"} result: configures_linking - match: {pattern: "-VVV"} result: driver_option - match: {pattern: "-mp"} result: none - match: {pattern: "-VV"} result: driver_option - match: {pattern: "--custom-ld-script={ }*"} result: configures_linking - match: {pattern: "-target-network={ }*"} result: none - match: {pattern: "-target-accel={ }*"} result: none - match: {pattern: "-target-cpu={ }*"} result: none - match: {pattern: "-A{ }*"} result: none - match: {pattern: "-b{ }*"} result: output - match: {pattern: "-D{ }*"} result: configures_preprocessing - match: {pattern: "-d{ }*"} result: none - match: {pattern: "-e{ }*"} result: none - match: {pattern: "-f{ }*"} result: none - match: {pattern: "-G{ }*"} result: none - match: {pattern: "-h{ }*"} result: none - match: {pattern: "-I{ }*"} result: configures_preprocessing - match: {pattern: "-J{ }*"} result: configures_preprocessing - match: {pattern: "-K{ }*"} result: none - match: {pattern: "-l{ }*"} result: configures_linking - match: {pattern: "-L{ }*"} result: configures_linking - match: {pattern: "-m{ }*"} result: none - match: {pattern: "-M{ }*"} result: none - match: {pattern: "-N{ }*"} result: none - match: {pattern: "-O{ }*"} result: configures_compiling - match: {pattern: "-o{ }*"} result: output - match: {pattern: "-p{ }*"} result: configures_preprocessing - match: {pattern: "-Q{ }*"} result: configures_preprocessing - match: {pattern: "-r{ }*"} result: driver_option - match: {pattern: "-R{ }*"} result: none - match: {pattern: "-s{ }*"} result: none - match: {pattern: "-U{ }*"} result: configures_preprocessing - match: {pattern: "-W{ }*"} result: none - match: {pattern: "-x{ }*"} result: none - match: {pattern: "-Y{ }*"} result: none - match: {pattern: "--cray*"} result: none - match: {pattern: "-cray*"} result: none - match: {pattern: "-c"} result: stops_at_compiling - match: {pattern: "-E"} result: stops_at_preprocessing - match: {pattern: "-F"} result: none - match: {pattern: "-g"} result: configures_compiling - match: {pattern: "-S"} result: stops_at_assembling - match: {pattern: "-T"} result: driver_option - match: {pattern: "-v"} result: driver_option - match: {pattern: "-V"} result: info_and_exit rizsotto-Bear-0c30b27/crates/semantic/compilers/cuda.yaml000066400000000000000000000107241527177720300235000ustar00rootroot00000000000000type: compiler compiler: id: cuda extends: gcc recognize: - description: "NVIDIA CUDA" references: - "https://docs.nvidia.com/cuda/cuda-compiler-driver-nvcc/index.html" executables: ["nvcc"] versioned: true cross_compilation: true slash_prefix: false flags: - match: {pattern: "--gpu-architecture{=}*"} result: configures_compiling - match: {pattern: "-arch{=}*"} result: configures_compiling - match: {pattern: "--gpu-code{=}*"} result: configures_compiling - match: {pattern: "-code{=}*"} result: configures_compiling - match: {pattern: "--generate-code{=}*"} result: configures_compiling - match: {pattern: "-gencode{=}*"} result: configures_compiling - match: {pattern: "--compile"} result: stops_at_compiling - match: {pattern: "-c"} result: stops_at_compiling - match: {pattern: "--device-c"} result: stops_at_compiling - match: {pattern: "-dc"} result: stops_at_compiling - match: {pattern: "--device-w"} result: stops_at_compiling - match: {pattern: "-dw"} result: stops_at_compiling - match: {pattern: "--device-link"} result: configures_linking - match: {pattern: "-dlink"} result: configures_linking - match: {pattern: "--link"} result: configures_linking - match: {pattern: "--lib"} result: configures_linking - match: {pattern: "--run"} result: none - match: {pattern: "--cudart{=}*"} result: configures_linking - match: {pattern: "--cuda-path{=}*"} result: configures_compiling - match: {pattern: "--compiler-bindir{=}*"} result: configures_compiling - match: {pattern: "-ccbin{=}*"} result: configures_compiling - match: {pattern: "-Xcompiler*", count: 1} result: configures_compiling - match: {pattern: "-Xlinker*", count: 1} result: configures_linking - match: {pattern: "--maxrregcount{=}*"} result: configures_compiling - match: {pattern: "-maxrregcount{=}*"} result: configures_compiling - match: {pattern: "--use_fast_math"} result: configures_compiling - match: {pattern: "-use_fast_math"} result: configures_compiling - match: {pattern: "--ftz", count: 1} result: configures_compiling - match: {pattern: "--prec-div", count: 1} result: configures_compiling - match: {pattern: "--prec-sqrt", count: 1} result: configures_compiling - match: {pattern: "--fmad", count: 1} result: configures_compiling - match: {pattern: "--ptx"} result: configures_compiling - match: {pattern: "--cubin"} result: configures_compiling - match: {pattern: "--fatbin"} result: configures_compiling - match: {pattern: "--device-debug"} result: configures_compiling - match: {pattern: "-G"} result: configures_compiling - match: {pattern: "--generate-line-info"} result: configures_compiling - match: {pattern: "-lineinfo"} result: configures_compiling - match: {pattern: "--profile"} result: configures_compiling - match: {pattern: "--optimize{=}*"} result: configures_compiling - match: {pattern: "-O{=}*"} result: configures_compiling - match: {pattern: "--std{=}*"} result: configures_compiling - match: {pattern: "--relocatable-device-code{=}*"} result: configures_compiling - match: {pattern: "-rdc", count: 1} result: configures_compiling - match: {pattern: "--extended-lambda"} result: configures_compiling - match: {pattern: "-extended-lambda"} result: configures_compiling - match: {pattern: "--expt-extended-lambda"} result: configures_compiling - match: {pattern: "--expt-relaxed-constexpr"} result: configures_compiling - match: {pattern: "--output-file{ }*"} result: output - match: {pattern: "-o{ }*"} result: output - match: {pattern: "--preprocess"} result: stops_at_preprocessing - match: {pattern: "-E"} result: stops_at_preprocessing - match: {pattern: "--verbose"} result: driver_option - match: {pattern: "-v"} result: driver_option - match: {pattern: "--version"} result: info_and_exit - match: {pattern: "--help"} result: info_and_exit - match: {pattern: "-W*"} result: none - match: {pattern: "--disable-warnings*"} result: none - match: {pattern: "--include-path{=}*"} result: configures_preprocessing - match: {pattern: "--keep"} result: driver_option - match: {pattern: "--keep-dir{=}*"} result: driver_option - match: {pattern: "--machine{=}*"} result: configures_compiling - match: {pattern: "-m{=}*"} result: configures_compiling rizsotto-Bear-0c30b27/crates/semantic/compilers/distcc.yaml000066400000000000000000000014411527177720300240310ustar00rootroot00000000000000# A compiler launcher: not a compiler itself; it carries the real # compiler in its own argv (`distcc gcc -c main.c`). type: wrapper recognize: - description: "Distributed compiler" references: - "https://www.distcc.org/man/distcc_1.html" executables: ["distcc"] # Launcher-own options that may precede the inner compiler in argv. # Same match sub-language as compiler flags: `count` is how many # following argv tokens the option's value consumes (so the skip loop # advances 1 + count slots). options: - match: {pattern: "-j", count: 1} - match: {pattern: "--jobs", count: 1} - match: {pattern: "-v"} - match: {pattern: "--verbose"} - match: {pattern: "-i"} - match: {pattern: "--show-hosts"} - match: {pattern: "--scan-avail"} - match: {pattern: "--show-principal"} rizsotto-Bear-0c30b27/crates/semantic/compilers/embarcadero.yaml000066400000000000000000000006721527177720300250310ustar00rootroot00000000000000type: compiler compiler: id: embarcadero extends: clang recognize: - description: "Embarcadero C++Builder (Clang-based)" references: - "https://docwiki.embarcadero.com/RADStudio/Athens/en/Command-Line_Interfaces_of_C++_Compilers" - "https://docwiki.embarcadero.com/RADStudio/Athens/en/BCC64X" executables: ["bcc64x", "bcc64", "bcc32x"] versioned: false cross_compilation: false slash_prefix: false flags: [] rizsotto-Bear-0c30b27/crates/semantic/compilers/fasm.yaml000066400000000000000000000016771527177720300235210ustar00rootroot00000000000000# fasm's CLI is `fasm [options] source [output]`: the output positional is # not recorded as an `output` field by this family -- it is simply not a # recognized source extension, so it is classified as a plain argument and # ignored. # # fasm has its own CLI dialect, so this family has no `extends:`. type: compiler compiler: id: fasm recognize: - description: "flat assembler" references: - "https://flatassembler.net/docs.php" executables: ["fasm"] versioned: false cross_compilation: false slash_prefix: false flags: # Memory limit (in KB, always a separate token, e.g. "-m 65536"). - match: {pattern: "-m", count: 1} result: none # Maximum number of passes. - match: {pattern: "-p", count: 1} result: none # Symbol file for debugging. - match: {pattern: "-s", count: 1} result: none # Define a symbol (name=value), glued or separate. - match: {pattern: "-d{ }*"} result: configures_preprocessing rizsotto-Bear-0c30b27/crates/semantic/compilers/flang.yaml000066400000000000000000000042701527177720300236520ustar00rootroot00000000000000type: compiler compiler: id: flang extends: gcc recognize: - description: "Flang" references: - "https://flang.llvm.org/docs/FlangDriver.html" executables: ["flang", "flang-new"] versioned: true cross_compilation: true - description: "AMD ROCm Flang" references: - "https://rocm.docs.amd.com/en/latest/" executables: ["amdflang"] versioned: false cross_compilation: false slash_prefix: false flags: - match: {pattern: "-J{ }*"} result: configures_compiling - match: {pattern: "-Xflang", count: 1} result: configures_compiling - match: {pattern: "-cpp"} result: configures_preprocessing - match: {pattern: "-nocpp"} result: configures_preprocessing - match: {pattern: "-fbackslash"} result: configures_compiling - match: {pattern: "-fno-backslash"} result: configures_compiling - match: {pattern: "-fconvert=*"} result: configures_compiling - match: {pattern: "-fdefault-real-8"} result: configures_compiling - match: {pattern: "-fdisable-real-10"} result: configures_compiling - match: {pattern: "-fdisable-real-3"} result: configures_compiling - match: {pattern: "-ffixed-form"} result: configures_compiling - match: {pattern: "-ffree-form"} result: configures_compiling - match: {pattern: "-fimplicit-none"} result: configures_compiling - match: {pattern: "-flarge-sizes"} result: configures_compiling - match: {pattern: "-fno-automatic"} result: configures_compiling - match: {pattern: "-frealloc-lhs"} result: configures_compiling - match: {pattern: "-fno-realloc-lhs"} result: configures_compiling - match: {pattern: "-funderscoring"} result: configures_compiling - match: {pattern: "-fno-underscoring"} result: configures_compiling - match: {pattern: "-funsigned"} result: configures_compiling - match: {pattern: "-fno-unsigned"} result: configures_compiling - match: {pattern: "-fxor-operator"} result: configures_compiling - match: {pattern: "-fno-xor-operator"} result: configures_compiling - match: {pattern: "-module-dir{ }*"} result: configures_compiling - match: {pattern: "--romc-path=*"} result: configures_linking rizsotto-Bear-0c30b27/crates/semantic/compilers/gcc.yaml000066400000000000000000000215701527177720300233210ustar00rootroot00000000000000type: compiler compiler: id: gcc recognize: - description: "GCC" references: - "https://gcc.gnu.org/onlinedocs/gcc/Option-Summary.html" executables: ["gcc", "g++", "gfortran", "egfortran", "f95"] versioned: true cross_compilation: true # Microchip XC8 drivers: xc8-cc (the gcc-styled driver of XC8 v2.30+) # and the legacy xc8 driver name. XC8's options are gcc-styled, so the # GCC table fits; xc16-gcc/xc32-gcc already match the cross-prefix rule # above. The XC8 archiver (xc8-ar) is deliberately absent -- it is not a # compiler driver. - description: "Microchip XC8" references: - "https://www.microchip.com/en-us/tools-resources/develop/mplab-xc-compilers" executables: ["xc8-cc", "xc8"] versioned: false cross_compilation: false # The bare names `cc` and `c++` are intentionally not listed here. They # are ambiguous (GCC on Linux, Clang on FreeBSD/OpenBSD/NetBSD/DragonFly # and macOS) and dispatch is owned by the `--version` probe in # semantic/src/interpreters/compilers/probe.rs. If the probe cannot # classify, recognition returns NotRecognized rather than guessing. ignore_when: executables: ["cc1", "cc1plus", "cc1obj", "cc1objplus", "f951", "collect2", "lto1"] slash_prefix: false flags: - match: {pattern: "-x", count: 1} result: configures_compiling - match: {pattern: "-c"} result: stops_at_compiling - match: {pattern: "-S"} result: stops_at_assembling - match: {pattern: "-E"} result: stops_at_preprocessing - match: {pattern: "-o{ }*"} result: output - match: {pattern: "-dumpbase", count: 1} result: driver_option - match: {pattern: "-dumpbase-ext", count: 1} result: driver_option - match: {pattern: "-dumpdir", count: 1} result: driver_option - match: {pattern: "-v"} result: driver_option - match: {pattern: "-###"} result: driver_option - match: {pattern: "--help*"} result: info_and_exit - match: {pattern: "--target-help"} result: info_and_exit - match: {pattern: "--version"} result: info_and_exit - match: {pattern: "-pass-exit-codes"} result: driver_option - match: {pattern: "-pipe"} result: driver_option - match: {pattern: "-specs=*"} result: driver_option - match: {pattern: "-wrapper", count: 1} result: driver_option - match: {pattern: "-ffile-prefix-map=*"} result: configures_compiling - match: {pattern: "-fplugin=*"} result: configures_compiling - match: {pattern: "-fplugin-arg-*"} result: configures_compiling - match: {pattern: "@*"} result: configures_compiling - match: {pattern: "-A{ }*"} result: configures_preprocessing - match: {pattern: "-D{ }*"} result: configures_preprocessing - match: {pattern: "-U{ }*"} result: configures_preprocessing - match: {pattern: "-include{ }*"} result: configures_preprocessing - match: {pattern: "-imacros", count: 1} result: configures_preprocessing - match: {pattern: "-undef"} result: configures_preprocessing - match: {pattern: "-pthread"} result: configures_preprocessing # -M and -MM imply -E: the compiler emits a dependency rule and stops # before compiling, so they suppress entry generation like -E does # (requirement output-compilation-entries). The other -M* flags only # modify dependency output and do not stop compilation. - match: {pattern: "-M"} result: stops_at_preprocessing - match: {pattern: "-MM"} result: stops_at_preprocessing - match: {pattern: "-MG"} result: configures_preprocessing - match: {pattern: "-MP"} result: configures_preprocessing - match: {pattern: "-MD"} result: configures_preprocessing - match: {pattern: "-MMD"} result: configures_preprocessing - match: {pattern: "-MF", count: 1} result: configures_preprocessing - match: {pattern: "-MT", count: 1} result: configures_preprocessing - match: {pattern: "-MQ", count: 1} result: configures_preprocessing - match: {pattern: "-C"} result: configures_preprocessing - match: {pattern: "-CC"} result: configures_preprocessing - match: {pattern: "-P"} result: configures_preprocessing - match: {pattern: "-traditional*"} result: configures_preprocessing - match: {pattern: "-trigraphs"} result: configures_preprocessing - match: {pattern: "-remap"} result: configures_preprocessing - match: {pattern: "-H"} result: configures_preprocessing - match: {pattern: "-Xpreprocessor", count: 1} result: configures_preprocessing - match: {pattern: "-Wp,*"} result: configures_preprocessing - match: {pattern: "-I{ }*"} result: configures_preprocessing - match: {pattern: "-iplugindir=*"} result: configures_preprocessing - match: {pattern: "-iquote", count: 1} result: configures_preprocessing - match: {pattern: "-isystem", count: 1} result: configures_preprocessing - match: {pattern: "-idirafter", count: 1} result: configures_preprocessing - match: {pattern: "-iprefix", count: 1} result: configures_preprocessing - match: {pattern: "-iwithprefix", count: 1} result: configures_preprocessing - match: {pattern: "-iwithprefixbefore", count: 1} result: configures_preprocessing - match: {pattern: "-isysroot", count: 1} result: configures_preprocessing - match: {pattern: "-imultilib", count: 1} result: configures_preprocessing - match: {pattern: "-L{ }*"} result: configures_linking - match: {pattern: "-B{ }*"} result: configures_preprocessing - match: {pattern: "--sysroot=*"} result: configures_preprocessing - match: {pattern: "-flinker-output=*"} result: configures_linking - match: {pattern: "-fuse-ld=*"} result: configures_linking - match: {pattern: "-l{ }*"} result: configures_linking - match: {pattern: "-nostartfiles"} result: configures_linking - match: {pattern: "-nodefaultlibs"} result: configures_linking - match: {pattern: "-nolibc"} result: configures_linking - match: {pattern: "-nostdlib"} result: configures_linking - match: {pattern: "-e", count: 1} result: configures_linking - match: {pattern: "-entry=*"} result: configures_linking - match: {pattern: "-pie"} result: configures_linking - match: {pattern: "-no-pie"} result: configures_linking - match: {pattern: "-static-pie"} result: configures_linking - match: {pattern: "-r"} result: configures_linking - match: {pattern: "-rdynamic"} result: configures_linking - match: {pattern: "-s"} result: configures_linking - match: {pattern: "-symbolic"} result: configures_linking - match: {pattern: "-static*"} result: configures_linking - match: {pattern: "-shared*"} result: configures_linking - match: {pattern: "-T", count: 1} result: configures_linking - match: {pattern: "-Xlinker", count: 1} result: configures_linking - match: {pattern: "-Wl,*"} result: configures_linking - match: {pattern: "-u", count: 1} result: configures_linking - match: {pattern: "-z", count: 1} result: configures_linking - match: {pattern: "-Xassembler", count: 1} result: configures_assembling - match: {pattern: "-Wa,*"} result: configures_assembling - match: {pattern: "-arch", count: 1} result: configures_compiling - match: {pattern: "-arch_only", count: 1} result: none - match: {pattern: "-ansi"} result: none - match: {pattern: "-aux-info", count: 1} result: none - match: {pattern: "-std{=}*"} result: configures_compiling - match: {pattern: "-O*"} result: configures_compiling - match: {pattern: "-g*"} result: configures_compiling # C++20 modules: explicit so the family stands out; the remaining # -f...modules spellings ride the -f* catch-all below. - match: {pattern: "-fmodule*"} result: configures_compiling - match: {pattern: "-f*"} result: configures_compiling - match: {pattern: "-m*"} result: configures_compiling - match: {pattern: "-p*"} result: none - match: {pattern: "-W*"} result: none - match: {pattern: "-no*"} result: none - match: {pattern: "-tno*"} result: none - match: {pattern: "-save*"} result: driver_option - match: {pattern: "-d*"} result: none - match: {pattern: "-Q*"} result: none - match: {pattern: "-X*"} result: none - match: {pattern: "-Y*"} result: none - match: {pattern: "--*"} result: none environment: - variable: CPATH effect: configures_preprocessing mapping: flag: "-I" separator: path - variable: C_INCLUDE_PATH effect: configures_preprocessing mapping: flag: "-isystem" separator: path - variable: CPLUS_INCLUDE_PATH effect: configures_preprocessing mapping: flag: "-isystem" separator: path - variable: OBJC_INCLUDE_PATH effect: configures_preprocessing mapping: flag: "-isystem" separator: path - variable: LIBRARY_PATH effect: configures_linking mapping: flag: "-L" separator: path rizsotto-Bear-0c30b27/crates/semantic/compilers/ibm_xl.yaml000066400000000000000000000051631527177720300240370ustar00rootroot00000000000000type: compiler compiler: id: ibm_xl # extends clang: also puts ibm_xl ahead of clang in recognition, so # "ibm-clang" is not first captured by clang's cross-compilation pattern. extends: clang recognize: - description: "IBM Open XL C/C++" references: - "https://www.ibm.com/docs/en/openxl-c-and-cpp-aix" executables: ["ibm-clang", "ibm-clang++"] versioned: true cross_compilation: false - description: "IBM XL C/C++" references: - "https://www.ibm.com/docs/en/xl-c-and-cpp-aix" executables: ["xlclang", "xlclang++"] versioned: true cross_compilation: false slash_prefix: false flags: # AltiVec/VSX - match: {pattern: "-qaltivec"} result: configures_compiling - match: {pattern: "-qnoaltivec"} result: configures_compiling # Architecture and tuning - match: {pattern: "-qtune{=}*"} result: configures_compiling - match: {pattern: "-qarch{=}*"} result: configures_compiling # High-order transformations - match: {pattern: "-qhot"} result: configures_compiling - match: {pattern: "-qnohot"} result: configures_compiling # SMP parallelization - match: {pattern: "-qsmp{=}*"} result: configures_compiling - match: {pattern: "-qnosmp"} result: configures_compiling # Interprocedural analysis - match: {pattern: "-qipa{=}*"} result: configures_compiling - match: {pattern: "-qnoipa"} result: configures_compiling # Profile-directed feedback - match: {pattern: "-qpdf1"} result: configures_compiling - match: {pattern: "-qpdf2"} result: configures_compiling - match: {pattern: "-qnopdf"} result: configures_compiling # Strict floating-point - match: {pattern: "-qstrict"} result: configures_compiling - match: {pattern: "-qnostrict"} result: configures_compiling # Memory limit for optimization - match: {pattern: "-qmaxmem{=}*"} result: configures_compiling # SIMD - match: {pattern: "-qsimd{=}*"} result: configures_compiling - match: {pattern: "-qnosimd"} result: configures_compiling # Optimization report - match: {pattern: "-qreport"} result: configures_compiling - match: {pattern: "-qnoreport"} result: configures_compiling # Listing file - match: {pattern: "-qlist"} result: configures_compiling - match: {pattern: "-qnolist"} result: configures_compiling # Inlining - match: {pattern: "-qinline{=}*"} result: configures_compiling - match: {pattern: "-qnoinline"} result: configures_compiling # Loop unrolling - match: {pattern: "-qunroll{=}*"} result: configures_compiling - match: {pattern: "-qnounroll"} result: configures_compiling rizsotto-Bear-0c30b27/crates/semantic/compilers/icecc.yaml000066400000000000000000000004731527177720300236320ustar00rootroot00000000000000type: wrapper recognize: - description: "Distributed compiler" references: - "https://github.com/icecc/icecream/blob/master/README.md" executables: ["icecc"] # icerun is deliberately absent: it runs arbitrary commands on the # cluster, not compiler invocations (see recognition-compiler-launchers). rizsotto-Bear-0c30b27/crates/semantic/compilers/intel_cc.yaml000066400000000000000000000132101527177720300243350ustar00rootroot00000000000000type: compiler compiler: id: intel_cc extends: clang recognize: - description: "Intel oneAPI C/C++" references: - "https://www.intel.com/content/www/us/en/developer/tools/oneapi/dpc-compiler.html" executables: ["icx", "icpx"] versioned: true cross_compilation: false - description: "Intel C++ Compiler Classic" references: - "https://www.intel.com/content/www/us/en/developer/tools/oneapi/dpc-compiler.html" executables: ["icc", "icpc"] versioned: true cross_compilation: false # Intel MPI wrappers: mpiicc/mpiicpc/mpiicx/mpiicpx are icc/icx with MPI # paths baked in, front-ending a gcc-compatible driver but accepting # Intel's flag surface (see docs/rationale/mpi-wrappers-as-compilers.md). # Not versioned/cross-compilation: those suffixes are not used for the # MPI wrapper names in practice. - description: "Intel MPI C/C++ wrapper" references: - "https://www.intel.com/content/www/us/en/developer/tools/oneapi/mpi-library.html" executables: ["mpiicc", "mpiicpc", "mpiicx", "mpiicpx"] versioned: false cross_compilation: false slash_prefix: false flags: # Microarchitecture targeting - match: {pattern: "-ax*"} result: configures_compiling - match: {pattern: "-x*"} result: configures_compiling # OpenMP (Intel -q prefix) - match: {pattern: "-qopenmp-link{=}*"} result: configures_linking - match: {pattern: "-qopenmp-stubs"} result: configures_compiling - match: {pattern: "-qopenmp-simd"} result: configures_compiling - match: {pattern: "-qopenmp"} result: configures_compiling - match: {pattern: "-qno-openmp"} result: configures_compiling # Optimization reports - match: {pattern: "-qopt-report-file{=}*"} result: configures_compiling - match: {pattern: "-qopt-report-phase{=}*"} result: configures_compiling - match: {pattern: "-qopt-report-format{=}*"} result: configures_compiling - match: {pattern: "-qopt-report{=}*"} result: configures_compiling # Optimization tuning - match: {pattern: "-qopt-zmm-usage{=}*"} result: configures_compiling - match: {pattern: "-qopt-assume-safe-padding"} result: configures_compiling - match: {pattern: "-qopt-streaming-stores{=}*"} result: configures_compiling - match: {pattern: "-qopt-prefetch{=}*"} result: configures_compiling - match: {pattern: "-qopt-mem-layout-trans{=}*"} result: configures_compiling - match: {pattern: "-qopt-multiple-gather-scatter-by-shuffles"} result: configures_compiling # MKL - match: {pattern: "-qmkl{=}*"} result: configures_linking # IPO (interprocedural optimization) - match: {pattern: "-ipo-c"} result: configures_compiling - match: {pattern: "-ipo-S"} result: configures_compiling - match: {pattern: "-ipo"} result: configures_compiling - match: {pattern: "-no-ipo"} result: configures_compiling - match: {pattern: "-ip"} result: configures_compiling # Parallelization - match: {pattern: "-parallel"} result: configures_compiling - match: {pattern: "-no-parallel"} result: configures_compiling - match: {pattern: "-par-threshold{=}*"} result: configures_compiling - match: {pattern: "-par-num-threads{=}*"} result: configures_compiling # Diagnostics - match: {pattern: "-diag-file-append{=}*"} result: none - match: {pattern: "-diag-file{=}*"} result: none - match: {pattern: "-diag-disable{=}*"} result: none - match: {pattern: "-diag-enable{=}*"} result: none - match: {pattern: "-diag-error{=}*"} result: none - match: {pattern: "-diag-warning{=}*"} result: none - match: {pattern: "-diag-remark{=}*"} result: none - match: {pattern: "-diag-dump"} result: none - match: {pattern: "-diag-id-numbers"} result: none # Debug and profiling - match: {pattern: "-debug", count: 1} result: configures_compiling - match: {pattern: "-prof-dir{ }*"} result: configures_compiling - match: {pattern: "-prof-gen"} result: configures_compiling - match: {pattern: "-prof-use"} result: configures_compiling # Intel-specific linking - match: {pattern: "-shared-intel"} result: configures_linking - match: {pattern: "-static-intel"} result: configures_linking - match: {pattern: "-shared-libgcc"} result: configures_linking - match: {pattern: "-static-libgcc"} result: configures_linking # Floating-point model - match: {pattern: "-fp-model{=}*"} result: configures_compiling - match: {pattern: "-ftz"} result: configures_compiling - match: {pattern: "-no-ftz"} result: configures_compiling - match: {pattern: "-prec-div"} result: configures_compiling - match: {pattern: "-no-prec-div"} result: configures_compiling - match: {pattern: "-prec-sqrt"} result: configures_compiling - match: {pattern: "-no-prec-sqrt"} result: configures_compiling - match: {pattern: "-fimf-precision{=}*"} result: configures_compiling # Vectorization - match: {pattern: "-vec"} result: configures_compiling - match: {pattern: "-no-vec"} result: configures_compiling - match: {pattern: "-simd"} result: configures_compiling - match: {pattern: "-no-simd"} result: configures_compiling # Other Intel-specific flags - match: {pattern: "-align"} result: configures_compiling - match: {pattern: "-heap-arrays{=}*"} result: configures_compiling - match: {pattern: "-traceback"} result: configures_compiling - match: {pattern: "-no-traceback"} result: configures_compiling - match: {pattern: "-V"} result: info_and_exit - match: {pattern: "-sox"} result: configures_compiling - match: {pattern: "-no-sox"} result: configures_compiling rizsotto-Bear-0c30b27/crates/semantic/compilers/intel_cl.yaml000066400000000000000000000010101527177720300243410ustar00rootroot00000000000000type: compiler compiler: id: intel_cl extends: clang_cl recognize: - description: "Intel oneAPI DPC++/C++ (MSVC mode)" references: - "https://www.intel.com/content/www/us/en/docs/oneapi/programming-guide/current/invoke-the-compiler.html" - "https://www.intel.com/content/www/us/en/docs/dpcpp-cpp-compiler/developer-guide-reference/2026-0/invoke-the-compiler.html" executables: ["icx-cl"] versioned: false cross_compilation: false slash_prefix: true response_file_syntax: msvc flags: [] rizsotto-Bear-0c30b27/crates/semantic/compilers/intel_fortran.yaml000066400000000000000000000122511527177720300254270ustar00rootroot00000000000000type: compiler compiler: id: intel_fortran recognize: - description: "Intel Fortran" references: - "https://www.intel.com/content/www/us/en/developer/tools/oneapi/fortran-compiler.html" executables: ["ifort", "ifx"] versioned: true cross_compilation: false # Intel MPI wrappers: mpiifort/mpiifx are ifort/ifx with MPI paths baked # in (see docs/rationale/mpi-wrappers-as-compilers.md). - description: "Intel Fortran MPI wrapper" references: - "https://www.intel.com/content/www/us/en/developer/tools/oneapi/mpi-library.html" executables: ["mpiifort", "mpiifx"] versioned: false cross_compilation: false slash_prefix: false flags: - match: {pattern: "-preprocess-only"} result: stops_at_preprocessing - match: {pattern: "-debug-parameters", count: 1} result: info_and_exit - match: {pattern: "-shared-intel"} result: configures_linking - match: {pattern: "-static-intel"} result: configures_linking - match: {pattern: "-static-libgcc"} result: configures_linking - match: {pattern: "-nogen-interfaces"} result: none - match: {pattern: "-shared-libgcc"} result: configures_linking - match: {pattern: "-gen-interfaces", count: 1} result: none - match: {pattern: "-nostartfiles"} result: configures_linking - match: {pattern: "-nodefaultlibs"} result: configures_linking - match: {pattern: "-gen-dep", count: 1} result: configures_preprocessing - match: {pattern: "-dumpmachine"} result: info_and_exit - match: {pattern: "--version"} result: info_and_exit - match: {pattern: "-nostdlib"} result: configures_linking - match: {pattern: "-pthread"} result: configures_preprocessing - match: {pattern: "-dryrun"} result: info_and_exit - match: {pattern: "-shared"} result: configures_linking - match: {pattern: "-static"} result: configures_linking - match: {pattern: "-isystem", count: 1} result: configures_preprocessing - match: {pattern: "-isysroot", count: 1} result: configures_preprocessing - match: {pattern: "-include", count: 1} result: configures_preprocessing - match: {pattern: "-iquote", count: 1} result: configures_preprocessing - match: {pattern: "-Xlinker", count: 1} result: configures_linking - match: {pattern: "-debug", count: 1} result: info_and_exit - match: {pattern: "-nofpp"} result: configures_preprocessing - match: {pattern: "-undef"} result: configures_preprocessing - match: {pattern: "--help"} result: info_and_exit - match: {pattern: "-MMD"} result: configures_preprocessing - match: {pattern: "-fpp"} result: configures_preprocessing - match: {pattern: "-Ep"} result: stops_at_preprocessing - match: {pattern: "-MF", count: 1} result: configures_preprocessing - match: {pattern: "-MD"} result: configures_preprocessing - match: {pattern: "-T", count: 1} result: configures_linking - match: {pattern: "-C"} result: configures_preprocessing - match: {pattern: "-u", count: 1} result: configures_linking - match: {pattern: "-V"} result: info_and_exit - match: {pattern: "-X"} result: configures_preprocessing - match: {pattern: "-c"} result: stops_at_compiling - match: {pattern: "-E"} result: stops_at_preprocessing - match: {pattern: "-o", count: 1} result: output - match: {pattern: "-P"} result: stops_at_preprocessing - match: {pattern: "-r"} result: configures_linking - match: {pattern: "-S"} result: stops_at_assembling - match: {pattern: "-s"} result: configures_linking - match: {pattern: "-Xoption,link*"} result: configures_linking - match: {pattern: "-Xoption,fpp*"} result: configures_preprocessing - match: {pattern: "-Xoption,cpp*"} result: configures_preprocessing - match: {pattern: "-Xoption,asm*"} result: none - match: {pattern: "--sysroot=*"} result: configures_preprocessing - match: {pattern: "-include{ }*"} result: configures_preprocessing - match: {pattern: "-D{ }*"} result: configures_preprocessing - match: {pattern: "-I{ }*"} result: configures_preprocessing - match: {pattern: "-L{ }*"} result: configures_linking - match: {pattern: "-l{ }*"} result: configures_linking - match: {pattern: "-U{ }*"} result: configures_preprocessing - match: {pattern: "-std=*"} result: configures_compiling - match: {pattern: "-diag-*"} result: none - match: {pattern: "--help*"} result: info_and_exit - match: {pattern: "-FA*"} result: info_and_exit - match: {pattern: "-Fa*"} result: info_and_exit - match: {pattern: "-Wl*"} result: configures_linking - match: {pattern: "-Wp*"} result: configures_preprocessing - match: {pattern: "--*"} result: none - match: {pattern: "-f*"} result: configures_compiling - match: {pattern: "-g*"} result: configures_compiling - match: {pattern: "-m*"} result: configures_compiling - match: {pattern: "-no*"} result: none - match: {pattern: "-O*"} result: configures_compiling - match: {pattern: "-v*"} result: driver_option - match: {pattern: "-x*"} result: configures_compiling - match: {pattern: "@*"} result: configures_compiling rizsotto-Bear-0c30b27/crates/semantic/compilers/mpi.yaml000066400000000000000000000033421527177720300233470ustar00rootroot00000000000000# MPI compiler wrappers (Open MPI, MPICH): mpicc, mpicxx, mpic++, mpiCC, # mpifort, mpif77, mpif90. These wrap a gcc-compatible driver and pass # arguments through to it, so flag parsing extends the GCC table. The # wrapper itself is recorded as the compiler (see # docs/rationale/mpi-wrappers-as-compilers.md); Intel MPI wrappers # (mpiicc, mpiifort, ...) are handled separately as extra basenames on # the Intel compiler families, not here. # # Open MPI wrapper docs: https://docs.open-mpi.org/en/main/building-apps/customizing-wrappers.html # MPICH wrapper docs: mpicc(1) -- -show/-compile_info/-link_info type: compiler compiler: id: mpi extends: gcc recognize: - description: "MPI compiler wrapper" references: - "https://www.open-mpi.org/doc/" - "https://www.mpich.org/documentation/" executables: ["mpicc", "mpicxx", "mpic++", "mpiCC", "mpifort", "mpif77", "mpif90"] versioned: true cross_compilation: false slash_prefix: false flags: # Wrapper-info invocations print the underlying compiler command and exit; # these never compile anything, so no database entry is produced for them. - match: {pattern: "-showme*"} result: info_and_exit - match: {pattern: "-show"} result: info_and_exit - match: {pattern: "-compile_info"} result: info_and_exit - match: {pattern: "-link_info"} result: info_and_exit # MPICH's compiler-override options. The optional '=' form must be # supported for both the glued (`-cc=gcc`) and separate (`-cc gcc`) # spellings, or the separate-token value would be misread as a source file. - match: {pattern: "-cc{=}*"} result: driver_option - match: {pattern: "-cxx{=}*"} result: driver_option - match: {pattern: "-fc{=}*"} result: driver_option rizsotto-Bear-0c30b27/crates/semantic/compilers/msvc.yaml000066400000000000000000000257441527177720300235440ustar00rootroot00000000000000type: compiler compiler: id: msvc recognize: - description: "Microsoft Visual C++" references: - "https://learn.microsoft.com/en-us/cpp/build/reference/compiler-options" executables: ["cl"] versioned: false cross_compilation: false ignore_when: executables: ["c1", "c1xx", "c2"] slash_prefix: true response_file_syntax: msvc flags: # --- Info and exit --- - match: {pattern: "/help"} result: info_and_exit - match: {pattern: "/?"} result: info_and_exit # --- Compilation control --- - match: {pattern: "/c"} result: stops_at_compiling - match: {pattern: "/EP"} result: stops_at_preprocessing - match: {pattern: "/E"} result: stops_at_preprocessing - match: {pattern: "/P"} result: stops_at_preprocessing # --- Output flags --- # /Fo and /Fe write their value four ways, and each needs its own rule: # # /Fo no value, so it names no file -> a plain flag # /Foa.obj glued, what CMake and Meson emit -> the prefix rule # /Fo:b.obj after a colon -> the colon rule # /Fo: c.obj colon, value in the next argument -> the colon rule # # The colon rules carry the colon in the flag itself, so "glued or # separate" covers the last two at once, and their longer flag sorts them # ahead of the prefix rules. # # The bare rule must stay above the prefix rule: both flags are the same # length, so the generated table keeps them in the order written here, and # the prefix rule would otherwise match a lone /Fo with an empty value, # leaving the entry to claim an output the build never named. - match: {pattern: "/Fo:{ }*"} result: output - match: {pattern: "/Fe:{ }*"} result: output - match: {pattern: "/Fo"} result: none - match: {pattern: "/Fe"} result: none - match: {pattern: "/Fo*"} result: output - match: {pattern: "/Fe*"} result: output - match: {pattern: "/Fd*"} result: configures_compiling # /FA[c][s][u] selects the listing contents and takes no path at all; # /Fa names the listing file. The two are distinguished by case. - match: {pattern: "/FA*"} result: configures_compiling - match: {pattern: "/Fa*"} result: configures_compiling - match: {pattern: "/Fi*"} result: configures_preprocessing - match: {pattern: "/Fm*"} result: configures_linking - match: {pattern: "/FR*"} result: configures_compiling - match: {pattern: "/doc{:}*"} result: configures_compiling # --- Include/define --- - match: {pattern: "/external:I{ }*"} result: configures_preprocessing - match: {pattern: "/external:anglebrackets"} result: none - match: {pattern: "/external:env:*"} result: configures_preprocessing - match: {pattern: "/external:W*"} result: none - match: {pattern: "/I{ }*"} result: configures_preprocessing - match: {pattern: "/D{ }*"} result: configures_preprocessing - match: {pattern: "/U{ }*"} result: configures_preprocessing - match: {pattern: "/FI{ }*"} result: configures_preprocessing - match: {pattern: "/u"} result: configures_preprocessing - match: {pattern: "/X"} result: configures_preprocessing - match: {pattern: "/showIncludes"} result: configures_preprocessing # --- Standard/language --- - match: {pattern: "/std:*"} result: configures_compiling - match: {pattern: "/Tc{ }*"} result: configures_compiling - match: {pattern: "/Tp{ }*"} result: configures_compiling - match: {pattern: "/TC"} result: configures_compiling - match: {pattern: "/TP"} result: configures_compiling - match: {pattern: "/Zc:*"} result: configures_compiling - match: {pattern: "/permissive-"} result: configures_compiling # --- Optimization --- - match: {pattern: "/O1"} result: configures_compiling - match: {pattern: "/O2"} result: configures_compiling - match: {pattern: "/Ob*"} result: configures_compiling - match: {pattern: "/Od"} result: configures_compiling - match: {pattern: "/Og"} result: configures_compiling - match: {pattern: "/Oi"} result: configures_compiling - match: {pattern: "/Os"} result: configures_compiling - match: {pattern: "/Ot"} result: configures_compiling - match: {pattern: "/Ox"} result: configures_compiling - match: {pattern: "/Oy"} result: configures_compiling - match: {pattern: "/GL"} result: configures_compiling - match: {pattern: "/Gw"} result: configures_compiling - match: {pattern: "/Gy"} result: configures_compiling # --- Code generation --- - match: {pattern: "/arch:*"} result: configures_compiling - match: {pattern: "/EH*"} result: configures_compiling - match: {pattern: "/fp:*"} result: configures_compiling - match: {pattern: "/favor:*"} result: configures_compiling - match: {pattern: "/GA"} result: configures_compiling - match: {pattern: "/Gd"} result: configures_compiling - match: {pattern: "/Ge"} result: configures_compiling - match: {pattern: "/GF"} result: configures_compiling - match: {pattern: "/Gh"} result: configures_compiling - match: {pattern: "/GH"} result: configures_compiling - match: {pattern: "/Gr"} result: configures_compiling - match: {pattern: "/GR-"} result: configures_compiling - match: {pattern: "/GR"} result: configures_compiling - match: {pattern: "/Gs*"} result: configures_compiling - match: {pattern: "/GT"} result: configures_compiling - match: {pattern: "/guard:*"} result: configures_compiling - match: {pattern: "/Gv"} result: configures_compiling - match: {pattern: "/Gz"} result: configures_compiling - match: {pattern: "/GZ"} result: configures_compiling - match: {pattern: "/volatile:*"} result: configures_compiling # --- Runtime library --- - match: {pattern: "/MDd"} result: configures_compiling - match: {pattern: "/MD"} result: configures_compiling - match: {pattern: "/MTd"} result: configures_compiling - match: {pattern: "/MT"} result: configures_compiling # --- CLR --- - match: {pattern: "/clr*"} result: configures_compiling # --- Runtime checks --- - match: {pattern: "/RTC*"} result: configures_compiling - match: {pattern: "/sdl"} result: configures_compiling # --- Spectre/parallel --- - match: {pattern: "/Qspectre-load"} result: configures_compiling - match: {pattern: "/Qspectre"} result: configures_compiling - match: {pattern: "/Qpar"} result: configures_compiling - match: {pattern: "/Qfast_transcendentals"} result: configures_compiling - match: {pattern: "/Qvec-report:*"} result: configures_compiling # --- Debug info --- - match: {pattern: "/ZI"} result: configures_compiling - match: {pattern: "/Zi"} result: configures_compiling - match: {pattern: "/Z7"} result: configures_compiling - match: {pattern: "/Zf"} result: configures_compiling - match: {pattern: "/ZH:*"} result: configures_compiling - match: {pattern: "/Zo-"} result: configures_compiling - match: {pattern: "/Zo"} result: configures_compiling # --- Warnings --- - match: {pattern: "/Wall"} result: none - match: {pattern: "/WX-"} result: none - match: {pattern: "/WX"} result: none - match: {pattern: "/W0"} result: none - match: {pattern: "/W1"} result: none - match: {pattern: "/W2"} result: none - match: {pattern: "/W3"} result: none - match: {pattern: "/W4"} result: none - match: {pattern: "/w"} result: none # /Wv[:version]: the value is optional (cl uses the current compiler version # when omitted), so we accept both the bare form and "/Wv:". - match: {pattern: "/Wv:*"} result: none - match: {pattern: "/Wv"} result: none # cl.exe accepts the warning-number value either glued ("/wd4995") or as a # separate argument ("/wd 4995") for all per-warning options; the patterns # below must consume both forms. - match: {pattern: "/w1{ }*"} result: none - match: {pattern: "/w2{ }*"} result: none - match: {pattern: "/w3{ }*"} result: none - match: {pattern: "/w4{ }*"} result: none - match: {pattern: "/wd{ }*"} result: none - match: {pattern: "/we{ }*"} result: none - match: {pattern: "/wo{ }*"} result: none - match: {pattern: "/diagnostics:*"} result: none - match: {pattern: "/errorReport:*"} result: none - match: {pattern: "/FC"} result: none # --- Precompiled headers --- # /Yc and /Yu take an optional glued filename ("/Yc", "/Ycstdafx.h") and # /Fp a required glued one; none of them reads a separate argument. - match: {pattern: "/Yc*"} result: configures_compiling - match: {pattern: "/Yu*"} result: configures_compiling - match: {pattern: "/Fp*"} result: configures_compiling - match: {pattern: "/YX"} result: configures_compiling - match: {pattern: "/Y-"} result: configures_compiling # --- Linking --- - match: {pattern: "/link"} result: pass_through # /F sets the stack size and spells its value as a separate argument # ("/F 1024"); it must never absorb a glued /F-family option. - match: {pattern: "/F", count: 1} result: configures_linking - match: {pattern: "/LDd"} result: configures_linking - match: {pattern: "/LD"} result: configures_linking - match: {pattern: "/LN"} result: configures_linking # --- Misc compilation --- - match: {pattern: "/source-charset:*"} result: configures_compiling - match: {pattern: "/execution-charset:*"} result: configures_compiling - match: {pattern: "/utf-8"} result: configures_compiling - match: {pattern: "/validate-charset"} result: configures_compiling - match: {pattern: "/bigobj"} result: configures_compiling - match: {pattern: "/J"} result: configures_compiling - match: {pattern: "/openmp*"} result: configures_compiling - match: {pattern: "/AI{ }*"} result: configures_compiling - match: {pattern: "/FU{ }*"} result: configures_compiling - match: {pattern: "/await:strict"} result: configures_compiling - match: {pattern: "/await"} result: configures_compiling - match: {pattern: "/constexpr:*"} result: configures_compiling # --- Analysis --- - match: {pattern: "/analyze-"} result: configures_compiling - match: {pattern: "/analyze:*"} result: configures_compiling - match: {pattern: "/analyze"} result: configures_compiling # --- Driver options --- - match: {pattern: "/nologo"} result: driver_option - match: {pattern: "/MP*"} result: driver_option - match: {pattern: "/FS"} result: driver_option environment: - variable: CL effect: configures_compiling mapping: expand: prepend separator: space - variable: _CL_ effect: configures_compiling mapping: expand: append separator: space - variable: INCLUDE effect: configures_preprocessing mapping: flag: "/I" separator: ";" - variable: LIB effect: configures_linking mapping: flag: "/LIBPATH:" separator: ";" rizsotto-Bear-0c30b27/crates/semantic/compilers/nasm.yaml000066400000000000000000000074671527177720300235340ustar00rootroot00000000000000# NASM/YASM have their own CLI dialect (not derived from gcc/clang), so this # family has no `extends:`. type: compiler compiler: id: nasm recognize: - description: "NASM / YASM assembler" references: - "https://www.nasm.us/doc/nasmdoc2.html" - "https://github.com/yasm/yasm" executables: ["nasm", "yasm"] versioned: false cross_compilation: false slash_prefix: false flags: # --- Output format / output file --- - match: {pattern: "-f{ }*"} result: configures_compiling - match: {pattern: "-o{ }*"} result: output # --- Preprocessor --- # NASM documents the lowercase forms as the canonical spellings; the # uppercase ones are the C-compiler-compatible aliases. Both consume a # glued or separate value, so a define like `-d NAME=release.asm` can # never leak its value into source detection. - match: {pattern: "-I{ }*"} result: configures_preprocessing - match: {pattern: "-i{ }*"} result: configures_preprocessing - match: {pattern: "-D{ }*"} result: configures_preprocessing - match: {pattern: "-d{ }*"} result: configures_preprocessing - match: {pattern: "-U{ }*"} result: configures_preprocessing - match: {pattern: "-u{ }*"} result: configures_preprocessing # -P/-p: pre-include a file before assembling (case-sensitive, both forms # exist across NASM/YASM). - match: {pattern: "-P{ }*"} result: configures_preprocessing - match: {pattern: "-p{ }*"} result: configures_preprocessing # Dependency-generation options, mirroring gcc's -M family so their # separate-token value is consumed rather than read as a source file. - match: {pattern: "-MF{ }*"} result: configures_preprocessing - match: {pattern: "-MD{ }*"} result: configures_preprocessing - match: {pattern: "-MT{ }*"} result: configures_preprocessing - match: {pattern: "-MQ{ }*"} result: configures_preprocessing # -M and -MG emit Makefile dependencies and stop: nothing is assembled, # so they suppress entry generation like -E does (requirement # output-compilation-entries). -MD assembles and generates the dependency # file as a side effect; the remaining -M* flags only shape the # dependency output. - match: {pattern: "-M"} result: stops_at_preprocessing - match: {pattern: "-MG"} result: stops_at_preprocessing - match: {pattern: "-MP"} result: configures_preprocessing - match: {pattern: "-MW"} result: configures_preprocessing - match: {pattern: "-E"} result: stops_at_preprocessing - match: {pattern: "-e"} result: stops_at_preprocessing # --- Compilation-affecting, no consumed value --- - match: {pattern: "-l{ }*"} result: configures_compiling - match: {pattern: "-F{ }*"} result: configures_compiling - match: {pattern: "-a"} result: configures_compiling - match: {pattern: "-t"} result: configures_compiling - match: {pattern: "-g*"} result: configures_compiling - match: {pattern: "-O*"} result: configures_compiling - match: {pattern: "-w*"} result: configures_compiling - match: {pattern: "-W*"} result: configures_compiling - match: {pattern: "-X*"} result: configures_compiling # --- No semantic effect, but must consume a glued/separate value --- - match: {pattern: "-Z{ }*"} result: none - match: {pattern: "-s"} result: none # --- Info and exit --- - match: {pattern: "-v"} result: info_and_exit - match: {pattern: "-h"} result: info_and_exit # YASM's long options (--prefix=, --postfix=, --version, ...) carry their # value after "=" when they take one -- never as a separate token -- so a # 0-arg catch-all is safe here: it can never swallow a following source # file. This also covers `yasm --version` (no entry either way, since # there is no source argument to classify). - match: {pattern: "--*"} result: none rizsotto-Bear-0c30b27/crates/semantic/compilers/nvidia_hpc.yaml000066400000000000000000000144241527177720300246710ustar00rootroot00000000000000type: compiler compiler: id: nvidia_hpc recognize: - description: "NVIDIA HPC SDK" references: - "https://docs.nvidia.com/hpc-sdk/compilers/hpc-compilers-user-guide/index.html" executables: ["nvc", "nvc++", "nvfortran"] versioned: true cross_compilation: false - description: "PGI (legacy NVIDIA HPC)" references: - "https://docs.nvidia.com/hpc-sdk/compilers/hpc-compilers-user-guide/index.html" executables: ["pgcc", "pgc++", "pgfortran"] versioned: true cross_compilation: false slash_prefix: false flags: # Output - match: {pattern: "-o{ }*"} result: output # Compilation control - match: {pattern: "-c"} result: stops_at_compiling - match: {pattern: "-S"} result: stops_at_assembling - match: {pattern: "-E"} result: stops_at_preprocessing # Include/define - match: {pattern: "-I{ }*"} result: configures_preprocessing - match: {pattern: "-D{ }*"} result: configures_preprocessing - match: {pattern: "-U{ }*"} result: configures_preprocessing # Standard - match: {pattern: "-std{=}*"} result: configures_compiling # Optimization - match: {pattern: "-O*"} result: configures_compiling - match: {pattern: "-fast"} result: configures_compiling - match: {pattern: "-Minline"} result: configures_compiling - match: {pattern: "-Mnoinline"} result: configures_compiling - match: {pattern: "-Munroll"} result: configures_compiling - match: {pattern: "-Mnounroll"} result: configures_compiling - match: {pattern: "-Mvect{=}*"} result: configures_compiling - match: {pattern: "-Mnovect"} result: configures_compiling - match: {pattern: "-Mconcur{=}*"} result: configures_compiling - match: {pattern: "-Mnoconcur"} result: configures_compiling - match: {pattern: "-Mipa{=}*"} result: configures_compiling - match: {pattern: "-Mnoipa"} result: configures_compiling - match: {pattern: "-Mfprelaxed"} result: configures_compiling - match: {pattern: "-Mnofprelaxed"} result: configures_compiling - match: {pattern: "-Mlre"} result: configures_compiling - match: {pattern: "-Mnolre"} result: configures_compiling - match: {pattern: "-Mautoinline"} result: configures_compiling - match: {pattern: "-Mnoautoinline"} result: configures_compiling # GPU/Accelerator - match: {pattern: "-acc{=}*"} result: configures_compiling - match: {pattern: "-noacc"} result: configures_compiling - match: {pattern: "-gpu{=}*"} result: configures_compiling - match: {pattern: "-cuda"} result: configures_compiling - match: {pattern: "-nocuda"} result: configures_compiling - match: {pattern: "-mp{=}*"} result: configures_compiling - match: {pattern: "-nomp"} result: configures_compiling - match: {pattern: "-Mcuda{=}*"} result: configures_compiling - match: {pattern: "-Minfo{=}*"} result: none - match: {pattern: "-Mneginfo{=}*"} result: none # Target - match: {pattern: "-tp{=}*"} result: configures_compiling - match: {pattern: "-m32"} result: configures_compiling - match: {pattern: "-m64"} result: configures_compiling - match: {pattern: "-mcmodel{=}*"} result: configures_compiling # Debug/profiling - match: {pattern: "-g"} result: configures_compiling - match: {pattern: "-gopt"} result: configures_compiling - match: {pattern: "-pg"} result: configures_compiling - match: {pattern: "-Mprof{=}*"} result: configures_compiling # Linking - match: {pattern: "-L{ }*"} result: configures_linking - match: {pattern: "-l{ }*"} result: configures_linking - match: {pattern: "-shared"} result: configures_linking - match: {pattern: "-static"} result: configures_linking - match: {pattern: "-Wl,*"} result: configures_linking - match: {pattern: "-rpath{ }*"} result: configures_linking - match: {pattern: "-soname{ }*"} result: configures_linking - match: {pattern: "-Bstatic"} result: configures_linking - match: {pattern: "-Bdynamic"} result: configures_linking - match: {pattern: "-r"} result: configures_linking - match: {pattern: "-nostdlib"} result: configures_linking - match: {pattern: "-nodefaultlibs"} result: configures_linking - match: {pattern: "-Mnostdlib"} result: configures_linking # Warnings - match: {pattern: "-w"} result: none - match: {pattern: "-W*"} result: none - match: {pattern: "-Minform{=}*"} result: none # Preprocessing - match: {pattern: "-Mpreprocess"} result: configures_preprocessing - match: {pattern: "-Mnostdinc"} result: configures_preprocessing # -M behavioral flags - match: {pattern: "-Msave"} result: configures_compiling - match: {pattern: "-Mnosave"} result: configures_compiling - match: {pattern: "-Mbounds"} result: configures_compiling - match: {pattern: "-Mnobounds"} result: configures_compiling - match: {pattern: "-Mchkstk"} result: configures_compiling - match: {pattern: "-Mnochkstk"} result: configures_compiling - match: {pattern: "-Mflushz"} result: configures_compiling - match: {pattern: "-Mnoflushz"} result: configures_compiling - match: {pattern: "-Mdaz"} result: configures_compiling - match: {pattern: "-Mnodaz"} result: configures_compiling - match: {pattern: "-Mfma"} result: configures_compiling - match: {pattern: "-Mnofma"} result: configures_compiling - match: {pattern: "-Mstack_arrays"} result: configures_compiling - match: {pattern: "-Mnostack_arrays"} result: configures_compiling # Misc - match: {pattern: "-V"} result: info_and_exit - match: {pattern: "--version"} result: info_and_exit - match: {pattern: "-v"} result: driver_option - match: {pattern: "-###"} result: info_and_exit - match: {pattern: "-help"} result: info_and_exit - match: {pattern: "--help"} result: info_and_exit - match: {pattern: "-show"} result: info_and_exit - match: {pattern: "-dryrun"} result: driver_option - match: {pattern: "-Mkeepasm"} result: configures_compiling - match: {pattern: "-Manno"} result: configures_compiling - match: {pattern: "-noswitcherror"} result: driver_option - match: {pattern: "-Werror"} result: none - match: {pattern: "@*"} result: configures_compiling rizsotto-Bear-0c30b27/crates/semantic/compilers/qnx.yaml000066400000000000000000000021131527177720300233630ustar00rootroot00000000000000# QNX Neutrino RTOS compiler drivers: qcc, q++. QNX 8 ships a GCC 12.2-based # toolchain (see # https://www.qnx.com/developers/docs/8.0/com.qnx.doc.qnxsdp.migration/topic/toolchain.html), # so flag parsing extends the GCC table. # # The "-V" variant selector picks the target/compiler variant, e.g. # `-Vgcc_ntoaarch64le`. It is modeled as attached-value only (prefix match, # 0 extra args): both the attached form (`-Vgcc_ntoaarch64le`) and the bare # form (`-V`, which lists available variants) are a single token that must # never be split and must never swallow a following source file. QNX does # not accept a separate-token spelling (`-V gcc_ntoaarch64le` would be # read as `-V` plus an unrelated positional argument), so no `{ }` form is # needed here, unlike MPI's `-cc{=}*`. type: compiler compiler: id: qnx extends: gcc recognize: - description: "QNX qcc" references: - "https://www.qnx.com/developers/docs/" executables: ["qcc", "q++"] versioned: false cross_compilation: false slash_prefix: false flags: - match: {pattern: "-V*"} result: driver_option rizsotto-Bear-0c30b27/crates/semantic/compilers/sccache.yaml000066400000000000000000000002511527177720300241470ustar00rootroot00000000000000type: wrapper recognize: - description: "Compiler cache" references: - "https://github.com/mozilla/sccache/blob/main/README.md" executables: ["sccache"] rizsotto-Bear-0c30b27/crates/semantic/compilers/swift.yaml000066400000000000000000000136701527177720300237230ustar00rootroot00000000000000# https://github.com/swiftlang/swift/blob/main/include/swift/Option/Options.td # is the authoritative option table this file is verified against. # # swiftc has its own CLI dialect (not derived from gcc/clang), so this family # has no `extends:`. Arity matters more here than for most families: most # value-taking long options are Separate-only in Options.td (`-target x`, # never `-targetx`), unlike gcc/clang's glued-or-separate convention, so they # are modeled with `count: 1` (exact match, separate token only) rather than # the `{ }*` glued-or-separate syntax. `-D`/`-I`/`-F`/`-L`/`-l`/`-o`/`-j` ARE # JoinedOrSeparate in Options.td and use `{ }*` accordingly. # # `swift` (the package-manager subcommand driver: `swift build`, `swift run`, # ...) is deliberately not recognized -- see recognition-compiler-names # Notes. Only `swiftc` is listed under `recognize:`. # # swiftc spawns per-file `swift-frontend` jobs the way gcc spawns `cc1` # (legacy toolchains self-invoke as `swiftc -frontend`); both are filtered # via `ignore_when`, the same mechanism gcc.yaml/clang.yaml use for their # own internal frontend executions. type: compiler compiler: id: swift recognize: - description: "Swift" references: - "https://www.swift.org/documentation/" executables: ["swiftc"] versioned: false cross_compilation: false ignore_when: executables: ["swift-frontend"] flags: ["-frontend"] slash_prefix: false # swiftc whole-module: one entry per source, each keeping the full invocation. source_mode: per-source-full flags: # --- Output --- - match: {pattern: "-o{ }*"} result: output # --- Stops at compiling (object-file-only output) --- - match: {pattern: "-emit-object"} result: stops_at_compiling - match: {pattern: "-c"} result: stops_at_compiling # --- Compilation-configuring, separate-token value only (Options.td: Separate) --- - match: {pattern: "-module-name", count: 1} result: configures_compiling - match: {pattern: "-sdk", count: 1} result: configures_compiling - match: {pattern: "-target", count: 1} result: configures_compiling - match: {pattern: "-swift-version", count: 1} result: configures_compiling - match: {pattern: "-emit-module-path", count: 1} result: configures_compiling - match: {pattern: "-index-store-path", count: 1} result: configures_compiling # Bridging header for mixed Swift/Objective-C targets; the value is a # `.h` path, which IS a recognized source extension (headers are TUs for # other families), so an unmatched `-import-objc-header` would leak its # value in as a phantom compilable source. - match: {pattern: "-import-objc-header", count: 1} result: configures_compiling - match: {pattern: "-emit-objc-header-path", count: 1} result: configures_compiling # Swift 5.9+ C++-interop generalization of the ObjC header emission; # same .h-valued separate-token shape, same phantom-source risk. - match: {pattern: "-emit-clang-header-path", count: 1} result: configures_compiling # --- Linking-configuring, separate-token value only --- - match: {pattern: "-framework", count: 1} result: configures_linking # --- Forwarded to a downstream tool; ambiguous, so classified to keep # rather than strip (dropping a compile-relevant forwarded flag is # worse than keeping a harmless one -- same reasoning as valac's -X). --- - match: {pattern: "-Xcc", count: 1} result: configures_compiling - match: {pattern: "-Xfrontend", count: 1} result: configures_compiling - match: {pattern: "-Xlinker", count: 1} result: configures_linking # --- Driver/build-machinery, separate-token value only; no compilation # semantic of their own, but must consume their value. --- - match: {pattern: "-output-file-map", count: 1} result: none - match: {pattern: "-module-cache-path", count: 1} result: none - match: {pattern: "-working-directory", count: 1} result: none - match: {pattern: "-num-threads", count: 1} result: none # --- JoinedOrSeparate value-taking flags --- - match: {pattern: "-j{ }*"} result: none - match: {pattern: "-D{ }*"} result: configures_preprocessing - match: {pattern: "-I{ }*"} result: configures_preprocessing - match: {pattern: "-F{ }*"} result: configures_preprocessing - match: {pattern: "-L{ }*"} result: configures_linking - match: {pattern: "-l{ }*"} result: configures_linking # --- Zero-arg compilation-configuring flags --- - match: {pattern: "-emit-library"} result: configures_compiling - match: {pattern: "-emit-executable"} result: configures_compiling - match: {pattern: "-emit-module"} result: configures_compiling - match: {pattern: "-whole-module-optimization"} result: configures_compiling - match: {pattern: "-wmo"} result: configures_compiling - match: {pattern: "-parse-as-library"} result: configures_compiling - match: {pattern: "-enable-testing"} result: configures_compiling - match: {pattern: "-static"} result: configures_compiling - match: {pattern: "-Onone"} result: configures_compiling - match: {pattern: "-Osize"} result: configures_compiling - match: {pattern: "-O"} result: configures_compiling - match: {pattern: "-g"} result: configures_compiling # --- Verbose: does not suppress entry generation, unlike --version/--help. # `swiftc -v -c hello.swift` still compiles; only bare `-v` with no # compile action produces no source argument to build an entry from. --- - match: {pattern: "-v"} result: none # --- Info and exit --- (Options.td: Flag<["-", "--"], "version"|"help">, # i.e. both single- and double-dash spellings are valid and must each be # listed since this schema matches literal strings, not prefix sets.) - match: {pattern: "--version"} result: info_and_exit - match: {pattern: "-version"} result: info_and_exit - match: {pattern: "--help"} result: info_and_exit - match: {pattern: "-help"} result: info_and_exit rizsotto-Bear-0c30b27/crates/semantic/compilers/ti_cgt.yaml000066400000000000000000000215401527177720300240330ustar00rootroot00000000000000# TI's classic Code Generation Tools: the pre-Clang drivers Code Composer # Studio still generates makefiles for. One option dialect is shared across # every target (the target-specific part is the `--silicon_version` value), # and it is not derived from gcc's, so this family has no `extends:`. # # TI's newer Clang-based driver (`tiarmclang`) is a different tool with # gcc-style options; it is recognized under the `clang` family, not here. type: compiler compiler: id: ti_cgt recognize: - description: "TI Code Generation Tools (Arm)" references: - "https://www.ti.com/lit/ug/spnu151v/spnu151v.pdf" executables: ["armcl"] versioned: false cross_compilation: false - description: "TI Code Generation Tools (C6000)" references: - "https://www.ti.com/lit/ug/spru187/spru187.pdf" executables: ["cl6x"] versioned: false cross_compilation: false - description: "TI Code Generation Tools (C7000)" references: - "https://www.ti.com/lit/ug/spruig8/spruig8.pdf" executables: ["cl7x"] versioned: false cross_compilation: false - description: "TI Code Generation Tools (C2000)" references: - "https://www.ti.com/lit/ug/spru514/spru514.pdf" executables: ["cl2000"] versioned: false cross_compilation: false - description: "TI Code Generation Tools (MSP430)" references: - "https://www.ti.com/lit/ug/slau132/slau132.pdf" executables: ["cl430"] versioned: false cross_compilation: false - description: "TI Code Generation Tools (PRU)" references: - "https://www.ti.com/lit/ug/spruhv7/spruhv7.pdf" executables: ["clpru"] versioned: false cross_compilation: false slash_prefix: false flags: # --- Control: what the driver stops after --- # `-c` and `-n` mirror gcc's `-c` and `-S`. `-z` hands everything that # follows to the linker, which is how a CCS makefile spells its link # step: same driver, object files and linker options after the `-z`. - match: {pattern: "--compile_only"} result: stops_at_compiling - match: {pattern: "-c"} result: stops_at_compiling - match: {pattern: "--skip_assembler"} result: stops_at_assembling - match: {pattern: "-n"} result: stops_at_assembling - match: {pattern: "--run_linker"} result: pass_through - match: {pattern: "-z"} result: pass_through # --- Preprocessing --- - match: {pattern: "--include_path=*"} result: configures_preprocessing - match: {pattern: "-I{ }*"} result: configures_preprocessing - match: {pattern: "--define=*"} result: configures_preprocessing - match: {pattern: "-D{ }*"} result: configures_preprocessing - match: {pattern: "--undefine=*"} result: configures_preprocessing - match: {pattern: "-U{ }*"} result: configures_preprocessing - match: {pattern: "--preinclude=*"} result: configures_preprocessing # The `-pp` family. Three of them write preprocessed output and # compile nothing, so they suppress the entry the way `-E` does. The # other three write a side file (dependencies, include list, macro # list); on their own they also stop before compiling, but Code # Composer Studio always pairs them with `--preproc_with_compile`, # which resumes the compilation. A flag table cannot express "unless # `-ppa` is also present", and dropping every CCS compile is the worse # of the two errors, so these three stay non-terminal. - match: {pattern: "--preproc_only"} result: stops_at_preprocessing - match: {pattern: "-ppo"} result: stops_at_preprocessing - match: {pattern: "--preproc_with_comment"} result: stops_at_preprocessing - match: {pattern: "-ppc"} result: stops_at_preprocessing - match: {pattern: "--preproc_with_line"} result: stops_at_preprocessing - match: {pattern: "-ppl"} result: stops_at_preprocessing - match: {pattern: "--preproc_dependency=*"} result: configures_preprocessing - match: {pattern: "-ppd*"} result: configures_preprocessing - match: {pattern: "--preproc_includes=*"} result: configures_preprocessing - match: {pattern: "-ppi*"} result: configures_preprocessing - match: {pattern: "--preproc_macros=*"} result: configures_preprocessing - match: {pattern: "-ppm*"} result: configures_preprocessing - match: {pattern: "--preproc_with_compile"} result: configures_preprocessing - match: {pattern: "-ppa"} result: configures_preprocessing # --- Output naming --- # `--output_file` names the compilation's output; the `_directory` # options only say where files land, so they configure the compile # rather than name its output (the entry's output field must stay a # file, never a directory). - match: {pattern: "--output_file=*"} result: output - match: {pattern: "-fe{ }*"} result: output - match: {pattern: "--obj_directory=*"} result: configures_compiling - match: {pattern: "-fr{ }*"} result: configures_compiling - match: {pattern: "--asm_directory=*"} result: configures_compiling - match: {pattern: "-fs{ }*"} result: configures_compiling - match: {pattern: "--list_directory=*"} result: configures_compiling - match: {pattern: "-ff{ }*"} result: configures_compiling - match: {pattern: "--abs_directory=*"} result: configures_compiling - match: {pattern: "-fb{ }*"} result: configures_compiling - match: {pattern: "--temp_directory=*"} result: configures_compiling - match: {pattern: "-ft{ }*"} result: configures_compiling - match: {pattern: "--pp_directory=*"} result: configures_preprocessing # --- File-type overrides: the value is a source path, so it must be # consumed as this flag's value and not classified a second time --- - match: {pattern: "--c_file=*"} result: configures_compiling - match: {pattern: "-fc{ }*"} result: configures_compiling - match: {pattern: "--cpp_file=*"} result: configures_compiling - match: {pattern: "-fp{ }*"} result: configures_compiling - match: {pattern: "--asm_file=*"} result: configures_assembling - match: {pattern: "-fa{ }*"} result: configures_assembling - match: {pattern: "--obj_file=*"} result: configures_linking - match: {pattern: "-fo{ }*"} result: configures_linking # --- Assembler --- - match: {pattern: "--asm_define=*"} result: configures_assembling - match: {pattern: "-ad{ }*"} result: configures_assembling - match: {pattern: "--asm_undefine=*"} result: configures_assembling - match: {pattern: "-au{ }*"} result: configures_assembling # -aa, -al, -apd, -api: listing and dependency switches, no value. - match: {pattern: "-a*"} result: configures_assembling # --- Linking (reached only when `-z` did not already stop parsing) --- - match: {pattern: "--library=*"} result: configures_linking - match: {pattern: "-l{ }*"} result: configures_linking - match: {pattern: "--search_path=*"} result: configures_linking - match: {pattern: "-i{ }*"} result: configures_linking # --- Command files --- # TI spells a response file `-@file`, not gcc's `@file`, so Bear's # response-file inlining does not apply; the argument is recorded as # written. - match: {pattern: "--cmd_file=*"} result: none - match: {pattern: "-@{ }*"} result: none # --- Code generation, diagnostics, and the rest of the short forms --- # Every one of these glues its value when it takes one (`-mv7R4`, # `-O2`, `-pds225`), so a prefix rule that consumes no separate # argument cannot swallow a source file. - match: {pattern: "-m*"} result: configures_compiling - match: {pattern: "-O*"} result: configures_compiling - match: {pattern: "-o*"} result: configures_compiling - match: {pattern: "-p*"} result: configures_compiling - match: {pattern: "-e*"} result: configures_compiling - match: {pattern: "-g"} result: configures_compiling - match: {pattern: "-k"} result: configures_compiling - match: {pattern: "-q*"} result: configures_compiling - match: {pattern: "-s*"} result: configures_compiling - match: {pattern: "-fg"} result: configures_compiling - match: {pattern: "-rr{ }*"} result: configures_compiling - match: {pattern: "-rtti"} result: configures_compiling - match: {pattern: "-neon"} result: configures_compiling # --- Info and exit --- - match: {pattern: "--help"} result: info_and_exit - match: {pattern: "-h"} result: info_and_exit - match: {pattern: "--tool_version"} result: info_and_exit - match: {pattern: "-version"} result: info_and_exit - match: {pattern: "--compiler_revision"} result: info_and_exit # A TI long option that takes a value always glues it after `=`, never # as a separate token, so this catch-all can never swallow a source # file. It keeps an unlisted option (there are well over a hundred, most # of them code-generation tuning) from being read as one. - match: {pattern: "--*"} result: none rizsotto-Bear-0c30b27/crates/semantic/compilers/vala.yaml000066400000000000000000000111241527177720300235020ustar00rootroot00000000000000# https://valadoc.org/ -- valac parses GNU/GOption-style options. Every # value-taking option accepts both `--opt value` and `--opt=value`; short # options also glue (`-ofoo`). Bear treats any bare argument as a source file, # so every value-consuming option must appear here, or its separate-token value # becomes a phantom source. Boolean flags (--debug, --fatal-warnings, ...) start # with `-`/`--` and are auto-classified as non-source, so they need no rules. # Likewise --color and --profile are deliberately absent: both are GOption # OPTIONAL_ARG callbacks that take their value only when glued (--color=always, # --profile=posix), never as a separate token, so a value-consuming rule would # wrongly swallow the next argument (e.g. the source file after a bare --profile). # # valac is a transpiler-driver: it emits C and, unless given -C/--ccode, spawns # a real C compiler on the generated C. See docs/rationale/vala-transpiler-database. type: compiler compiler: id: vala recognize: - description: "Vala" references: - "https://vala.dev/" executables: ["valac"] versioned: true # valac-0.56, valac-0.48 (major.minor) cross_compilation: true # Debian/Ubuntu ship x86_64-linux-gnu-valac slash_prefix: false # valac compiles all sources into one combined translation unit / output. source_mode: combined flags: # Output and driver stop points - match: {pattern: "-o{ }*"} result: output - match: {pattern: "--output{=}*"} result: output - match: {pattern: "-C"} result: stops_at_compiling - match: {pattern: "--ccode"} result: stops_at_compiling - match: {pattern: "-c"} result: stops_at_compiling - match: {pattern: "--compile"} result: stops_at_compiling # Compilation-configuring options that take a value - match: {pattern: "--pkg{=}*"} result: configures_compiling - match: {pattern: "-D{ }*"} result: configures_compiling - match: {pattern: "--define{=}*"} result: configures_compiling - match: {pattern: "--vapidir{=}*"} result: configures_compiling - match: {pattern: "--girdir{=}*"} result: configures_compiling - match: {pattern: "--metadatadir{=}*"} result: configures_compiling - match: {pattern: "--gresources{=}*"} result: configures_compiling - match: {pattern: "--gresourcesdir{=}*"} result: configures_compiling - match: {pattern: "--includedir{=}*"} result: configures_compiling - match: {pattern: "-b{ }*"} result: configures_compiling - match: {pattern: "--basedir{=}*"} result: configures_compiling - match: {pattern: "--target-glib{=}*"} result: configures_compiling - match: {pattern: "--main{=}*"} result: configures_compiling # -X / --Xcc forward one token to the downstream C compiler. The token is # ambiguous (could be a compile or a link flag, e.g. `-X -fPIC` vs `-X -lm`) # and often starts with '-', so `count: 1` consumes it unconditionally. # Classify as compile-affecting (not linking) so the forwarded flag is kept # in the entry rather than stripped as link noise -- dropping a compile flag # is worse than keeping a harmless link flag. - match: {pattern: "-X", count: 1} result: configures_compiling - match: {pattern: "--Xcc", count: 1} result: configures_compiling - match: {pattern: "--Xcc=*"} result: configures_compiling # --library / --shared-library set valac's output mode; keep them verbatim. - match: {pattern: "--library{=}*"} result: none - match: {pattern: "--shared-library{=}*"} result: none # Value-taking options with no further semantic effect for Bear; listed so # their separate-token value is consumed rather than read as a source file. - match: {pattern: "-d{ }*"} result: none - match: {pattern: "--directory{=}*"} result: none - match: {pattern: "-H{ }*"} result: none - match: {pattern: "--header{=}*"} result: none - match: {pattern: "-h{ }*"} result: none - match: {pattern: "--internal-header{=}*"} result: none - match: {pattern: "--vapi{=}*"} result: none - match: {pattern: "--internal-vapi{=}*"} result: none - match: {pattern: "--gir{=}*"} result: none - match: {pattern: "--fast-vapi{=}*"} result: none - match: {pattern: "--use-fast-vapi{=}*"} result: none - match: {pattern: "--depfile{=}*"} result: none - match: {pattern: "--deps{=}*"} result: none - match: {pattern: "--symbols{=}*"} result: none - match: {pattern: "--cc{=}*"} result: none - match: {pattern: "--pkg-config{=}*"} result: none - match: {pattern: "--dump-tree{=}*"} result: none - match: {pattern: "--run-args{=}*"} result: none rizsotto-Bear-0c30b27/crates/semantic/src/000077500000000000000000000000001527177720300204665ustar00rootroot00000000000000rizsotto-Bear-0c30b27/crates/semantic/src/interpreters/000077500000000000000000000000001527177720300232145ustar00rootroot00000000000000rizsotto-Bear-0c30b27/crates/semantic/src/interpreters/combinators.rs000066400000000000000000000124341527177720300261060ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later use super::super::{Execution, Interpreter, RecognizeResult}; /// Represents a set of interpreters, where any of them can recognize the semantic. /// The evaluation is done in the order of the interpreters. The first one which /// recognizes the semantic will be returned as result. pub(super) struct Any { interpreters: Vec>, } impl Any { pub(super) fn new(tools: Vec>) -> Self { Self { interpreters: tools } } } impl Interpreter for Any { fn recognize(&self, mut execution: Execution) -> RecognizeResult { for tool in &self.interpreters { match tool.recognize(execution) { RecognizeResult::NotRecognized(returned) => execution = returned, result => return result, } } RecognizeResult::NotRecognized(execution) } } /// A combinator that logs the input execution before delegating to the inner interpreter. pub(super) struct InputLogger { inner: T, } impl InputLogger { pub(super) fn new(inner: T) -> Self { Self { inner } } } impl Interpreter for InputLogger { fn recognize(&self, execution: Execution) -> RecognizeResult { log::debug!("Recognizing execution: {execution:?}"); self.inner.recognize(execution) } } /// A combinator that logs the output result after delegating to the inner interpreter. pub(super) struct OutputLogger { inner: T, name: String, } impl OutputLogger { pub(super) fn new(inner: T, name: impl Into) -> Self { Self { inner, name: name.into() } } } impl Interpreter for OutputLogger { fn recognize(&self, execution: Execution) -> RecognizeResult { let result = self.inner.recognize(execution); log::debug!("{:20}: {result:?}", self.name); result } } #[cfg(test)] mod test { use super::*; use crate::Command; use crate::MockInterpreter; use crate::SourceMode; use std::collections::HashMap; use std::path::PathBuf; #[test] fn test_any_when_no_match() { let mut mock1 = MockInterpreter::new(); let mut mock2 = MockInterpreter::new(); let mut mock3 = MockInterpreter::new(); mock1.expect_recognize().returning(RecognizeResult::NotRecognized); mock2.expect_recognize().returning(RecognizeResult::NotRecognized); mock3.expect_recognize().returning(RecognizeResult::NotRecognized); let sut = Any { interpreters: vec![Box::new(mock1), Box::new(mock2), Box::new(mock3)] }; let input = execution_fixture(); assert!(matches!(sut.recognize(input), RecognizeResult::NotRecognized(_)), "Expected NotRecognized"); } #[test] fn test_any_when_success() { let mut mock1 = MockInterpreter::new(); let mut mock2 = MockInterpreter::new(); let mock3 = MockInterpreter::new(); mock1.expect_recognize().returning(RecognizeResult::NotRecognized); mock2.expect_recognize().returning(|_| command_fixture()); // mock3 should not be called since mock2 returns a match let sut = Any { interpreters: vec![Box::new(mock1), Box::new(mock2), Box::new(mock3)] }; let input = execution_fixture(); assert!(matches!(sut.recognize(input), RecognizeResult::Recognized(_)), "Expected Recognized"); } fn execution_fixture() -> Execution { Execution { executable: PathBuf::from("/usr/bin/ls"), arguments: vec!["ls".to_string()], working_dir: PathBuf::new(), environment: HashMap::new(), } } fn command_fixture() -> RecognizeResult { RecognizeResult::Recognized({ Command { working_dir: PathBuf::new(), executable: PathBuf::new(), arguments: vec![], source_mode: SourceMode::PerSourceStripped, } }) } #[test] fn test_input_logger_passes_through_results() { let execution = Execution::from_strings( "/usr/bin/gcc", vec!["gcc", "-c", "main.c"], "/home/user", HashMap::new(), ); struct AlwaysNotRecognized; impl Interpreter for AlwaysNotRecognized { fn recognize(&self, execution: Execution) -> RecognizeResult { RecognizeResult::NotRecognized(execution) } } let logger = InputLogger::new(AlwaysNotRecognized); assert!(matches!(logger.recognize(execution), RecognizeResult::NotRecognized(_))); } #[test] fn test_output_logger_passes_through_results() { let execution = Execution::from_strings( "/usr/bin/gcc", vec!["gcc", "-c", "main.c"], "/home/user", HashMap::new(), ); struct AlwaysNotRecognized; impl Interpreter for AlwaysNotRecognized { fn recognize(&self, execution: Execution) -> RecognizeResult { RecognizeResult::NotRecognized(execution) } } let logger = OutputLogger::new(AlwaysNotRecognized, "test"); assert!(matches!(logger.recognize(execution), RecognizeResult::NotRecognized(_))); } } rizsotto-Bear-0c30b27/crates/semantic/src/interpreters/compilers/000077500000000000000000000000001527177720300252115ustar00rootroot00000000000000rizsotto-Bear-0c30b27/crates/semantic/src/interpreters/compilers/compiler_recognition.rs000066400000000000000000001723571527177720300320100ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Unified compiler recognition using regex patterns. //! //! This module provides a consolidated approach to recognizing compiler executables //! using regular expressions instead of separate hard-coded lists and pattern //! matching functions for each compiler. use super::identity::{CompilerType, SpellingError}; use super::probe::{CompilerProbe, default_probe}; use super::wrapper::WRAPPER_NAMES; use regex_lite::Regex; use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::LazyLock; /// Basenames whose underlying toolchain cannot be inferred from the name /// alone -- `cc` and `c++` resolve to GCC on most Linuxes but Clang on /// FreeBSD/OpenBSD/NetBSD/DragonFly and macOS. `CC` is the HPE Cray PrgEnv /// wrapper: it drives whichever compiler module the loaded programming /// environment selects (CCE, GCC, or another vendor's compiler), so the /// basename alone is ambiguous the same way. /// /// The probe is the sole classifier for these names: there is no regex /// fallback (gcc.yaml and cray_cc.yaml deliberately omit them). When the /// probe declines (timeout, unrecognizable output, spawn failure), /// `recognize` returns `None` rather than guessing -- a missing entry is /// visible and debuggable, whereas a wrongly-classified entry corrupts /// the compilation database silently via mismatched flag-arity tables. const AMBIGUOUS_NAMES: &[&str] = &["cc", "c++", "CC"]; /// Recognizes the compiler type for an executable path, using a layered /// strategy: /// /// 1. **Hint lookup** -- driver-supplied [`CompilerHints`] entries /// (canonicalized) are checked first; a hit short-circuits both probe and /// regex. A bare executable spelling also matches a configured entry by /// filename, because config paths must exist on disk while builds often /// spell only the name. /// 2. **Probe** — for ambiguous basenames (`cc`, `c++`, `CC`) the binary is /// invoked with `--version` and classified by signature. Memoization /// of probe results lives in the probe itself (see /// `CachingProbe`); the recognizer only owns the /// dispatch policy. /// 3. **Regex fallback** — filename is matched against patterns generated /// from `interpreters/*.yaml`. Note that the ambiguous names above are /// intentionally omitted from the regex; if the probe declines, /// recognition returns `None` rather than guessing. pub struct CompilerRecognizer { patterns: Vec<(CompilerType, Regex)>, hints: HashMap, probe: Box, } impl CompilerRecognizer { /// Creates a new compiler recognizer with default patterns and the /// platform-default probe (`default_probe` -- the real /// `--version` probe on Unix, a no-op on Windows where compiler /// basenames are unambiguous). pub fn new() -> Self { Self::with_probe(CompilerHints::new(), default_probe()) } /// Creates a new compiler recognizer with driver-supplied hints. /// /// Uses the platform-default probe; user hints are consulted first and /// short-circuit the probe regardless of platform. /// /// # Arguments /// /// * `hints` - The hint table built by [`CompilerHints`] pub fn new_with_hints(hints: CompilerHints) -> Self { Self::with_probe(hints, default_probe()) } /// Creates a recognizer with an injectable probe. Used by tests to swap /// in a fake probe that does not fork+exec. pub(crate) fn with_probe(hints: CompilerHints, probe: Box) -> Self { Self { patterns: DEFAULT_PATTERNS.clone(), hints: hints.entries, probe } } /// Recognizes the compiler type from an executable path. /// /// Order: configured hints, then `--version` probe (only for the /// ambiguous basename set), then regex. /// /// # Arguments /// /// * `executable_path` - The path to the executable (can be relative or absolute) /// /// # Returns /// /// `Some(CompilerType)` if the executable is recognized, `None` otherwise pub fn recognize(&self, executable_path: &Path) -> Option { // 1. Check configured hints first (by canonical path matching). // The user override always wins; it also short-circuits the probe. if let Some(hint_type) = self.lookup_hint(executable_path) { return Some(hint_type); } // 2. For known-ambiguous basenames, run the --version probe. The // probe is what distinguishes BSD/macOS `cc` (Clang) from Linux // `cc` (GCC). Caching of probe results lives inside the probe // itself (`CachingProbe`) so the cost is at most // one fork+exec per unique compiler path per process. let filename = executable_path.file_name().and_then(|n| n.to_str()).unwrap_or(""); if AMBIGUOUS_NAMES.contains(&filename) && let Some(t) = self.probe_canonical(executable_path) { return Some(t); } // 3. Fall back to regex-based recognition. self.recognize_by_regex(executable_path) } /// Looks up a hint for the given executable path. /// /// Tries both the original path and its canonicalized version. The map /// also carries the configured paths' filenames as keys, so a bare /// spelling matches directly; a path spelling cannot collide with a /// filename key because [`Path`] equality is component-wise. fn lookup_hint(&self, executable_path: &Path) -> Option { // Try original path first if let Some(&compiler_type) = self.hints.get(executable_path) { return Some(compiler_type); } // Try canonicalized path if let Ok(canonical_path) = executable_path.canonicalize() && let Some(&compiler_type) = self.hints.get(&canonical_path) { return Some(compiler_type); } None } /// Canonicalize `executable_path` and ask the probe to classify it. /// /// Canonicalization happens here (not in the probe) so the cache key /// in `CachingProbe` collapses different argv /// spellings of the same compiler into one entry. /// /// A masquerade link (an ambiguous name canonicalizing to a wrapper, /// e.g. `/usr/lib/ccache/cc` -> `ccache`) is the exception: it /// answers `--version` in the name it was invoked by, passing the /// flag through to the underlying compiler, so it is probed -- and /// cached -- under the invoked path. The canonical key would collapse /// `cc` and `c++` onto the wrapper binary, whose answer depends on /// argv[0]. See docs/rationale/ambiguous-cc-version-probe.md. fn probe_canonical(&self, executable_path: &Path) -> Option { let key = executable_path.canonicalize().unwrap_or_else(|_| executable_path.to_path_buf()); if let Some(name) = key.file_name().and_then(|n| n.to_str()) && WRAPPER_NAMES.contains(&name) { self.probe.probe(executable_path) } else { self.probe.probe(&key) } } /// Internal regex-based recognition. /// /// Ignores the directory path and only looks at the filename to /// determine the compiler type using [`DEFAULT_PATTERNS`]. fn recognize_by_regex(&self, executable_path: &Path) -> Option { let filename = executable_path.file_name()?.to_str()?; // Check each compiler pattern for (compiler_type, pattern) in &self.patterns { if pattern.is_match(filename) { return Some(*compiler_type); } } None } } impl Default for CompilerRecognizer { fn default() -> Self { Self::new() } } /// Builds the hint lookup table a [`CompilerRecognizer`] consults first. /// /// The driver adds one configured compiler at a time -- a path plus the /// optional `as:` spelling the user wrote -- so the configuration schema /// never crosses into this module. Insertion order is the configuration /// order, which is what the first-wins rule for colliding filenames is /// defined against. #[derive(Clone, Debug, Default)] pub struct CompilerHints { entries: HashMap, } impl CompilerHints { /// An empty hint table: recognition falls back to probe and regex only. pub fn new() -> Self { Self::default() } /// Resolves `spelling` and records the resulting hint for `path`. /// /// The entry contributes two keys: its canonicalized path, and its bare /// filename -- builds often invoke a configured compiler by name alone, /// and the filename key lets the hint reach those spellings. When two /// entries share a filename but disagree on the type, the first wins and /// a warning is logged. /// /// # Compiler type resolution /// /// 1. **Explicit spelling**: `Some(spelling)` resolves to that type, or /// fails with [`SpellingError`] when it names no known compiler. /// 2. **Pattern matching**: `None` matches the filename against the /// default recognition patterns. /// 3. **Fallback**: if no pattern matches, the `gcc` family. Unreachable /// for a spelling the user actually wrote. /// /// # Path canonicalization /// /// `path` is canonicalized for the path key; when canonicalization fails /// (e.g. the path does not exist) the original is used. This matches /// paths that are spelled differently but name the same executable. pub fn add(&mut self, path: &Path, spelling: Option<&str>) -> Result<(), SpellingError> { let compiler_type = match spelling { Some(spelling) => spelling.parse::()?, None => Self::guess_from_filename(path), }; if let Some(name) = path.file_name() { match self.entries.entry(PathBuf::from(name)) { std::collections::hash_map::Entry::Vacant(entry) => { entry.insert(compiler_type); } std::collections::hash_map::Entry::Occupied(entry) => { if *entry.get() != compiler_type { log::warn!( "compiler config: '{}' shares the filename '{}' with an earlier \ entry of a different type; the earlier entry classifies bare \ invocations of that name", path.display(), name.to_string_lossy() ); } } } } // Never collides with a filename key for validated config: // config paths must exist, so canonicalization yields an // absolute, multi-component path. let canonical_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf()); self.entries.insert(canonical_path, compiler_type); Ok(()) } /// Resolves `spelling` without recording a hint. /// /// A configured entry marked `ignore: true` contributes no hint, but a /// misspelled `as:` in one must still fail the run, so the driver routes /// those entries through here. pub fn check(spelling: Option<&str>) -> Result<(), SpellingError> { match spelling { Some(spelling) => spelling.parse::().map(|_| ()), None => Ok(()), } } /// Guesses a compiler type for an entry that named no `as:` spelling, by /// matching its filename against the default recognition patterns and /// falling back to the `gcc` family. fn guess_from_filename(path: &Path) -> CompilerType { let filename = path.file_name().and_then(|name| name.to_str()).unwrap_or(""); DEFAULT_PATTERNS .iter() .find(|(_, pattern)| pattern.is_match(filename)) .map(|(compiler_type, _)| *compiler_type) .unwrap_or(CompilerType::compiler("gcc")) } } // ----- Pattern infrastructure ----------------------------------------- // // Default regex patterns and the helpers that build them. These are // implementation details of CompilerRecognizer and are not part of its // public surface. The patterns themselves are generated at build time // from the YAML files under `bear/interpreters/`. // Generated recognition pattern data from flags/*.yaml. include!(concat!(env!("OUT_DIR"), "/recognition.rs")); /// Compile-time initialized default regex patterns for compiler recognition. /// /// Built entirely from YAML-defined `recognize` entries, including the four /// compiler-launcher (wrapper) files: each contributes its own `(Wrapper, /// regex)` pattern matching only its own executable name(s), which is /// behaviorally equivalent to one combined regex since matching is a linear /// scan. Each entry maps a `CompilerType` to a regex that matches executable /// filenames, supporting cross-compilation prefixes, version suffixes, and /// `.exe` extensions. static DEFAULT_PATTERNS: LazyLock> = LazyLock::new(|| { let mut patterns = Vec::new(); // Build patterns from generated YAML data. The id column is trusted // generated data, so it maps to a `CompilerType` without validation: // `"wrapper"` is the launcher kind, every other value is a compiler id. for &(type_str, executables, cross_compilation, versioned, _description) in RECOGNITION_PATTERNS { let compiler_type = CompilerType::from_recognized_id(type_str); let regex = create_compiler_regex(executables, cross_compilation, versioned); patterns.push((compiler_type, regex)); } patterns }); /// Build a regex that matches any of the given `executables`, with optional /// cross-compilation prefix and version suffix support, plus `.exe` extension. fn create_compiler_regex(executables: &[&str], cross_compilation: bool, versioned: bool) -> Regex { // Escape for regex (handles '+' in names like "c++", "clang++") let escaped: Vec = executables.iter().map(|n| escape_executable(n)).collect(); let alternation = escaped.join("|"); let base = if cross_compilation { format!(r"(?:[^/]*-)?(?:{})", alternation) } else { format!(r"(?:{})", alternation) }; let with_version = if versioned { format!(r"{}(?:[-_]?([0-9]+(?:[._-][0-9a-zA-Z]+)*))?", base) } else { base }; // On Windows, executable names are case-insensitive (CL.EXE, cl.exe, Cl.exe) let case_flag = if cfg!(windows) { "(?i)" } else { "" }; let full_pattern = format!(r"^{}{}(?:\.exe)?$", case_flag, with_version); Regex::new(&full_pattern).unwrap_or_else(|_| panic!("Invalid regex pattern: {}", full_pattern)) } /// Escape regex metacharacters that may appear in compiler executable names. /// /// The metacharacters that actually appear in the YAML-defined executable /// names are `+` (e.g. `c++`, `g++`, `clang++`) and `.` (e.g. `emcc.py`). /// An unescaped `.` would be a one-character wildcard, so a name like /// `emccxpy` would falsely match the `emcc.py` pattern. `regex-lite` does /// not expose a public `escape` helper, so we provide a minimal local one. fn escape_executable(name: &str) -> String { let mut out = String::with_capacity(name.len()); for c in name.chars() { if c == '+' || c == '.' { out.push('\\'); } out.push(c); } out } #[cfg(test)] mod tests { use super::super::probe::NoProbe; use super::*; use std::path::Path; fn path(s: &str) -> &Path { Path::new(s) } /// Builds a hint table from `(path, spelling)` pairs, the way the driver /// does for the configured compilers it does not ignore. fn hints_for(entries: &[(&str, Option<&str>)]) -> CompilerHints { let mut hints = CompilerHints::new(); for (path, spelling) in entries { hints.add(Path::new(path), *spelling).expect("test spellings must resolve"); } hints } /// Asserts each `(name, expected)` case against `recognizer`, naming the /// case in the failure message so a table failure says which row broke. fn assert_cases(recognizer: &CompilerRecognizer, cases: &[(&str, Option)]) { for (name, expected) in cases { assert_eq!(recognizer.recognize(path(name)), *expected, "name: {}", name); } } /// Constructs a default recognizer and asserts each `(name, expected)` case. fn assert_recognition(cases: &[(&str, Option)]) { assert_cases(&CompilerRecognizer::new(), cases); } /// Recognizer with the probe disabled. Use this for tests that exercise /// the regex/hint layer in isolation, without depending on whatever /// `cc`/`c++` resolve to on the host running the test. fn no_probe_recognizer() -> CompilerRecognizer { CompilerRecognizer::with_probe(CompilerHints::new(), Box::new(NoProbe)) } // Requirements: recognition-compiler-names #[test] fn test_gcc_recognition() { // Pure regex behavior. The bare names `cc` and `c++` are // intentionally absent from the gcc.yaml regex: they are // ambiguous (Linux=GCC, BSDs/macOS=Clang) and dispatch is owned // by the probe. Tests for those names live in the probe_* group. assert_cases( &no_probe_recognizer(), &[ // Basic GCC names ("gcc", Some(CompilerType::compiler("gcc"))), ("g++", Some(CompilerType::compiler("gcc"))), // Cross-compilation variants ("arm-linux-gnueabi-gcc", Some(CompilerType::compiler("gcc"))), ("aarch64-linux-gnu-g++", Some(CompilerType::compiler("gcc"))), ("x86_64-w64-mingw32-gcc", Some(CompilerType::compiler("gcc"))), // Versioned variants ("gcc-9", Some(CompilerType::compiler("gcc"))), ("g++-11", Some(CompilerType::compiler("gcc"))), ("gcc-11.2", Some(CompilerType::compiler("gcc"))), ("gcc9", Some(CompilerType::compiler("gcc"))), ("g++11", Some(CompilerType::compiler("gcc"))), // With full paths ("/usr/bin/gcc", Some(CompilerType::compiler("gcc"))), ("/opt/gcc/bin/g++", Some(CompilerType::compiler("gcc"))), ], ); } // Requirements: recognition-compiler-names #[test] fn test_clang_recognition() { assert_recognition(&[ // Basic Clang names ("clang", Some(CompilerType::compiler("clang"))), ("clang++", Some(CompilerType::compiler("clang"))), // Cross-compilation variants ("aarch64-linux-gnu-clang", Some(CompilerType::compiler("clang"))), ("arm-linux-gnueabi-clang++", Some(CompilerType::compiler("clang"))), // Versioned variants ("clang-15", Some(CompilerType::compiler("clang"))), ("clang++-16", Some(CompilerType::compiler("clang"))), ("clang15", Some(CompilerType::compiler("clang"))), ("clang++16", Some(CompilerType::compiler("clang"))), ("clang-15.0", Some(CompilerType::compiler("clang"))), // With full paths ("/usr/bin/clang", Some(CompilerType::compiler("clang"))), ("/opt/llvm/bin/clang++", Some(CompilerType::compiler("clang"))), ]); } #[test] fn test_windows_exe_extensions() { let recognizer = CompilerRecognizer::new(); // GCC with .exe extensions. (`cc.exe`/`c++.exe` are intentionally // absent: those names are ambiguous and the probe owns dispatch // for them; the regex returns no match.) assert_eq!(recognizer.recognize(path("gcc.exe")), Some(CompilerType::compiler("gcc"))); assert_eq!(recognizer.recognize(path("g++.exe")), Some(CompilerType::compiler("gcc"))); // Cross-compilation variants with .exe assert_eq!( recognizer.recognize(path("arm-linux-gnueabi-gcc.exe")), Some(CompilerType::compiler("gcc")) ); assert_eq!( recognizer.recognize(path("x86_64-w64-mingw32-g++.exe")), Some(CompilerType::compiler("gcc")) ); // Versioned variants with .exe assert_eq!(recognizer.recognize(path("gcc-9.exe")), Some(CompilerType::compiler("gcc"))); assert_eq!(recognizer.recognize(path("g++-11.2.exe")), Some(CompilerType::compiler("gcc"))); // Clang with .exe extensions assert_eq!(recognizer.recognize(path("clang.exe")), Some(CompilerType::compiler("clang"))); assert_eq!(recognizer.recognize(path("clang++.exe")), Some(CompilerType::compiler("clang"))); assert_eq!(recognizer.recognize(path("clang-15.exe")), Some(CompilerType::compiler("clang"))); // Fortran with .exe extensions assert_eq!(recognizer.recognize(path("gfortran.exe")), Some(CompilerType::compiler("gcc"))); assert_eq!(recognizer.recognize(path("flang.exe")), Some(CompilerType::compiler("flang"))); assert_eq!(recognizer.recognize(path("f95.exe")), Some(CompilerType::compiler("gcc"))); // Intel Fortran with .exe extensions assert_eq!(recognizer.recognize(path("ifort.exe")), Some(CompilerType::compiler("intel_fortran"))); assert_eq!(recognizer.recognize(path("ifx.exe")), Some(CompilerType::compiler("intel_fortran"))); // Cray Fortran with .exe extensions assert_eq!(recognizer.recognize(path("crayftn.exe")), Some(CompilerType::compiler("cray_fortran"))); assert_eq!(recognizer.recognize(path("ftn.exe")), Some(CompilerType::compiler("cray_fortran"))); // CUDA with .exe extensions assert_eq!(recognizer.recognize(path("nvcc.exe")), Some(CompilerType::compiler("cuda"))); // Wrapper tools with .exe extensions assert_eq!(recognizer.recognize(path("ccache.exe")), Some(CompilerType::Wrapper)); assert_eq!(recognizer.recognize(path("distcc.exe")), Some(CompilerType::Wrapper)); assert_eq!(recognizer.recognize(path("sccache.exe")), Some(CompilerType::Wrapper)); } #[test] fn test_windows_paths_with_exe() { let recognizer = CompilerRecognizer::new(); // Simple Unix-style paths with .exe (should work cross-platform) assert_eq!(recognizer.recognize(path("/mingw64/bin/gcc.exe")), Some(CompilerType::compiler("gcc"))); assert_eq!(recognizer.recognize(path("/usr/bin/clang.exe")), Some(CompilerType::compiler("clang"))); } // Requirements: recognition-compiler-names #[test] fn test_fortran_recognition() { assert_recognition(&[ // Basic Fortran names ("gfortran", Some(CompilerType::compiler("gcc"))), ("f95", Some(CompilerType::compiler("gcc"))), ("flang", Some(CompilerType::compiler("flang"))), ("flang-new", Some(CompilerType::compiler("flang"))), // Cross-compilation variants ("arm-linux-gnueabi-gfortran", Some(CompilerType::compiler("gcc"))), // Versioned variants ("gfortran-11", Some(CompilerType::compiler("gcc"))), ("gfortran11", Some(CompilerType::compiler("gcc"))), ("f95-4.8", Some(CompilerType::compiler("gcc"))), ]); } #[test] fn test_intel_fortran_recognition() { assert_recognition(&[ // Intel Fortran names ("ifort", Some(CompilerType::compiler("intel_fortran"))), ("ifx", Some(CompilerType::compiler("intel_fortran"))), // Versioned variants ("ifort-2021", Some(CompilerType::compiler("intel_fortran"))), ("ifx-2023", Some(CompilerType::compiler("intel_fortran"))), ]); } // Requirements: recognition-compiler-names #[test] fn test_cray_fortran_recognition() { assert_recognition(&[ // Cray Fortran names ("crayftn", Some(CompilerType::compiler("cray_fortran"))), ("ftn", Some(CompilerType::compiler("cray_fortran"))), ]); } #[test] fn test_unrecognized_executables() { let recognizer = CompilerRecognizer::new(); // Should not recognize these assert_eq!(recognizer.recognize(path("unknown-compiler")), None); assert_eq!(recognizer.recognize(path("make")), None); assert_eq!(recognizer.recognize(path("cmake")), None); assert_eq!(recognizer.recognize(path("rustc")), None); assert_eq!(recognizer.recognize(path("javac")), None); } #[test] fn test_path_independence() { let recognizer = CompilerRecognizer::new(); // The directory path should not matter, only the filename let paths = vec!["gcc", "./gcc", "/usr/bin/gcc", "/opt/custom/path/gcc", "../../../../some/deep/path/gcc"]; for path_str in paths { assert_eq!( recognizer.recognize(path(path_str)), Some(CompilerType::compiler("gcc")), "Failed for path: {}", path_str ); } } #[test] fn test_recognize_with_config_hints() { let hints = hints_for(&[("custom-gcc-wrapper", Some("gcc")), ("weird-clang-name", Some("clang"))]); let recognizer = CompilerRecognizer::new_with_hints(hints); // Configured hints take priority assert_eq!(recognizer.recognize(path("custom-gcc-wrapper")), Some(CompilerType::compiler("gcc"))); assert_eq!(recognizer.recognize(path("weird-clang-name")), Some(CompilerType::compiler("clang"))); // Regex detection still works for non-configured compilers assert_eq!(recognizer.recognize(path("gcc")), Some(CompilerType::compiler("gcc"))); assert_eq!(recognizer.recognize(path("unknown-compiler")), None); } // Requirements: recognition-ambiguous-name-probe #[test] fn test_config_hint_matches_bare_spelling_by_filename() { // The configured path is absolute (validation requires it to exist // on real hosts); the build spells only the name. NoProbe declines // everything and `cc` has no regex, so a hit proves the hint. let hints = hints_for(&[("/opt/toolchain/cc", Some("clang"))]); let sut = CompilerRecognizer::with_probe(hints, Box::new(NoProbe)); assert_eq!(sut.recognize(path("cc")), Some(CompilerType::compiler("clang"))); } // Requirements: recognition-ambiguous-name-probe #[test] fn test_config_hint_does_not_filename_match_path_spellings() { // A path spelling names one concrete binary; it must not borrow the // classification of a differently-located configured entry. let hints = hints_for(&[("/opt/toolchain/cc", Some("clang"))]); let sut = CompilerRecognizer::with_probe(hints, Box::new(NoProbe)); assert_eq!(sut.recognize(path("/usr/bin/cc")), None); assert_eq!(sut.recognize(path("./cc")), None); } // Requirements: recognition-compiler-names #[test] fn test_config_hint_beats_name_recognition_of_another_family() { // `f95` is a gcc name; the hint must override it for both the // configured full path and the bare spelling. let hints = hints_for(&[("/opt/toolchain/f95", Some("flang"))]); let unhinted = CompilerRecognizer::with_probe(CompilerHints::new(), Box::new(NoProbe)); let sut = CompilerRecognizer::with_probe(hints, Box::new(NoProbe)); assert_eq!(unhinted.recognize(path("f95")), Some(CompilerType::compiler("gcc"))); let cases = ["/opt/toolchain/f95", "f95"]; for name in cases { assert_eq!(sut.recognize(path(name)), Some(CompilerType::compiler("flang")), "name: {}", name); } } // Requirements: recognition-ambiguous-name-probe #[test] fn test_conflicting_filename_hints_first_entry_wins() { let hints = hints_for(&[("/opt/a/cc", Some("clang")), ("/opt/b/cc", Some("gcc"))]); let sut = CompilerRecognizer::with_probe(hints, Box::new(NoProbe)); // The bare spelling takes the first configured entry (a warning is // logged for the disagreeing second); path spellings keep their own. assert_eq!(sut.recognize(path("cc")), Some(CompilerType::compiler("clang"))); assert_eq!(sut.recognize(path("/opt/a/cc")), Some(CompilerType::compiler("clang"))); assert_eq!(sut.recognize(path("/opt/b/cc")), Some(CompilerType::compiler("gcc"))); } #[test] fn test_hint_builder_rejects_an_unknown_spelling() { // Both entry points resolve the spelling; only `add` records a hint. let mut hints = CompilerHints::new(); let sut = hints.add(path("/opt/toolchain/cc"), Some("not-a-compiler")); assert!(sut.is_err(), "an unknown spelling must not build a hint"); assert!(sut.unwrap_err().to_string().contains("unknown compiler id")); let sut = CompilerHints::check(Some("not-a-compiler")); assert!(sut.is_err(), "an unknown spelling must fail even without a hint"); } #[test] fn test_is_compiler_type() { let recognizer = CompilerRecognizer::new(); assert_eq!(recognizer.recognize(path("gcc")), Some(CompilerType::compiler("gcc"))); assert_eq!(recognizer.recognize(path("clang")), Some(CompilerType::compiler("clang"))); assert_ne!(recognizer.recognize(path("gcc")), Some(CompilerType::compiler("clang"))); assert_ne!(recognizer.recognize(path("clang")), Some(CompilerType::compiler("gcc"))); } #[test] fn test_empty_config() { // Test that recognizer with empty config works the same as new() let recognizer_new = CompilerRecognizer::new(); let recognizer_empty_config = CompilerRecognizer::new_with_hints(CompilerHints::new()); assert_eq!(recognizer_new.recognize(path("gcc")), recognizer_empty_config.recognize(path("gcc"))); assert_eq!(recognizer_new.recognize(path("clang")), recognizer_empty_config.recognize(path("clang"))); assert_eq!( recognizer_new.recognize(path("unknown")), recognizer_empty_config.recognize(path("unknown")) ); } #[test] fn test_gcc_internal_executables_recognition() { assert_recognition(&[ // GCC internal executables are recognized as GCC type ("cc1", Some(CompilerType::compiler("gcc"))), ("cc1plus", Some(CompilerType::compiler("gcc"))), ("cc1obj", Some(CompilerType::compiler("gcc"))), ("cc1objplus", Some(CompilerType::compiler("gcc"))), ("collect2", Some(CompilerType::compiler("gcc"))), ("f951", Some(CompilerType::compiler("gcc"))), ("lto1", Some(CompilerType::compiler("gcc"))), // With full paths ("/usr/libexec/gcc/x86_64-linux-gnu/11/cc1", Some(CompilerType::compiler("gcc"))), ("/usr/lib/gcc/x86_64-linux-gnu/11/cc1plus", Some(CompilerType::compiler("gcc"))), // Non-GCC internal executables are not matched by this pattern ("cc1foo", None), ("foo-cc1", None), ]); } #[test] fn test_hint_type_resolution_per_entry() { let hints = hints_for(&[ // Entry with an explicit spelling - should use that type ("custom-wrapper", Some("clang")), // Entry without a spelling but matching a default pattern - should guess Clang ("clang++", None), // Entry without a spelling and no pattern match - should fall back to GCC ("unknown-compiler", None), // Another entry without a spelling, matching the Fortran pattern ("gfortran", None), ]); let recognizer = CompilerRecognizer::new_with_hints(hints); // Test explicit 'as' field is used assert_eq!(recognizer.recognize(path("custom-wrapper")), Some(CompilerType::compiler("clang"))); // Test pattern matching works when 'as' is None assert_eq!(recognizer.recognize(path("clang++")), Some(CompilerType::compiler("clang"))); // Test fallback to GCC when no pattern matches assert_eq!(recognizer.recognize(path("unknown-compiler")), Some(CompilerType::compiler("gcc"))); // Test a name the builder never saw falls back to the regex pattern assert_eq!(recognizer.recognize(path("unhinted-gcc")), Some(CompilerType::compiler("gcc"))); // Test Fortran pattern matching when 'as' is None assert_eq!(recognizer.recognize(path("gfortran")), Some(CompilerType::compiler("gcc"))); } #[test] fn test_cuda_recognition() { assert_recognition(&[ ("nvcc", Some(CompilerType::compiler("cuda"))), // Versioned variant ("nvcc-12.0", Some(CompilerType::compiler("cuda"))), // Cross-compilation variant ("aarch64-linux-gnu-nvcc", Some(CompilerType::compiler("cuda"))), // Non-CUDA executables don't match: "nvcc-fake" has an invalid // version suffix, "not-nvcc-at-all" merely contains the substring. ("nvcc-fake", None), ("not-nvcc-at-all", None), ("gcc", Some(CompilerType::compiler("gcc"))), ]); } // Requirements: recognition-compiler-launchers #[test] fn test_wrapper_recognition() { assert_recognition(&[ ("ccache", Some(CompilerType::Wrapper)), ("distcc", Some(CompilerType::Wrapper)), ("sccache", Some(CompilerType::Wrapper)), ("icecc", Some(CompilerType::Wrapper)), // Full paths ("/usr/bin/ccache", Some(CompilerType::Wrapper)), ("/opt/distcc/bin/distcc", Some(CompilerType::Wrapper)), // Non-wrapper executables don't match ("ccache-fake", None), ("fake-distcc", None), ("not-sccache", None), // icerun launches arbitrary commands, not compiler invocations. ("icerun", None), ]); } #[test] fn test_version_capture_functionality() { // Test that the DEFAULT_PATTERNS contain regexes that can extract version numbers let recognizer = CompilerRecognizer::new(); // Test basic dash-separated versions assert_eq!(recognizer.recognize(path("gcc-11")), Some(CompilerType::compiler("gcc"))); assert_eq!(recognizer.recognize(path("g++-9.3.0")), Some(CompilerType::compiler("gcc"))); assert_eq!(recognizer.recognize(path("clang-15")), Some(CompilerType::compiler("clang"))); assert_eq!(recognizer.recognize(path("clang-12.1")), Some(CompilerType::compiler("clang"))); assert_eq!(recognizer.recognize(path("gfortran-12")), Some(CompilerType::compiler("gcc"))); assert_eq!(recognizer.recognize(path("ifort-2023")), Some(CompilerType::compiler("intel_fortran"))); assert_eq!(recognizer.recognize(path("nvcc-11.8")), Some(CompilerType::compiler("cuda"))); // Test underscore-separated versions assert_eq!(recognizer.recognize(path("gcc_11")), Some(CompilerType::compiler("gcc"))); assert_eq!(recognizer.recognize(path("clang_15.0.7")), Some(CompilerType::compiler("clang"))); // Test that non-versioned compilers still work assert_eq!(recognizer.recognize(path("gcc")), Some(CompilerType::compiler("gcc"))); assert_eq!(recognizer.recognize(path("clang")), Some(CompilerType::compiler("clang"))); assert_eq!(recognizer.recognize(path("gfortran")), Some(CompilerType::compiler("gcc"))); // Test that wrapper executables don't have version patterns (as expected) assert_eq!(recognizer.recognize(path("ccache")), Some(CompilerType::Wrapper)); assert_eq!(recognizer.recognize(path("ccache-1.0")), None); // No version pattern for wrappers // Verify behaviorally that GCC patterns include a versioned variant: // a numeric-suffixed name resolves to GCC, while a non-numeric suffix // (which the version sub-pattern rejects) does not. assert_eq!(recognizer.recognize(path("gcc-11")), Some(CompilerType::compiler("gcc"))); assert_eq!(recognizer.recognize(path("gcc-abc")), None); } #[test] fn test_msvc_recognition() { assert_recognition(&[ ("cl", Some(CompilerType::compiler("msvc"))), ("cl.exe", Some(CompilerType::compiler("msvc"))), // Internal executables should be recognized as MSVC (then ignored by interpreter) ("c1", Some(CompilerType::compiler("msvc"))), ("c1xx", Some(CompilerType::compiler("msvc"))), ("c2", Some(CompilerType::compiler("msvc"))), ]); } #[test] fn test_clang_cl_recognition() { assert_recognition(&[ ("clang-cl", Some(CompilerType::compiler("clang_cl"))), ("clang-cl.exe", Some(CompilerType::compiler("clang_cl"))), ("clang-cl-17", Some(CompilerType::compiler("clang_cl"))), ]); } #[test] fn test_intel_cc_recognition() { assert_recognition(&[ ("icx", Some(CompilerType::compiler("intel_cc"))), ("icpx", Some(CompilerType::compiler("intel_cc"))), ("icc", Some(CompilerType::compiler("intel_cc"))), ("icpc", Some(CompilerType::compiler("intel_cc"))), ("icx-2024", Some(CompilerType::compiler("intel_cc"))), ]); } #[test] fn test_nvidia_hpc_recognition() { assert_recognition(&[ ("nvc", Some(CompilerType::compiler("nvidia_hpc"))), ("nvc++", Some(CompilerType::compiler("nvidia_hpc"))), ("nvfortran", Some(CompilerType::compiler("nvidia_hpc"))), ("pgcc", Some(CompilerType::compiler("nvidia_hpc"))), ("pgc++", Some(CompilerType::compiler("nvidia_hpc"))), ("pgfortran", Some(CompilerType::compiler("nvidia_hpc"))), ]); } #[test] fn test_armclang_recognition() { assert_recognition(&[ ("armclang", Some(CompilerType::compiler("armclang"))), ("armclang++", Some(CompilerType::compiler("armclang"))), ("armclang-14", Some(CompilerType::compiler("armclang"))), ]); } // Requirements: recognition-compiler-names #[test] fn test_cray_cc_recognition() { // Case sensitivity note: on Unix the recognition regex is // case-sensitive, so "crayCC" only matches because it is listed as // its own literal alternative in cray_cc.yaml, not because "craycc" // case-folds to it. Both spellings must resolve independently. assert_recognition(&[ ("craycc", Some(CompilerType::compiler("cray_cc"))), ("crayCC", Some(CompilerType::compiler("cray_cc"))), ("craycxx", Some(CompilerType::compiler("cray_cc"))), // Versioned variant ("craycc-17", Some(CompilerType::compiler("cray_cc"))), ]); } // Requirements: recognition-compiler-names #[test] fn test_amd_compiler_recognition() { assert_recognition(&[ ("amdclang", Some(CompilerType::compiler("clang"))), ("amdclang++", Some(CompilerType::compiler("clang"))), ("hipcc", Some(CompilerType::compiler("clang"))), ("amdflang", Some(CompilerType::compiler("flang"))), // A GPU-arch reporting tool, not a compiler driver -- must not be recognized. ("amdgpu-arch", None), ]); } // Requirements: recognition-compiler-names #[test] fn test_mpi_wrapper_recognition() { assert_recognition(&[ ("mpicc", Some(CompilerType::compiler("mpi"))), ("mpicxx", Some(CompilerType::compiler("mpi"))), ("mpic++", Some(CompilerType::compiler("mpi"))), ("mpiCC", Some(CompilerType::compiler("mpi"))), ("mpifort", Some(CompilerType::compiler("mpi"))), ("mpif77", Some(CompilerType::compiler("mpi"))), ("mpif90", Some(CompilerType::compiler("mpi"))), // Versioned variant ("mpicc-14", Some(CompilerType::compiler("mpi"))), // MPI launchers execute programs, they do not compile -- must not be recognized. ("mpirun", None), ("mpiexec", None), ]); } // Requirements: recognition-compiler-names #[test] fn test_intel_mpi_wrapper_recognition() { assert_recognition(&[ ("mpiicc", Some(CompilerType::compiler("intel_cc"))), ("mpiicpc", Some(CompilerType::compiler("intel_cc"))), ("mpiicx", Some(CompilerType::compiler("intel_cc"))), ("mpiicpx", Some(CompilerType::compiler("intel_cc"))), ("mpiifort", Some(CompilerType::compiler("intel_fortran"))), ("mpiifx", Some(CompilerType::compiler("intel_fortran"))), ]); } // Requirements: recognition-compiler-names #[test] fn test_qnx_recognition() { assert_recognition(&[ ("qcc", Some(CompilerType::compiler("qnx"))), ("q++", Some(CompilerType::compiler("qnx"))), // A name that merely shares the "q" prefix is not a QNX driver. ("qnxcc", None), ("qcc-fake", None), ]); } // Requirements: recognition-compiler-names #[test] fn test_assembler_recognition() { assert_recognition(&[ ("nasm", Some(CompilerType::compiler("nasm"))), ("yasm", Some(CompilerType::compiler("nasm"))), ("fasm", Some(CompilerType::compiler("fasm"))), // The GNU assembler is deliberately not recognized: gcc/clang // spawn it internally on temporary .s files during ordinary // compiles (see the recognition-compiler-names Deliberately // not recognized table). ("as", None), ("gas", None), // A name that merely shares the "nasm" prefix is not NASM. ("nasm-doc", None), ]); } // Requirements: recognition-compiler-names #[test] fn test_emscripten_and_ti_recognition() { assert_recognition(&[ // Emscripten drivers, including the .py-suffixed spellings some // installs expose. tiarmclang is a single token (no hyphen), so // the -clang cross rule does not catch it; it must be // listed. ("emcc", Some(CompilerType::compiler("clang"))), ("em++", Some(CompilerType::compiler("clang"))), ("emcc.py", Some(CompilerType::compiler("clang"))), ("em++.py", Some(CompilerType::compiler("clang"))), ("tiarmclang", Some(CompilerType::compiler("clang"))), // Emscripten's binutils companions are not compiler drivers. ("emar", None), ("emranlib", None), // The dot in "emcc.py" must match literally, not as a regex // wildcard: a name with any other character in that position is // not an Emscripten driver. ("emccxpy", None), ]); } // Requirements: recognition-compiler-names #[test] fn test_swift_recognition() { assert_recognition(&[ ("swiftc", Some(CompilerType::compiler("swift"))), // swift-frontend is routed to the Swift type via ignore_when // (the same mechanism gcc's cc1 uses) so the interpreter can // filter it; the recognizer itself still classifies the // basename as Swift. ("swift-frontend", Some(CompilerType::compiler("swift"))), // `swift` is the package-manager subcommand driver (`swift // build`, `swift run`, ...), a different command-line model // (subcommand dispatcher, not a compiler invocation) -- it is // deliberately not recognized. ("swift", None), ]); } // Requirements: recognition-compiler-names #[test] fn test_microchip_xc8_recognition() { assert_recognition(&[ ("xc8-cc", Some(CompilerType::compiler("gcc"))), ("xc8", Some(CompilerType::compiler("gcc"))), // The XC8 archiver is not a compiler driver. ("xc8-ar", None), ]); } // Coverage lock: these names were already matched by the existing // cross-compilation prefix rules (no YAML change); the test pins that // behavior so a recognition-pattern refactor cannot silently lose them. // // Requirements: recognition-compiler-names #[test] fn test_cross_prefixed_embedded_names_already_recognized() { let recognizer = CompilerRecognizer::new(); for name in ["hexagon-clang", "hexagon-unknown-linux-musl-clang"] { assert_eq!( recognizer.recognize(path(name)), Some(CompilerType::compiler("clang")), "name: {}", name ); } for name in ["xc32-gcc", "riscv64-unknown-elf-gcc"] { assert_eq!( recognizer.recognize(path(name)), Some(CompilerType::compiler("gcc")), "name: {}", name ); } } #[test] fn test_ibm_xl_recognition() { assert_recognition(&[ ("ibm-clang", Some(CompilerType::compiler("ibm_xl"))), ("ibm-clang++", Some(CompilerType::compiler("ibm_xl"))), ("xlclang", Some(CompilerType::compiler("ibm_xl"))), ("xlclang++", Some(CompilerType::compiler("ibm_xl"))), ]); } #[test] fn test_case_sensitivity_behavior() { let recognizer = CompilerRecognizer::new(); // On Windows, these should match (case-insensitive regex) // On Unix, these should NOT match (case-sensitive regex) let upper_gcc = recognizer.recognize(path("GCC")); let upper_clang = recognizer.recognize(path("CLANG")); let mixed_gcc = recognizer.recognize(path("Gcc")); if cfg!(windows) { assert_eq!(upper_gcc, Some(CompilerType::compiler("gcc"))); assert_eq!(upper_clang, Some(CompilerType::compiler("clang"))); assert_eq!(mixed_gcc, Some(CompilerType::compiler("gcc"))); } else { assert_eq!(upper_gcc, None); assert_eq!(upper_clang, None); assert_eq!(mixed_gcc, None); } } #[test] fn test_exe_extension_case_on_windows() { let recognizer = CompilerRecognizer::new(); // On Windows, .EXE should also match due to case-insensitive regex let upper_exe = recognizer.recognize(path("gcc.EXE")); if cfg!(windows) { assert_eq!(upper_exe, Some(CompilerType::compiler("gcc"))); } else { assert_eq!(upper_exe, None); } } // ----- Probe dispatch tests ----------------------------------------- use std::sync::Mutex as StdMutex; use std::sync::atomic::{AtomicUsize, Ordering}; /// Probe that returns canned answers and counts calls. struct FakeProbe { answers: StdMutex>, calls: AtomicUsize, } impl FakeProbe { fn new() -> Self { Self { answers: StdMutex::new(HashMap::new()), calls: AtomicUsize::new(0) } } fn answer(self, p: &str, t: CompilerType) -> Self { self.answers.lock().unwrap().insert(PathBuf::from(p), t); self } fn calls(&self) -> usize { self.calls.load(Ordering::SeqCst) } } impl super::super::probe::CompilerProbe for FakeProbe { fn probe(&self, p: &Path) -> Option { self.calls.fetch_add(1, Ordering::SeqCst); self.answers.lock().unwrap().get(p).copied() } } // Requirements: recognition-ambiguous-name-probe #[test] fn probe_classifies_cc_as_clang_on_bsd_like_host() { // Simulates `/usr/bin/cc` resolving to Clang (FreeBSD, macOS). // The relative path "cc" canonicalizes to itself when the file does // not exist, so the probe key is the original PathBuf. let probe = Box::new(FakeProbe::new().answer("cc", CompilerType::compiler("clang"))); let recognizer = CompilerRecognizer::with_probe(CompilerHints::new(), probe); assert_eq!(recognizer.recognize(path("cc")), Some(CompilerType::compiler("clang"))); } // Requirements: recognition-ambiguous-name-probe #[test] fn probe_classifies_cc_as_gcc_on_linux_like_host() { let probe = Box::new(FakeProbe::new().answer("cc", CompilerType::compiler("gcc"))); let recognizer = CompilerRecognizer::with_probe(CompilerHints::new(), probe); assert_eq!(recognizer.recognize(path("cc")), Some(CompilerType::compiler("gcc"))); } // Requirements: recognition-ambiguous-name-probe #[test] fn probe_inconclusive_yields_not_recognized() { // The probe is the sole classifier for ambiguous names; there is // no regex fallback. When the probe returns None, recognition // returns None and the dispatcher will surface NotRecognized. // This matters because the previous "default to gcc" behavior // produced silently wrong entries on BSD/macOS hosts where cc is // Clang — exactly the bug this whole mechanism exists to fix. let probe = Box::new(FakeProbe::new()); let recognizer = CompilerRecognizer::with_probe(CompilerHints::new(), probe); assert_eq!(recognizer.recognize(path("cc")), None); assert_eq!(recognizer.recognize(path("c++")), None); } // Requirements: recognition-ambiguous-name-probe #[test] fn probe_classifies_cray_prgenv_cc_as_clang_via_cray_banner() { // Simulates the HPE Cray PrgEnv wrapper "CC" resolving to the CCE // Clang frontend under PrgEnv-cray. The Cray banner ("Cray clang // version ...") contains the substring "clang version", which // classify_version_output already recognizes -- no change to the // classifier itself is needed for this case. let probe = Box::new(FakeProbe::new().answer("CC", CompilerType::compiler("clang"))); let recognizer = CompilerRecognizer::with_probe(CompilerHints::new(), probe); assert_eq!(recognizer.recognize(path("CC")), Some(CompilerType::compiler("clang"))); } // Requirements: recognition-ambiguous-name-probe #[test] fn probe_classifies_cray_prgenv_cc_as_gcc_via_fsf_banner() { // Simulates "CC" resolving to g++ under PrgEnv-gnu. let probe = Box::new(FakeProbe::new().answer("CC", CompilerType::compiler("gcc"))); let recognizer = CompilerRecognizer::with_probe(CompilerHints::new(), probe); assert_eq!(recognizer.recognize(path("CC")), Some(CompilerType::compiler("gcc"))); } // Requirements: recognition-ambiguous-name-probe #[test] fn config_hint_beats_probe_and_suppresses_it_for_cray_prgenv_cc() { let hints = hints_for(&[("CC", Some("cray_cc"))]); let probe = Box::new(FakeProbe::new().answer("CC", CompilerType::compiler("clang"))); let probe_ptr: *const FakeProbe = &*probe; let recognizer = CompilerRecognizer::with_probe(hints, probe); assert_eq!(recognizer.recognize(path("CC")), Some(CompilerType::compiler("cray_cc"))); // SAFETY: probe_ptr points into the Box owned by recognizer, // which is alive and not moved here. calls() takes &self and only reads // an atomic. let calls = unsafe { (*probe_ptr).calls() }; assert_eq!(calls, 0, "hint must short-circuit the probe"); } // Requirements: recognition-ambiguous-name-probe #[test] fn probe_inconclusive_for_cray_prgenv_cc_yields_not_recognized() { // A programming environment whose compiler prints a banner the // probe does not know (e.g. nvc++ under PrgEnv-nvidia) stays // unrecognized -- documented limitation in // recognition-ambiguous-name-probe.md. let probe = Box::new(FakeProbe::new()); let recognizer = CompilerRecognizer::with_probe(CompilerHints::new(), probe); assert_eq!(recognizer.recognize(path("CC")), None); } // Requirements: recognition-ambiguous-name-probe #[test] fn config_hint_beats_probe_and_suppresses_it() { // The user's compilers: entry must win, and the probe must not run // when a hint already classifies the path. let hints = hints_for(&[("cc", Some("gcc"))]); let probe = Box::new(FakeProbe::new().answer("cc", CompilerType::compiler("clang"))); // Take a raw pointer to the FakeProbe so we can read the call count // after handing ownership to the recognizer. Safe because the // recognizer owns the box for the duration of the test. let probe_ptr: *const FakeProbe = &*probe; let recognizer = CompilerRecognizer::with_probe(hints, probe); assert_eq!(recognizer.recognize(path("cc")), Some(CompilerType::compiler("gcc"))); // SAFETY: probe_ptr points into the Box owned by recognizer, // which is alive and not moved here. calls() takes &self and only reads // an atomic. let calls = unsafe { (*probe_ptr).calls() }; assert_eq!(calls, 0, "hint must short-circuit the probe"); } // Requirements: recognition-ambiguous-name-probe #[test] fn non_ambiguous_names_are_not_probed() { let probe = Box::new(FakeProbe::new()); let probe_ptr: *const FakeProbe = &*probe; let recognizer = CompilerRecognizer::with_probe(CompilerHints::new(), probe); // A handful of non-ambiguous names: each must take the regex path // without going through the probe at all. for name in &["gcc", "clang", "g++", "clang++", "gfortran", "icx", "nvcc"] { let _ = recognizer.recognize(path(name)); } // SAFETY: probe_ptr points into the Box owned by recognizer, // which is alive and not moved here. calls() takes &self and only reads // an atomic. let calls = unsafe { (*probe_ptr).calls() }; assert_eq!(calls, 0, "only AMBIGUOUS_NAMES should ever reach the probe"); } // Cache behavior is the responsibility of `super::super::probe::CachingProbe` // and is exercised in its own test module. The recognizer simply asks the // probe; whether the answer is fresh or memoized is opaque here. // Requirements: recognition-ambiguous-name-probe, recognition-compiler-launchers #[test] fn wrapper_basenames_are_never_probed_even_under_ambiguous_paths() { // ccache, distcc, sccache, icecc invoked by their own names must // reach the regex (which returns CompilerType::Wrapper) so the // launcher interpreter unwraps them; a wrapper's own name is not // ambiguous and must never enter the probe path. let probe = Box::new(FakeProbe::new()); let probe_ptr: *const FakeProbe = &*probe; let recognizer = CompilerRecognizer::with_probe(CompilerHints::new(), probe); assert_eq!(recognizer.recognize(path("ccache")), Some(CompilerType::Wrapper)); assert_eq!(recognizer.recognize(path("distcc")), Some(CompilerType::Wrapper)); assert_eq!(recognizer.recognize(path("sccache")), Some(CompilerType::Wrapper)); assert_eq!(recognizer.recognize(path("icecc")), Some(CompilerType::Wrapper)); // SAFETY: probe_ptr points into the Box owned by recognizer, // which is alive and not moved here. calls() takes &self and only reads // an atomic. let calls = unsafe { (*probe_ptr).calls() }; assert_eq!(calls, 0); } // The recognizer canonicalizes ambiguous-name paths before delegating // to the (caching) probe. Two argv spellings of the same compiler -- // e.g. reached through different symlinks -- must therefore collapse // to one inner-probe call. This invariant used to be exercised // implicitly by the now-removed probe_runs_at_most_once_per_canonical_path // test; with caching extracted into CachingProbe, we restore explicit // coverage by wiring CachingProbe over a counting probe and observing // the call count after asking the recognizer to classify two // symlinks pointing at the same target. // // Requirements: recognition-ambiguous-name-probe #[test] #[cfg(unix)] fn distinct_paths_canonicalizing_to_same_target_share_cache_entry() { use super::super::probe::CachingProbe; use std::os::unix::fs::symlink; use std::sync::Arc; struct CountingProbe { calls: Arc, } impl super::super::probe::CompilerProbe for CountingProbe { fn probe(&self, _: &Path) -> Option { self.calls.fetch_add(1, Ordering::SeqCst); Some(CompilerType::compiler("clang")) } } let dir = tempfile::tempdir().expect("tempdir"); let real = dir.path().join("real-cc"); std::fs::write(&real, b"").expect("write real-cc"); let dir_a = dir.path().join("a"); let dir_b = dir.path().join("b"); std::fs::create_dir(&dir_a).expect("mkdir a"); std::fs::create_dir(&dir_b).expect("mkdir b"); let link_a = dir_a.join("cc"); let link_b = dir_b.join("cc"); symlink(&real, &link_a).expect("symlink a/cc -> real-cc"); symlink(&real, &link_b).expect("symlink b/cc -> real-cc"); let calls = Arc::new(AtomicUsize::new(0)); let counting = CountingProbe { calls: Arc::clone(&calls) }; let probe: Box = Box::new(CachingProbe::new(counting)); let recognizer = CompilerRecognizer::with_probe(CompilerHints::new(), probe); assert_eq!(recognizer.recognize(&link_a), Some(CompilerType::compiler("clang"))); assert_eq!(recognizer.recognize(&link_b), Some(CompilerType::compiler("clang"))); assert_eq!( calls.load(Ordering::SeqCst), 1, "canonicalization in the recognizer must collapse symlinks before the cache lookup" ); } // The masquerade contract: an ambiguous basename (`cc`) that // canonicalizes to a wrapper binary is probed AS INVOKED -- the // masquerade binary answers `--version` in the name it was called // by, passing through to the underlying compiler, so probing the // invoked path (not the canonical wrapper) yields the real // toolchain's banner. // // Requirements: recognition-ambiguous-name-probe #[test] #[cfg(unix)] fn ambiguous_name_canonicalizing_to_masquerade_wrapper_is_probed_as_invoked() { use std::os::unix::fs::symlink; let dir = tempfile::tempdir().expect("tempdir"); let launcher = dir.path().join("ccache"); std::fs::write(&launcher, b"").expect("write ccache"); let farm = dir.path().join("masquerade"); std::fs::create_dir(&farm).expect("mkdir masquerade"); let link = farm.join("cc"); symlink(&launcher, &link).expect("symlink masquerade/cc -> ccache"); // The canned answer is keyed by the INVOKED path: a hit proves the // probe received it rather than the canonical wrapper path. let probe = Box::new(FakeProbe::new().answer(link.to_str().unwrap(), CompilerType::compiler("gcc"))); let probe_ptr: *const FakeProbe = &*probe; let recognizer = CompilerRecognizer::with_probe(CompilerHints::new(), probe); let sut = recognizer.recognize(&link); assert_eq!(sut, Some(CompilerType::compiler("gcc"))); // SAFETY: probe_ptr points into the Box owned by recognizer, // which is alive and not moved here. calls() takes &self and only reads // an atomic. let calls = unsafe { (*probe_ptr).calls() }; assert_eq!(calls, 1, "the masquerade link must be probed exactly once, as invoked"); } // Masquerade links cache under the invoked path, not the canonical // one: `cc` and `c++` links to the same wrapper binary answer for // different underlying compilers, so collapsing them onto the // canonical wrapper key would return the first answer for both. // // Requirements: recognition-ambiguous-name-probe #[test] #[cfg(unix)] fn masquerade_links_to_same_wrapper_are_probed_separately() { use super::super::probe::CachingProbe; use std::os::unix::fs::symlink; use std::sync::Arc; struct CountingProbe { calls: Arc, } impl super::super::probe::CompilerProbe for CountingProbe { fn probe(&self, _: &Path) -> Option { self.calls.fetch_add(1, Ordering::SeqCst); Some(CompilerType::compiler("gcc")) } } let dir = tempfile::tempdir().expect("tempdir"); let launcher = dir.path().join("ccache"); std::fs::write(&launcher, b"").expect("write ccache"); let farm = dir.path().join("masquerade"); std::fs::create_dir(&farm).expect("mkdir masquerade"); let link_cc = farm.join("cc"); let link_cxx = farm.join("c++"); symlink(&launcher, &link_cc).expect("symlink masquerade/cc -> ccache"); symlink(&launcher, &link_cxx).expect("symlink masquerade/c++ -> ccache"); let calls = Arc::new(AtomicUsize::new(0)); let counting = CountingProbe { calls: Arc::clone(&calls) }; let probe: Box = Box::new(CachingProbe::new(counting)); let recognizer = CompilerRecognizer::with_probe(CompilerHints::new(), probe); assert_eq!(recognizer.recognize(&link_cc), Some(CompilerType::compiler("gcc"))); assert_eq!(recognizer.recognize(&link_cxx), Some(CompilerType::compiler("gcc"))); assert_eq!(recognizer.recognize(&link_cc), Some(CompilerType::compiler("gcc"))); assert_eq!( calls.load(Ordering::SeqCst), 2, "each masquerade name probes once (cached), but cc and c++ must not share an entry" ); } } rizsotto-Bear-0c30b27/crates/semantic/src/interpreters/compilers/flag_based.rs000066400000000000000000001044511527177720300276330ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Generic flag-based compiler interpreter. //! //! This module provides a single interpreter type that handles all flag-table-driven //! compilers (GCC, Clang, Flang, CUDA, Intel Fortran, Cray Fortran). Each compiler //! is parameterized by its generated flag table and optional ignore filters, eliminating //! the need for per-compiler structs and trait implementations. use super::super::matchers::{ EnvMapping, EnvPosition, EnvRule, EnvSeparator, FlagAnalyzer, FlagPattern, FlagRule, }; use super::response_file::{Syntax, tokenize_msvc}; use crate::{ Argument, ArgumentKind, Command, CompilerPass, Execution, Interpreter, OutputSpelling, PassEffect, RecognizeResult, SourceMode, }; /// A generic compiler interpreter parameterized by a flag table and ignore filters. /// /// This replaces the individual per-compiler interpreter structs (GccInterpreter, /// ClangInterpreter, etc.) with a single type driven by build-time-generated data. struct FlagBasedInterpreter { analyzer: FlagAnalyzer, ignore_executables: &'static [&'static str], ignore_flags: &'static [&'static str], /// When true, arguments starting with '/' are treated as flags (MSVC-style). /// When false (default), only '-' prefixed arguments are treated as flags. slash_prefix: bool, env_rules: &'static [EnvRule], /// When true, environment variables in `env_rules` are folded into the /// recognized arguments (`format.arguments.from_environment`). from_environment: bool, /// How `env_rules` values that expand to words are split. env_syntax: Syntax, /// How this family's invocations map to compilation database entries. /// See [`SourceMode`]. Comes from the family's YAML `source_mode:` (via the /// generated [`FamilyDef`]); it is a per-family datum, consumed at the /// converter (post-parse). source_mode: SourceMode, } impl FlagBasedInterpreter { /// Creates a new flag-based interpreter from one family's generated data. fn new(def: &FamilyDef, from_environment: bool) -> Self { Self { analyzer: FlagAnalyzer::new(def.flags), ignore_executables: def.ignore_executables, ignore_flags: def.ignore_flags, slash_prefix: def.slash_prefix, env_rules: def.env_rules, from_environment, env_syntax: def.response_file_syntax, source_mode: def.source_mode, } } fn should_ignore(&self, execution: &Execution) -> Option<&'static str> { // Check executable name against ignore list if !self.ignore_executables.is_empty() && let Some(filename) = execution.executable.file_name() && let Some(filename_str) = filename.to_str() && self.ignore_executables.contains(&filename_str) { return Some("internal executable"); } // Check arguments against ignore flags if !self.ignore_flags.is_empty() && self.ignore_flags.iter().any(|flag| execution.arguments.iter().any(|arg| arg == flag)) { return Some("internal invocation"); } None } } impl Interpreter for FlagBasedInterpreter { fn recognize(&self, execution: Execution) -> RecognizeResult { if let Some(reason) = self.should_ignore(&execution) { return RecognizeResult::Ignored(reason); } let Execution { executable, mut arguments, working_dir, environment } = execution; let annotated_args = parse_arguments_owned(&self.analyzer, &mut arguments, self.slash_prefix); let (prepend_args, append_args) = if self.from_environment { parse_environment(&environment, self.env_rules, self.env_syntax) } else { (Vec::new(), Vec::new()) }; // The compiler stays first; prepended words go right after it. let mut annotated_args = annotated_args.into_iter(); let mut all_args: Vec = annotated_args.next().into_iter().collect(); all_args.extend(prepend_args); all_args.extend(annotated_args); all_args.extend(append_args); RecognizeResult::Recognized(Command { working_dir, executable, arguments: all_args, source_mode: self.source_mode, }) } } /// Parse command line arguments, moving strings out of the owned Vec. /// /// Uses `std::mem::take` to move strings into Argument variants without cloning. /// The source Vec elements become empty strings after being taken. fn parse_arguments_owned( flag_analyzer: &FlagAnalyzer, args: &mut [String], slash_prefix: bool, ) -> Vec { let mut result: Vec = Vec::with_capacity(args.len()); let mut i = 0; while i < args.len() { // Handle the first argument (compiler name) if i == 0 { result.push(Argument::Other { arguments: vec![std::mem::take(&mut args[0])], kind: ArgumentKind::Compiler, }); i += 1; continue; } // match_flag needs a view of the remaining args; taken slots are behind us let remaining_args = &args[i..]; if let Some(match_result) = flag_analyzer.match_flag(remaining_args) { // Handle pass-through first (early exit) if matches!(match_result.rule.kind, ArgumentKind::Other(PassEffect::PassThrough)) { result.push(Argument::Other { arguments: vec![std::mem::take(&mut args[i])], kind: ArgumentKind::Other(PassEffect::PassThrough), }); i += 1; while i < args.len() { result.push(Argument::Other { arguments: vec![std::mem::take(&mut args[i])], kind: ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking)), }); i += 1; } break; } let consumed_count = match_result.consumed_count; let arg = match match_result.rule.kind { ArgumentKind::Compiler => Argument::Other { arguments: vec![std::mem::take(&mut args[i])], kind: ArgumentKind::Compiler, }, ArgumentKind::Source { .. } => { unreachable!("Source files should be detected by heuristic, not flag matching") } ArgumentKind::Output => match consumed_count { 1 => { let val = std::mem::take(&mut args[i]); let flag_str = match_result.rule.pattern.flag(); let after_flag = &val[flag_str.len()..]; // Remember how the build joined the flag and its // value, so the entry can spell it back the way this // compiler accepts. let (spelling, path) = match after_flag.strip_prefix('=') { Some(rest) => (OutputSpelling::Equals, rest), None => match after_flag.strip_prefix(':') { Some(rest) => (OutputSpelling::Colon, rest), None => (OutputSpelling::Glued, after_flag), }, }; Argument::Output { flag: flag_str.to_string(), path: path.to_string(), spelling } } 2 => Argument::Output { flag: std::mem::take(&mut args[i]), path: std::mem::take(&mut args[i + 1]), spelling: OutputSpelling::Separate, }, _ => { unreachable!("Output file should be specified with glued or separate value") } }, ArgumentKind::Other(compiler_pass) => { let moved: Vec = (i..i + consumed_count).map(|j| std::mem::take(&mut args[j])).collect(); Argument::Other { arguments: moved, kind: ArgumentKind::Other(compiler_pass) } } }; result.push(arg); i += consumed_count; } else if args[i].starts_with('-') || (slash_prefix && args[i].starts_with('/')) { result.push(Argument::Other { arguments: vec![std::mem::take(&mut args[i])], kind: ArgumentKind::Other(PassEffect::None), }); i += 1; } else { result.push(Argument::new_source(std::mem::take(&mut args[i]))); i += 1; } } result } /// Parse environment variables into compiler arguments using the given rules. /// /// Returns `(prepend, append)` argument vectors. Prepend args go before command-line /// args (e.g., MSVC `CL`), append args go after (e.g., include paths, MSVC `_CL_`). fn parse_environment( environment: &std::collections::HashMap, rules: &[EnvRule], syntax: Syntax, ) -> (Vec, Vec) { let mut prepend = Vec::new(); let mut append = Vec::new(); for rule in rules { let Some(value) = environment.get(rule.variable) else { continue; }; match rule.mapping { EnvMapping::Flag { flag, separator } => { let parts = split_env_value(value, separator); for part in parts { if !part.is_empty() { append.push(Argument::Other { arguments: vec![flag.to_string(), part], kind: rule.kind, }); } } } EnvMapping::Expand { position } => { let words = match syntax { Syntax::Msvc => tokenize_msvc(value), Syntax::GnuClang => shell_words::split(value).unwrap_or_else(|_| vec![value.clone()]), }; let target = match position { EnvPosition::Prepend => &mut prepend, EnvPosition::Append => &mut append, }; for word in words { target.push(Argument::Other { arguments: vec![word], kind: rule.kind }); } } } } (prepend, append) } /// Split an environment variable value by the given separator type. fn split_env_value(value: &str, separator: EnvSeparator) -> Vec { match separator { EnvSeparator::Path => std::env::split_paths(value).map(|p| p.to_string_lossy().to_string()).collect(), EnvSeparator::Fixed(sep) => value.split(sep).map(|s| s.to_string()).collect(), } } // Flag tables and the family registry, generated at build time from // compilers/*.yaml. families.rs include!s each per-family flag-table file // (bringing that family's X_FLAGS / X_IGNORE_* / X_SLASH_PREFIX / X_ENV_RULES // statics into scope) and then defines FAMILIES, one row per family in // recognition order. include!(concat!(env!("OUT_DIR"), "/families.rs")); /// One compiler family's generated data: references to its flag-table statics /// plus the two per-family behavior selectors. The generated `FAMILIES` array /// is a slice of these; `register_all` (in the parent module) loops over it, /// and `response_file::syntax_for` reads `response_file_syntax` back out, as /// does `FlagBasedInterpreter::new` to split environment-derived words. pub(super) struct FamilyDef { pub(super) id: &'static str, pub(super) flags: &'static [FlagRule], pub(super) ignore_executables: &'static [&'static str], pub(super) ignore_flags: &'static [&'static str], pub(super) slash_prefix: bool, pub(super) env_rules: &'static [EnvRule], pub(super) source_mode: SourceMode, pub(super) response_file_syntax: Syntax, } /// Build the interpreter for one family, returning an opaque `impl Interpreter` /// so callers never see the concrete `FlagBasedInterpreter`. pub(super) fn interpreter(def: &FamilyDef, from_environment: bool) -> impl Interpreter { FlagBasedInterpreter::new(def, from_environment) } #[cfg(test)] mod flag_table_invariants { use super::*; fn assert_invariants(flags: &[FlagRule]) { assert!(!flags.is_empty(), "Flag table must not be empty"); // Sorted by flag length descending for window in flags.windows(2) { assert!( window[0].pattern.flag().len() >= window[1].pattern.flag().len(), "Flags not sorted by length: {:?} (len {}) before {:?} (len {})", window[0].pattern.flag(), window[0].pattern.flag().len(), window[1].pattern.flag(), window[1].pattern.flag().len(), ); } for rule in flags { assert!( !matches!(rule.kind, ArgumentKind::Source { .. }), "Flag rule {:?} must not use ArgumentKind::Source", rule.pattern.flag() ); let flag = rule.pattern.flag(); assert!( flag.starts_with('-') || flag.starts_with('@') || flag.starts_with('/'), "Flag {:?} must start with '-', '@', or '/'", flag ); if matches!(rule.kind, ArgumentKind::Output) { match rule.pattern { FlagPattern::Exactly(_, n) => { assert!(n <= 1, "Output rule {:?} must take 0 or 1 extra args", flag) } FlagPattern::ExactlyWithEq(_) | FlagPattern::ExactlyWithEqOrSep(_) | FlagPattern::ExactlyWithColon(_) | FlagPattern::ExactlyWithColonOrSep(_) | FlagPattern::ExactlyWithGluedOrSep(_) => {} FlagPattern::Prefix(_, n) => { assert!(n <= 1, "Output rule {:?} must take 0 or 1 extra args", flag) } } } } } #[test] fn gcc() { assert_invariants(&GCC_FLAGS); } #[test] fn clang() { assert_invariants(&CLANG_FLAGS); } #[test] fn flang() { assert_invariants(&FLANG_FLAGS); } #[test] fn cuda() { assert_invariants(&CUDA_FLAGS); } #[test] fn intel_fortran() { assert_invariants(&INTEL_FORTRAN_FLAGS); } #[test] fn cray_fortran() { assert_invariants(&CRAY_FORTRAN_FLAGS); } #[test] fn msvc() { assert_invariants(&MSVC_FLAGS); } #[test] fn clang_cl() { assert_invariants(&CLANG_CL_FLAGS); } #[test] fn intel_cc() { assert_invariants(&INTEL_CC_FLAGS); } #[test] fn nvidia_hpc() { assert_invariants(&NVIDIA_HPC_FLAGS); } #[test] fn armclang() { assert_invariants(&ARMCLANG_FLAGS); } #[test] fn ibm_xl() { assert_invariants(&IBM_XL_FLAGS); } #[test] fn vala() { assert_invariants(&VALA_FLAGS); } #[test] fn mpi() { assert_invariants(&MPI_FLAGS); } #[test] fn cray_cc() { assert_invariants(&CRAY_CC_FLAGS); } #[test] fn qnx() { assert_invariants(&QNX_FLAGS); } #[test] fn nasm() { assert_invariants(&NASM_FLAGS); } #[test] fn fasm() { assert_invariants(&FASM_FLAGS); } #[test] fn swift() { assert_invariants(&SWIFT_FLAGS); } #[test] fn clang_inherits_all_gcc_flags() { let gcc_flag_strings: std::collections::HashSet<&str> = GCC_FLAGS.iter().map(|f| f.pattern.flag()).collect(); let clang_flag_strings: std::collections::HashSet<&str> = CLANG_FLAGS.iter().map(|f| f.pattern.flag()).collect(); assert!( CLANG_FLAGS.len() > GCC_FLAGS.len(), "Clang should have more flags than GCC, got gcc: {}, clang: {}", GCC_FLAGS.len(), CLANG_FLAGS.len() ); let missing_flags: Vec<&str> = gcc_flag_strings.difference(&clang_flag_strings).cloned().collect(); assert!(missing_flags.is_empty(), "These GCC flags are missing from Clang: {:?}", missing_flags); } } #[cfg(test)] mod pass_through_tests { use super::*; use crate::interpreters::matchers::{FlagAnalyzer, FlagPattern, FlagRule}; #[test] fn test_pass_through_flag_stops_parsing() { static PASS_THROUGH_FLAGS: std::sync::LazyLock> = std::sync::LazyLock::new(|| { let mut flags = vec![ FlagRule::new( FlagPattern::Exactly("-c", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling)), ), FlagRule::new(FlagPattern::Exactly("/link", 0), ArgumentKind::Other(PassEffect::PassThrough)), FlagRule::new(FlagPattern::Exactly("-o", 1), ArgumentKind::Output), ]; flags.sort_by_key(|b| std::cmp::Reverse(b.pattern.flag().len())); flags }); let analyzer = FlagAnalyzer::new(&PASS_THROUGH_FLAGS); let mut args = vec![ "cl".to_string(), "-c".to_string(), "foo.c".to_string(), "/link".to_string(), "/SUBSYSTEM:CONSOLE".to_string(), "/OUT:foo.exe".to_string(), ]; let result = parse_arguments_owned(&analyzer, &mut args, false); // cl (compiler) assert!(matches!(result[0], Argument::Other { ref kind, .. } if *kind == ArgumentKind::Compiler)); // -c (stops at compiling) assert!( matches!(result[1], Argument::Other { ref kind, .. } if *kind == ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling))) ); // foo.c (source) assert!(matches!(result[2], Argument::Source { .. })); // /link (pass-through marker) assert!( matches!(result[3], Argument::Other { ref kind, .. } if *kind == ArgumentKind::Other(PassEffect::PassThrough)) ); // /SUBSYSTEM:CONSOLE (linker arg) assert!( matches!(result[4], Argument::Other { ref kind, .. } if *kind == ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))) ); // /OUT:foo.exe (linker arg) assert!( matches!(result[5], Argument::Other { ref kind, .. } if *kind == ArgumentKind::Other(PassEffect::Configures(CompilerPass::Linking))) ); } } #[cfg(test)] mod vala_tests { use super::*; use crate::interpreters::matchers::FlagAnalyzer; // `-X` forwards exactly one token to the C compiler, and that token usually // starts with '-' (e.g. `-X -lm`). Because Bear treats any bare argument as // a source file, the danger is that the value leaks in as a phantom source. // `count: 1` must consume it regardless of its leading dash. #[test] fn dash_prefixed_x_value_is_consumed_not_a_source() { let analyzer = FlagAnalyzer::new(&VALA_FLAGS); let mut args = vec!["valac".to_string(), "-X".to_string(), "-lm".to_string(), "foo.vala".to_string()]; let result = parse_arguments_owned(&analyzer, &mut args, false); // valac (compiler), [-X -lm] (one option), foo.vala (the only source) assert_eq!(result.len(), 3, "expected exactly 3 parsed arguments, got {:?}", result); assert!(matches!(result[0], Argument::Other { ref kind, .. } if *kind == ArgumentKind::Compiler)); assert!( matches!(&result[1], Argument::Other { arguments, .. } if arguments == &vec!["-X".to_string(), "-lm".to_string()]), "-X must consume -lm as its value, got {:?}", result[1] ); assert!( matches!(&result[2], Argument::Source { path, binary } if path == "foo.vala" && !*binary), "foo.vala must be the sole compilable source, got {:?}", result[2] ); } } #[cfg(test)] mod slash_prefix_tests { use super::*; use crate::interpreters::matchers::{FlagAnalyzer, FlagRule}; #[test] fn slash_prefixed_args_treated_as_source_without_slash_support() { let flags: &[FlagRule] = &[]; let analyzer = FlagAnalyzer::new(flags); let mut args = vec!["cl".to_string(), "/c".to_string(), "foo.c".to_string()]; let result = parse_arguments_owned(&analyzer, &mut args, false); // /c should be a source file since slash_prefix is false assert!(matches!(result[1], Argument::Source { .. })); } #[test] fn slash_prefixed_args_treated_as_flags_with_slash_support() { let flags: &[FlagRule] = &[]; let analyzer = FlagAnalyzer::new(flags); let mut args = vec!["cl".to_string(), "/c".to_string(), "foo.c".to_string()]; let result = parse_arguments_owned(&analyzer, &mut args, true); // /c should be an unrecognized flag (Other with None) since slash_prefix is true assert!(matches!( result[1], Argument::Other { ref kind, .. } if *kind == ArgumentKind::Other(PassEffect::None) )); } #[test] fn output_extraction_works_with_glued_eq() { use crate::interpreters::matchers::FlagPattern; use std::sync::LazyLock; static OUTPUT_FLAGS: LazyLock> = LazyLock::new(|| vec![FlagRule::new(FlagPattern::ExactlyWithEq("-o"), ArgumentKind::Output)]); let analyzer = FlagAnalyzer::new(&OUTPUT_FLAGS); let mut args = vec!["gcc".to_string(), "-o=foo.o".to_string()]; let result = parse_arguments_owned(&analyzer, &mut args, false); assert!(matches!(result[1], Argument::Output { ref flag, ref path, spelling } if flag == "-o" && path == "foo.o" && spelling == OutputSpelling::Equals)); } #[test] fn output_extraction_works_with_glued_colon() { use crate::interpreters::matchers::FlagPattern; use std::sync::LazyLock; static OUTPUT_FLAGS: LazyLock> = LazyLock::new(|| vec![FlagRule::new(FlagPattern::ExactlyWithColon("/Fo"), ArgumentKind::Output)]); let analyzer = FlagAnalyzer::new(&OUTPUT_FLAGS); let mut args = vec!["cl".to_string(), "/Fo:foo.obj".to_string()]; let result = parse_arguments_owned(&analyzer, &mut args, true); assert!(matches!(result[1], Argument::Output { ref flag, ref path, spelling } if flag == "/Fo" && path == "foo.obj" && spelling == OutputSpelling::Colon)); } #[test] fn output_extraction_works_with_glued_value() { use crate::interpreters::matchers::FlagPattern; use std::sync::LazyLock; static OUTPUT_FLAGS: LazyLock> = LazyLock::new(|| { vec![FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-o"), ArgumentKind::Output)] }); let analyzer = FlagAnalyzer::new(&OUTPUT_FLAGS); let mut args = vec!["gcc".to_string(), "-ofoo.o".to_string()]; let result = parse_arguments_owned(&analyzer, &mut args, false); assert!(matches!(result[1], Argument::Output { ref flag, ref path, spelling } if flag == "-o" && path == "foo.o" && spelling == OutputSpelling::Glued)); } /// The separate spelling is the only one that may re-emit as two tokens. #[test] fn output_extraction_records_the_separate_form() { use crate::interpreters::matchers::FlagPattern; use std::sync::LazyLock; static OUTPUT_FLAGS: LazyLock> = LazyLock::new(|| { vec![FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("-o"), ArgumentKind::Output)] }); let analyzer = FlagAnalyzer::new(&OUTPUT_FLAGS); let mut args = vec!["gcc".to_string(), "-o".to_string(), "foo.o".to_string()]; let result = parse_arguments_owned(&analyzer, &mut args, false); assert!(matches!(result[1], Argument::Output { ref flag, ref path, spelling } if flag == "-o" && path == "foo.o" && spelling == OutputSpelling::Separate)); } } #[cfg(test)] mod environment_mapping_tests { use super::*; use std::collections::HashMap; fn collect_args(args: &[Argument]) -> Vec { args.iter() .flat_map(|a| match a { Argument::Other { arguments, .. } => arguments.clone(), Argument::Output { flag, path, .. } => vec![flag.clone(), path.clone()], Argument::Source { path, .. } => vec![path.clone()], }) .collect() } #[test] fn path_separator_mapping() { let rules = &[EnvRule::new( "CPATH", EnvMapping::Flag { flag: "-I", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing)), )]; let mut env = HashMap::new(); // std::env::split_paths uses `:` on Unix, `;` on Windows let (value, expected_a, expected_b) = if cfg!(windows) { (r"C:\a;C:\b", r"C:\a", r"C:\b") } else { ("/a:/b", "/a", "/b") }; env.insert("CPATH".to_string(), value.to_string()); let (prepend, append) = parse_environment(&env, rules, Syntax::GnuClang); assert!(prepend.is_empty()); let args = collect_args(&append); assert_eq!(args, vec!["-I", expected_a, "-I", expected_b]); } #[test] fn path_separator_filters_empty_elements() { let rules = &[EnvRule::new( "CPATH", EnvMapping::Flag { flag: "-I", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing)), )]; let mut env = HashMap::new(); // Leading/trailing/double separators produce empty elements that must be filtered let (value, expected_a, expected_b) = if cfg!(windows) { (r";C:\a;;C:\b;", r"C:\a", r"C:\b") } else { (":/a::/b:", "/a", "/b") }; env.insert("CPATH".to_string(), value.to_string()); let (_prepend, append) = parse_environment(&env, rules, Syntax::GnuClang); let args = collect_args(&append); assert_eq!(args, vec!["-I", expected_a, "-I", expected_b]); } #[test] fn fixed_separator_mapping() { let rules = &[EnvRule::new( "INCLUDE", EnvMapping::Flag { flag: "/I", separator: EnvSeparator::Fixed(";") }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing)), )]; let mut env = HashMap::new(); env.insert("INCLUDE".to_string(), r"C:\a;C:\b".to_string()); let (_prepend, append) = parse_environment(&env, rules, Syntax::Msvc); let args = collect_args(&append); assert_eq!(args, vec!["/I", r"C:\a", "/I", r"C:\b"]); } // Requirements: output-env-derived-flags #[test] fn expand_prepend() { let rules = &[EnvRule::new( "CL", EnvMapping::Expand { position: EnvPosition::Prepend }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling)), )]; let mut env = HashMap::new(); env.insert("CL".to_string(), "/O2 /W4".to_string()); let (prepend, append) = parse_environment(&env, rules, Syntax::Msvc); assert!(append.is_empty()); let args = collect_args(&prepend); assert_eq!(args, vec!["/O2", "/W4"]); } // Requirements: output-env-derived-flags #[test] fn expand_append() { let rules = &[EnvRule::new( "_CL_", EnvMapping::Expand { position: EnvPosition::Append }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling)), )]; let mut env = HashMap::new(); env.insert("_CL_".to_string(), "/link foo.lib".to_string()); let (prepend, append) = parse_environment(&env, rules, Syntax::Msvc); assert!(prepend.is_empty()); let args = collect_args(&append); assert_eq!(args, vec!["/link", "foo.lib"]); } fn family(id: &str) -> &'static FamilyDef { FAMILIES.iter().find(|def| def.id == id).unwrap() } fn msvc() -> FlagBasedInterpreter { FlagBasedInterpreter::new(family("msvc"), true) } fn armclang() -> FlagBasedInterpreter { FlagBasedInterpreter::new(family("armclang"), true) } // Requirements: output-env-derived-flags, output-json-compilation-database #[test] fn prepended_environment_words_follow_the_compiler() { let cases: Vec<(FlagBasedInterpreter, Execution, Vec<&str>)> = vec![ ( msvc(), Execution::from_strings( "cl", vec!["cl", "/c", "foo.c"], "/home/user", HashMap::from([("CL", "/O2 /W4"), ("_CL_", "/DX")]), ), vec!["cl", "/O2", "/W4", "/c", "foo.c", "/DX"], ), ( armclang(), Execution::from_strings( "armclang", vec!["armclang", "-c", "foo.c"], "/home/user", HashMap::from([("ARMCOMPILER6_CLANGOPT", "-O2")]), ), vec!["armclang", "-O2", "-c", "foo.c"], ), ]; for (interpreter, execution, expected) in cases { let input = execution.to_string(); let sut = match interpreter.recognize(execution) { RecognizeResult::Recognized(command) => collect_args(&command.arguments), _ => panic!("expected a recognized command for {input}"), }; assert_eq!(sut, expected, "argument order for {input}"); } } // Requirements: output-env-derived-flags #[test] fn environment_words_split_with_the_family_syntax() { let cl = |value: &str| { Execution::from_strings( "cl", vec!["cl", "/c", "foo.c"], "/home/user", HashMap::from([("CL", value)]), ) }; let armclang_opt = |value: &str| { Execution::from_strings( "armclang", vec!["armclang", "-c", "foo.c"], "/home/user", HashMap::from([("ARMCOMPILER6_CLANGOPT", value)]), ) }; let cases: Vec<(FlagBasedInterpreter, Execution, Vec<&str>)> = vec![ (msvc(), cl(r"/IC:\foo /W4"), vec!["cl", r"/IC:\foo", "/W4", "/c", "foo.c"]), ( msvc(), cl(r#"/I"C:\Program Files\x" /W4"#), vec!["cl", r"/IC:\Program Files\x", "/W4", "/c", "foo.c"], ), (msvc(), cl(r#"/DNAME=\"v\""#), vec!["cl", r#"/DNAME="v""#, "/c", "foo.c"]), ( armclang(), armclang_opt(r#"-I/a\ b -DX=\"v\""#), vec!["armclang", "-I/a b", r#"-DX="v""#, "-c", "foo.c"], ), ]; for (interpreter, execution, expected) in cases { let input = execution.to_string(); let sut = match interpreter.recognize(execution) { RecognizeResult::Recognized(command) => collect_args(&command.arguments), _ => panic!("expected a recognized command for {input}"), }; assert_eq!(sut, expected, "environment words for {input}"); } } #[test] fn missing_variable_produces_no_args() { let rules = &[EnvRule::new( "CPATH", EnvMapping::Flag { flag: "-I", separator: EnvSeparator::Path }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing)), )]; let env = HashMap::new(); let (prepend, append) = parse_environment(&env, rules, Syntax::GnuClang); assert!(prepend.is_empty()); assert!(append.is_empty()); } #[test] fn nvidia_hpc_has_no_gcc_env_rules() { assert!( NVIDIA_HPC_ENV_RULES.is_empty(), "NVIDIA HPC should not inherit GCC environment variables, got {} rules", NVIDIA_HPC_ENV_RULES.len() ); } #[test] fn clang_inherits_gcc_env_rules() { let gcc_vars: std::collections::HashSet<&str> = GCC_ENV_RULES.iter().map(|r| r.variable).collect(); let clang_vars: std::collections::HashSet<&str> = CLANG_ENV_RULES.iter().map(|r| r.variable).collect(); let missing: Vec<&str> = gcc_vars.difference(&clang_vars).cloned().collect(); assert!(missing.is_empty(), "Clang should inherit all GCC env vars, missing: {:?}", missing); } #[test] fn armclang_inherits_gcc_env_rules_transitively() { let gcc_vars: std::collections::HashSet<&str> = GCC_ENV_RULES.iter().map(|r| r.variable).collect(); let arm_vars: std::collections::HashSet<&str> = ARMCLANG_ENV_RULES.iter().map(|r| r.variable).collect(); let missing: Vec<&str> = gcc_vars.difference(&arm_vars).cloned().collect(); assert!( missing.is_empty(), "ARMClang should transitively inherit all GCC env vars, missing: {:?}", missing ); // Also has its own variable assert!(arm_vars.contains("ARMCOMPILER6_CLANGOPT")); } #[test] fn msvc_env_rules_present() { let vars: Vec<&str> = MSVC_ENV_RULES.iter().map(|r| r.variable).collect(); assert!(vars.contains(&"CL")); assert!(vars.contains(&"_CL_")); assert!(vars.contains(&"INCLUDE")); assert!(vars.contains(&"LIB")); } #[test] fn clang_cl_inherits_msvc_env_rules() { let msvc_vars: std::collections::HashSet<&str> = MSVC_ENV_RULES.iter().map(|r| r.variable).collect(); let clang_cl_vars: std::collections::HashSet<&str> = CLANG_CL_ENV_RULES.iter().map(|r| r.variable).collect(); let missing: Vec<&str> = msvc_vars.difference(&clang_cl_vars).cloned().collect(); assert!(missing.is_empty(), "Clang-CL should inherit all MSVC env vars, missing: {:?}", missing); } #[test] fn expand_with_quoted_values() { let rules = &[EnvRule::new( "CL", EnvMapping::Expand { position: EnvPosition::Prepend }, ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling)), )]; let mut env = HashMap::new(); env.insert("CL".to_string(), r#"/DPATH="C:\Program Files" /W4"#.to_string()); let (prepend, _append) = parse_environment(&env, rules, Syntax::Msvc); let args = collect_args(&prepend); assert_eq!(args, vec![r"/DPATH=C:\Program Files", "/W4"]); } } rizsotto-Bear-0c30b27/crates/semantic/src/interpreters/compilers/identity.rs000066400000000000000000000210751527177720300274150ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! The identity of a recognized tool: a compiler family or a launcher. //! //! This module also owns the resolution of a configured `as:` spelling onto //! that identity. Resolution lives here, next to the generated id data it //! validates against, so the [`CompilerId`] invariant -- always one of the //! generated [`KNOWN_IDS`], as a `&'static str` -- holds by construction. //! The configuration schema keeps the spelling the user wrote and knows //! nothing about the compiler families. use std::fmt; use std::str::FromStr; // Generated compiler-id data (KNOWN_IDS, WRAPPER_AS_NAMES) from // compilers/*.yaml. These are the sole accepted config `as:` spellings; the // resolution below validates against them instead of a hand-maintained // mirror of the family set. See compiler-family-definition. include!(concat!(env!("OUT_DIR"), "/compiler_ids.rs")); /// A compiler family's identity: the `compiler.id` declared in its YAML /// definition, which is also its sole accepted config `as:` spelling. /// /// Wraps a `&'static str` that is always one of the generated [`KNOWN_IDS`]: /// [`CompilerType`]'s [`FromStr`] resolves user input to a canonical entry, /// and internal literals are drift-guarded against that list by a test. #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] pub struct CompilerId(&'static str); impl CompilerId { /// Construct from a compile-time-known id. The caller guarantees the id is /// one of [`KNOWN_IDS`]; `compiler_id_literals_are_known` enforces this for /// every internal literal. pub(crate) const fn new(id: &'static str) -> Self { CompilerId(id) } /// The canonical id string, verbatim. pub fn as_str(&self) -> &'static str { self.0 } } impl fmt::Display for CompilerId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.0) } } /// The kind of tool Bear recognizes: a real compiler family (carrying its /// [`CompilerId`]) or a compiler launcher (wrapper). Mirrors the YAML `type:` /// field; the family set lives in data ([`KNOWN_IDS`]), not in this enum. #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] pub enum CompilerType { Compiler(CompilerId), Wrapper, } impl CompilerType { /// Construct a compiler-kind value from a compile-time-known id. pub(crate) const fn compiler(id: &'static str) -> Self { CompilerType::Compiler(CompilerId::new(id)) } /// Map a generated `RECOGNITION_PATTERNS` id column to a value. The string /// is trusted generated data: `"wrapper"` names the launcher kind, every /// other value is a real compiler id. pub(crate) fn from_recognized_id(id: &'static str) -> Self { if id == "wrapper" { CompilerType::Wrapper } else { CompilerType::compiler(id) } } } impl fmt::Display for CompilerType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { CompilerType::Compiler(id) => fmt::Display::fmt(id, f), CompilerType::Wrapper => f.write_str("wrapper"), } } } /// Failure to resolve a configured `as:` spelling onto a `CompilerType`. #[derive(Debug, thiserror::Error)] pub enum SpellingError { // List every accepted `as:` value: the compiler ids plus the launcher // spellings ("wrapper" and each basename). Omitting the launcher // spellings would misdescribe a mistyped `ccache` as if only compiler // ids were valid. #[error( "unknown compiler id {spelling:?}, expected one of: {}, wrapper, {}", KNOWN_IDS.join(", "), WRAPPER_AS_NAMES.join(", ") )] Unknown { spelling: String }, } impl FromStr for CompilerType { type Err = SpellingError; fn from_str(spelling: &str) -> Result { // The four launchers share one runtime kind: "wrapper" plus each // launcher basename all resolve to it (see compiler-family-definition). if spelling == "wrapper" || WRAPPER_AS_NAMES.contains(&spelling) { return Ok(CompilerType::Wrapper); } // A compiler family is its id, verbatim -- no aliases. Resolve to the // canonical `&'static str` so the value is unconstructible unvalidated. if let Some(&canonical) = KNOWN_IDS.iter().find(|&&id| id == spelling) { return Ok(CompilerType::Compiler(CompilerId::new(canonical))); } Err(SpellingError::Unknown { spelling: spelling.to_string() }) } } #[cfg(test)] mod tests { use super::*; #[test] fn compiler_id_literals_are_known() { // Every compiler id hand-written as a literal in production code must // be one of the generated KNOWN_IDS. If a YAML rename ever drifts one // of these, this fails instead of silently misclassifying at runtime. // // These are the only such literals: the ambiguous-name probe's two // verdicts and the recognition fallback (all `gcc`/`clang`). Family // registration and response-file syntax are driven entirely by // generated data (the FAMILIES registry), so they carry no id // literals to guard here; the dispatch test // (every_recognition_pattern_row_is_dispatched_by_a_registered_interpreter) // covers that path end to end. for id in ["gcc", "clang"] { assert!(KNOWN_IDS.contains(&id), "internal id literal {id:?} is not in KNOWN_IDS"); } } #[test] fn unknown_as_value_error_lists_wrapper_and_a_real_id() { // The "expected one of" list must name every accepted `as:` value, // including the launcher spellings -- not just the compiler ids. let sut = "nope".parse::().unwrap_err().to_string(); assert!(sut.contains("unknown compiler id"), "message: {sut}"); assert!(sut.contains(KNOWN_IDS[0]), "message should list a real id: {sut}"); assert!(sut.contains("wrapper"), "message should list the wrapper spelling: {sut}"); assert!(sut.contains(WRAPPER_AS_NAMES[0]), "message should list a launcher name: {sut}"); } #[test] fn compiler_type_round_trips_through_its_spelling() { // Display then resolve yields the same value, for both kinds. for sut in [CompilerType::compiler(KNOWN_IDS[0]), CompilerType::Wrapper] { let spelling = sut.to_string(); let back: CompilerType = spelling.parse().unwrap(); assert_eq!(back, sut, "round-trip via {spelling}"); } } #[test] fn every_known_id_resolves_to_its_own_family() { // Every canonical id resolves to its own compiler family. The set // is generated (KNOWN_IDS); no id is hand-listed here, so a new family // is covered automatically. for &id in KNOWN_IDS { let sut = id.parse::().unwrap(); assert_eq!(sut, CompilerType::compiler(id), "id: {id}"); } } #[test] fn every_launcher_spelling_resolves_to_the_wrapper_kind() { // The wrapper kind: "wrapper" plus every launcher basename resolve to // the one runtime CompilerType::Wrapper (see compiler-family-definition). for spelling in std::iter::once("wrapper").chain(WRAPPER_AS_NAMES.iter().copied()) { let sut = spelling.parse::().unwrap(); assert_eq!(sut, CompilerType::Wrapper, "spelling: {spelling}"); } } #[test] fn dropped_aliases_are_rejected() { // Every spelling that used to be an accepted alias now fails: one id, // one spelling, verbatim. let dropped_spellings = ["clangcl", "llvm", "intel-cc", "craycc"]; for spelling in dropped_spellings { let result = spelling.parse::(); match result { Err(error) => { let message = error.to_string(); assert!( message.contains("unknown compiler id"), "case: {spelling}, unexpected error message: {message}" ); } Ok(value) => panic!("expected {spelling} to be rejected, got: {value:?}"), } } } #[test] fn display_is_the_id_verbatim() { // Display prints the id, not a curated pretty name: one spelling // everywhere (YAML, config `as:`, generated data, output). assert_eq!(CompilerType::compiler("gcc").to_string(), "gcc"); assert_eq!(CompilerType::compiler("clang_cl").to_string(), "clang_cl"); assert_eq!(CompilerType::compiler("intel_fortran").to_string(), "intel_fortran"); assert_eq!(CompilerType::Wrapper.to_string(), "wrapper"); } } rizsotto-Bear-0c30b27/crates/semantic/src/interpreters/compilers/mod.rs000066400000000000000000003157721527177720300263550ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Compiler interpreters for recognizing and parsing compiler invocations. //! //! This module provides interpreters for various compiler toolchains including //! GCC, Clang, CUDA, and Fortran compilers, as well as support for compiler //! wrappers like ccache, distcc, and sccache. pub mod compiler_recognition; mod flag_based; pub mod identity; mod print; mod probe; mod response_file; mod wrapper; pub use print::print_compilers; use super::super::{Interpreter, RecognizeResult}; use super::combinators::OutputLogger; use compiler_recognition::{CompilerHints, CompilerRecognizer}; use identity::CompilerType; use intercept::Execution; use std::collections::HashMap; /// Main compiler interpreter that delegates to specific compiler implementations. /// /// `recognize` runs the recognizer to classify the executable, transparently /// peels off any wrapper layer (ccache/distcc/sccache) via [`wrapper::unwrap`], /// then dispatches to the per-type flag interpreter. pub struct CompilerInterpreter { recognizer: CompilerRecognizer, interpreters: HashMap>, inline_response_files: bool, } impl CompilerInterpreter { /// Builds a fully configured compiler interpreter from the compiler hints /// and the two argument-formatting switches: `from_response_files` inlines /// `@file` references, `from_environment` folds compiler environment /// variables into the arguments. Registers every supported compiler type /// exactly once. pub fn new_with_format(hints: CompilerHints, from_response_files: bool, from_environment: bool) -> Self { let mut result = Self { recognizer: CompilerRecognizer::new_with_hints(hints), interpreters: HashMap::new(), inline_response_files: from_response_files, }; result.register_all(from_environment); result } /// Convenience constructor with the default argument handling (response-file /// inlining off, environment-flag folding on). Used by [`Default`] and tests. pub fn new_with_hints(hints: CompilerHints) -> Self { Self::new_with_format(hints, false, true) } /// Registers every supported compiler family from the generated /// [`flag_based::FAMILIES`] registry, threading `from_environment` into /// each per-family interpreter. Adding a family is a YAML edit; nothing /// here changes. fn register_all(&mut self, from_environment: bool) { for def in flag_based::FAMILIES { self.register(CompilerType::compiler(def.id), flag_based::interpreter(def, from_environment)); } } /// Registers an interpreter for a specific compiler type, wrapping it /// with [`OutputLogger`] so its recognition result is traced. fn register(&mut self, compiler_type: CompilerType, interpreter: impl Interpreter + 'static) { let logged_interpreter = OutputLogger::new(interpreter, compiler_type.to_string()); self.interpreters.insert(compiler_type, Box::new(logged_interpreter)); } } impl Default for CompilerInterpreter { fn default() -> Self { Self::new_with_hints(CompilerHints::new()) } } impl Interpreter for CompilerInterpreter { fn recognize(&self, execution: Execution) -> RecognizeResult { // Classify the executable, peeling off a wrapper layer in place if // needed. ccache/distcc/sccache aren't real compilers; they exist // to carry one in argv. wrapper::unwrap returns both the unwrapped // execution and the inner compiler's type so we don't have to // re-run the recognizer after unwrapping. let (execution, compiler_type) = match self.recognizer.recognize(&execution.executable) { Some(CompilerType::Wrapper) => match wrapper::unwrap(execution, &self.recognizer) { Ok(unwrapped) => unwrapped, Err(execution) => return RecognizeResult::NotRecognized(execution), }, Some(ty) => (execution, ty), None => return RecognizeResult::NotRecognized(execution), }; // Inline @file response files before flag classification, so their // tokens are classified, link-stripped, and split per source like any // other argument. Tokenization follows the family just identified. let execution = if self.inline_response_files { response_file::expand(execution, response_file::syntax_for(compiler_type)) } else { execution }; match self.interpreters.get(&compiler_type) { Some(interpreter) => interpreter.recognize(execution), None => RecognizeResult::NotRecognized(execution), } } } #[cfg(test)] mod tests { use super::*; use crate::{ArgumentKind, CompilerPass, PassEffect}; use std::borrow::Cow; use std::collections::HashMap; use std::path::{Path, PathBuf}; use ArgumentKind::*; /// Short alias for `Other(PassEffect::StopsAt(pass))`. fn stops_at(pass: CompilerPass) -> ArgumentKind { Other(PassEffect::StopsAt(pass)) } /// Short alias for `Other(PassEffect::Configures(pass))`. fn configures(pass: CompilerPass) -> ArgumentKind { Other(PassEffect::Configures(pass)) } /// Short alias for `Other(PassEffect::None)`. fn none() -> ArgumentKind { Other(PassEffect::None) } /// Short alias for `Other(PassEffect::DriverOption)`. fn driver() -> ArgumentKind { Other(PassEffect::DriverOption) } /// Short alias for `Other(PassEffect::InfoAndExit)`. fn info() -> ArgumentKind { Other(PassEffect::InfoAndExit) } fn assert_command(result: RecognizeResult, expected: Vec<(ArgumentKind, Vec<&str>)>) { let RecognizeResult::Recognized(cmd) = result else { panic!("Expected Recognized, got {:?}", result); }; let actual: Vec<(ArgumentKind, Vec)> = cmd.arguments.iter().map(|a| (a.kind(), a.as_arguments(&|p| Cow::Borrowed(p)))).collect(); let expected: Vec<(ArgumentKind, Vec)> = expected.into_iter().map(|(k, args)| (k, args.into_iter().map(String::from).collect())).collect(); assert_eq!(actual, expected); } fn assert_ignored(result: RecognizeResult, expected_reason: &str) { let RecognizeResult::Ignored(reason) = result else { panic!("Expected Ignored, got {:?}", result); }; assert_eq!(reason, expected_reason); } fn create_execution(executable: &str, args: Vec<&str>, working_dir: &str) -> Execution { Execution::from_strings(executable, args, working_dir, HashMap::new()) } fn create_execution_with_env( executable: &str, args: Vec<&str>, working_dir: &str, environment: HashMap<&str, &str>, ) -> Execution { Execution::from_strings(executable, args, working_dir, environment) } fn create_path_string(paths: &[&str]) -> String { let path_bufs: Vec = paths.iter().map(std::path::PathBuf::from).collect(); std::env::join_paths(path_bufs).unwrap().to_string_lossy().to_string() } fn noop(path: &Path) -> Cow<'_, Path> { Cow::from(path) } mod structural { use super::*; #[test] fn gcc_recognition_and_delegation() { let sut = CompilerInterpreter::default(); let execution = create_execution("/usr/bin/gcc", vec!["/usr/bin/gcc", "-c", "test.c"], "/tmp"); let result = sut.recognize(execution); assert!(matches!(result, RecognizeResult::Recognized(_)), "GCC command should be recognized"); if let RecognizeResult::Recognized(cmd) = result { assert_eq!(cmd.executable, PathBuf::from("/usr/bin/gcc")); assert_eq!(cmd.working_dir, PathBuf::from("/tmp")); } else { panic!("Expected compiler command"); } } #[test] fn clang_recognition_and_delegation() { let sut = CompilerInterpreter::default(); let execution = create_execution("clang", vec!["clang", "-c", "main.c", "-o", "main.o"], "/tmp"); let result = sut.recognize(execution); assert!(matches!(result, RecognizeResult::Recognized(_)), "Clang command should be recognized"); if let RecognizeResult::Recognized(cmd) = result { assert_eq!(cmd.executable, PathBuf::from("clang")); assert_eq!(cmd.working_dir, PathBuf::from("/tmp")); } else { panic!("Expected compiler command"); } } #[test] fn unrecognized_compiler() { let sut = CompilerInterpreter::default(); let execution = create_execution("unknown_compiler", vec!["unknown_compiler", "-c", "test.c"], "/tmp"); assert!( matches!(sut.recognize(execution), RecognizeResult::NotRecognized(_)), "Unknown compiler should not be recognized" ); } #[test] fn delegation_preserves_execution_details() { let sut = CompilerInterpreter::default(); let working_dir = PathBuf::from("/custom/working/dir"); let mut environment = std::collections::HashMap::new(); environment.insert("CC".to_string(), "gcc".to_string()); let execution = Execution { executable: PathBuf::from("gcc"), arguments: vec!["gcc".to_string(), "-c".to_string(), "file.c".to_string()], working_dir: working_dir.clone(), environment, }; let result = sut.recognize(execution); assert!(matches!(result, RecognizeResult::Recognized(_)), "Command should be recognized"); if let RecognizeResult::Recognized(cmd) = result { assert_eq!(cmd.working_dir, working_dir); } else { panic!("Expected compiler command"); } } #[test] fn end_to_end_config_based_compiler_hints() { let mut hints = CompilerHints::new(); hints.add(Path::new("/custom/path/my-gcc"), Some("gcc")).unwrap(); hints.add(Path::new("/opt/clang/bin/clang++"), Some("clang")).unwrap(); let sut = CompilerInterpreter::new_with_hints(hints); let custom_gcc = create_execution("/custom/path/my-gcc", vec!["/custom/path/my-gcc", "-c", "test.c"], "/tmp"); assert!( matches!(sut.recognize(custom_gcc), RecognizeResult::Recognized(_)), "Custom GCC path should be recognized via config hint" ); let custom_clang = create_execution( "/opt/clang/bin/clang++", vec!["/opt/clang/bin/clang++", "-c", "main.cpp"], "/tmp", ); assert!( matches!(sut.recognize(custom_clang), RecognizeResult::Recognized(_)), "Custom Clang path should be recognized via config hint" ); let normal_gcc = create_execution("gcc", vec!["gcc", "-c", "normal.c"], "/tmp"); assert!( matches!(sut.recognize(normal_gcc), RecognizeResult::Recognized(_)), "Standard GCC should still be recognized" ); } // Requirements: recognition-compiler-launchers #[test] fn wrapper_recognition_and_delegation() { let sut = CompilerInterpreter::default(); let ccache_execution = create_execution("ccache", vec!["ccache", "gcc", "-c", "test.c"], "/tmp"); let result = sut.recognize(ccache_execution); if let RecognizeResult::Recognized(cmd) = result { assert_eq!(*cmd.executable, *"gcc"); let arguments: Vec = cmd.arguments.into_iter().flat_map(|arg| arg.as_arguments(&noop)).collect(); assert_eq!(vec!["gcc".to_string(), "-c".to_string(), "test.c".to_string()], arguments); } else { panic!("Expected compiler command"); } } #[test] fn uniform_delegation() { let sut = CompilerInterpreter::default(); let executables = vec!["gcc", "clang", "nvcc", "gfortran", "ifort"]; for executable in executables { let execution = create_execution(executable, vec![executable, "-c", "test.c"], "/tmp"); let recognized_type = sut.recognizer.recognize(&execution.executable); if let Some(compiler_type) = recognized_type { assert!( sut.interpreters.contains_key(&compiler_type), "Interpreter should be registered for {}", executable ); } } } /// Data-driven completeness check over every row of the codegen-generated /// `RECOGNITION_PATTERNS` table (see `compiler_recognition.rs`): for the /// first executable name in each row, dispatch through the full /// `CompilerInterpreter` (recognizer + wrapper-unwrap + registered /// interpreter), not just the recognizer. /// /// Why this matters: recognition and registration are generated from /// the same compiler-definition data, but through separate paths (the /// `RECOGNITION_PATTERNS` table and the `FAMILIES` registry). If those /// two ever disagree -- a recognized id with no registry row -- every /// recognition unit test still passes (the regex matches and /// `recognize()` returns `Some(type)`), but `CompilerInterpreter` has no /// interpreter for that type, so every real execution of the family /// silently falls through to `NotRecognized` -- the same failure mode /// as not recognizing the compiler at all, just discovered later and /// more confusingly. This test catches that gap for every family /// automatically, current and future, without needing a new test per /// family. /// /// `NotRecognized` is the only failure signal: `Ignored` also proves /// registration ran (the internal-executable rows -- cc1, c1, etc. -- /// legitimately resolve there via `ignore_when`), so the assertion is /// "not NotRecognized", not "Recognized". #[test] fn every_recognition_pattern_row_is_dispatched_by_a_registered_interpreter() { let sut = CompilerInterpreter::default(); for &(type_str, executables, _cross_compilation, _versioned, _description) in compiler_recognition::RECOGNITION_PATTERNS { let name = executables[0]; // Every family dispatches on a plain "-c hello.c" invocation // regardless of the source extension: source-vs-flag // classification in FlagBasedInterpreter never inspects the // file extension (verified empirically against vala, cuda, // and the fortran families -- all still recognize a `.c` // source; extension only affects the binary-vs-source flag on // an already-classified Source argument, not the RecognizeResult // variant). Wrapper rows are the one exception: `-c` is not a // real compiler name, so a wrapper's own unwrap logic would // resolve NotRecognized on that invocation; dispatch a full // valid wrapper invocation instead (` gcc -c hello.c`), // which also proves every wrapper basename unwraps to a // registered inner compiler. let args = if type_str == "wrapper" { vec![name, "gcc", "-c", "hello.c"] } else { vec![name, "-c", "hello.c"] }; let execution = create_execution(name, args, "/project"); let result = sut.recognize(execution); assert!( !matches!(result, RecognizeResult::NotRecognized(_)), "family '{}' (executable '{}') matched the recognizer but was not dispatched \ by any registered interpreter -- its id is in RECOGNITION_PATTERNS but not in \ the generated FAMILIES registry", type_str, name ); } } } mod gcc { use super::*; #[test] fn simple_compilation() { let sut = CompilerInterpreter::default(); let execution = create_execution("gcc", vec!["gcc", "-c", "main.c", "-o", "main.o"], "/project"); assert_command( sut.recognize(execution), vec![ (Compiler, vec!["gcc"]), (stops_at(CompilerPass::Compiling), vec!["-c"]), (Source { binary: false }, vec!["main.c"]), (Output, vec!["-o", "main.o"]), ], ); } #[test] fn ada_compilation() { let sut = CompilerInterpreter::default(); for source in ["main.adb", "sensor.ads"] { let execution = create_execution("gcc", vec!["gcc", "-c", "-x", "ada", "-gnatA", source], "/project"); assert_command( sut.recognize(execution), vec![ (Compiler, vec!["gcc"]), (stops_at(CompilerPass::Compiling), vec!["-c"]), (configures(CompilerPass::Compiling), vec!["-x", "ada"]), (configures(CompilerPass::Compiling), vec!["-gnatA"]), (Source { binary: false }, vec![source]), ], ); } } #[test] fn combined_flags() { let sut = CompilerInterpreter::default(); let execution = create_execution( "gcc", vec!["gcc", "-I/usr/include", "-DDEBUG=1", "-o", "main", "main.c"], "/project", ); assert_command( sut.recognize(execution), vec![ (Compiler, vec!["gcc"]), (configures(CompilerPass::Preprocessing), vec!["-I/usr/include"]), (configures(CompilerPass::Preprocessing), vec!["-DDEBUG=1"]), (Output, vec!["-o", "main"]), (Source { binary: false }, vec!["main.c"]), ], ); } #[test] fn separate_flags() { let sut = CompilerInterpreter::default(); let execution = create_execution( "gcc", vec!["gcc", "-I", "/usr/include", "-D", "DEBUG=1", "main.c"], "/project", ); assert_command( sut.recognize(execution), vec![ (Compiler, vec!["gcc"]), (configures(CompilerPass::Preprocessing), vec!["-I", "/usr/include"]), (configures(CompilerPass::Preprocessing), vec!["-D", "DEBUG=1"]), (Source { binary: false }, vec!["main.c"]), ], ); } #[test] fn response_file() { let sut = CompilerInterpreter::default(); let execution = create_execution("gcc", vec!["gcc", "@response.txt", "main.c"], "/project"); assert_command( sut.recognize(execution), vec![ (Compiler, vec!["gcc"]), (configures(CompilerPass::Compiling), vec!["@response.txt"]), (Source { binary: false }, vec!["main.c"]), ], ); } #[test] fn warning_flags() { let sut = CompilerInterpreter::default(); let execution = create_execution("gcc", vec!["gcc", "-Wall", "-Wextra", "-Wno-unused", "main.c"], "/project"); assert_command( sut.recognize(execution), vec![ (Compiler, vec!["gcc"]), (none(), vec!["-Wall"]), (none(), vec!["-Wextra"]), (none(), vec!["-Wno-unused"]), (Source { binary: false }, vec!["main.c"]), ], ); } #[test] fn std_flag_variations() { let sut = CompilerInterpreter::default(); for (args, expected_flag_args) in [ (vec!["gcc", "-std", "c99", "main.c"], vec!["-std", "c99"]), (vec!["gcc", "-std=c99", "main.c"], vec!["-std=c99"]), ] { let execution = create_execution("gcc", args, "/project"); let result = sut.recognize(execution); if let RecognizeResult::Recognized(cmd) = result { assert_eq!(cmd.arguments[1].kind(), configures(CompilerPass::Compiling)); assert_eq!(cmd.arguments[1].as_arguments(&|p| Cow::Borrowed(p)), expected_flag_args); } } } #[test] fn complex_compilation() { let sut = CompilerInterpreter::default(); let execution = create_execution( "gcc", vec![ "gcc", "-Wall", "-Werror", "-O2", "-g", "-I/usr/local/include", "-I", "/opt/include", "-DVERSION=1.0", "-D", "DEBUG", "-fPIC", "-m64", "-c", "main.c", "utils.c", "-o", "program", ], "/project", ); let result = sut.recognize(execution); if let RecognizeResult::Recognized(cmd) = result { assert!(cmd.arguments.len() >= 10); let source_count = cmd.arguments.iter().filter(|a| matches!(a.kind(), Source { .. })).count(); let output_count = cmd.arguments.iter().filter(|a| a.kind() == Output).count(); assert_eq!(source_count, 2); assert_eq!(output_count, 1); } else { panic!("Expected compiler command"); } } #[test] fn comprehensive_flag_coverage() { let sut = CompilerInterpreter::default(); // (flags, expected_kind) for prefix-matching flag groups let cases: Vec<(Vec<&str>, ArgumentKind)> = vec![ (vec!["-O2", "-Os", "-Ofast", "-Og"], configures(CompilerPass::Compiling)), (vec!["-g", "-g3", "-gdwarf-4", "-ggdb"], configures(CompilerPass::Compiling)), (vec!["-Wall", "-Wextra", "-Wno-unused", "-Werror=format"], none()), ( vec!["-fPIC", "-fstack-protector", "-fno-omit-frame-pointer", "-flto"], configures(CompilerPass::Compiling), ), ( vec!["-m64", "-march=native", "-mtune=generic", "-msse4.2"], configures(CompilerPass::Compiling), ), ]; for (flags, expected_kind) in cases { let mut args = vec!["gcc"]; args.extend(&flags); args.push("main.c"); let execution = create_execution("gcc", args, "/project"); let result = sut.recognize(execution); if let RecognizeResult::Recognized(cmd) = result { assert!(cmd.arguments.len() > flags.len()); for i in 1..=flags.len() { assert_eq!(cmd.arguments[i].kind(), expected_kind, "flag: {}", flags[i - 1]); } } } } #[test] fn linker_and_system_flags() { let sut = CompilerInterpreter::default(); // Test linker flags let execution = create_execution( "gcc", vec![ "gcc", "-Wl,--gc-sections", "-Wl,-rpath,/usr/local/lib", "-static", "-shared", "-pie", "-pthread", "main.c", ], "/project", ); let result = sut.recognize(execution); if let RecognizeResult::Recognized(cmd) = result { for i in 1..=3 { assert_eq!(cmd.arguments[i].kind(), configures(CompilerPass::Linking)); } } // Test system include and library paths let execution = create_execution( "gcc", vec![ "gcc", "-isystem", "/usr/local/include", "-L/usr/local/lib", "-lmath", "--sysroot=/opt/sysroot", "main.c", ], "/project", ); let result = sut.recognize(execution); if let RecognizeResult::Recognized(cmd) = result { assert_eq!(cmd.arguments[1].kind(), configures(CompilerPass::Preprocessing)); assert_eq!(cmd.arguments[2].kind(), configures(CompilerPass::Linking)); assert_eq!(cmd.arguments[3].kind(), configures(CompilerPass::Linking)); } } #[test] fn response_files_and_plugins() { let sut = CompilerInterpreter::default(); let execution = create_execution( "gcc", vec![ "gcc", "@compile_flags.txt", "-fplugin=myplugin.so", "-fplugin-arg-myplugin-option=value", "-save-temps=obj", "main.c", ], "/project", ); let result = sut.recognize(execution); if let RecognizeResult::Recognized(cmd) = result { assert_eq!(cmd.arguments[1].kind(), configures(CompilerPass::Compiling)); assert_eq!(cmd.arguments[2].kind(), configures(CompilerPass::Compiling)); assert_eq!(cmd.arguments[3].kind(), configures(CompilerPass::Compiling)); assert_eq!(cmd.arguments[4].kind(), driver()); } } /// Requirements: output-env-derived-flags #[test] fn environment_disabled_suppresses_injected_flags() { let sut = CompilerInterpreter::new_with_format(CompilerHints::new(), false, false); let cpath = create_path_string(&["/usr/include", "/opt/include"]); let mut env = HashMap::new(); env.insert("CPATH", cpath.as_str()); let execution = create_execution_with_env( "gcc", vec!["gcc", "-c", "main.c", "-o", "main.o"], "/project", env, ); assert_command( sut.recognize(execution), vec![ (Compiler, vec!["gcc"]), (stops_at(CompilerPass::Compiling), vec!["-c"]), (Source { binary: false }, vec!["main.c"]), (Output, vec!["-o", "main.o"]), ], ); } /// Uses contains-checks instead of assert_command because environment /// variable processing order may not match HashMap iteration order. #[test] fn environment_variables_cpath() { let sut = CompilerInterpreter::default(); let cpath = create_path_string(&["/usr/include", "/opt/include"]); let mut env = HashMap::new(); env.insert("CPATH", cpath.as_str()); let execution = create_execution_with_env( "gcc", vec!["gcc", "-c", "main.c", "-o", "main.o"], "/project", env, ); let result = sut.recognize(execution); if let RecognizeResult::Recognized(cmd) = result { assert_eq!(cmd.arguments.len(), 6); let args: Vec = cmd.arguments.iter().flat_map(|a| a.as_arguments(&|p| Cow::Borrowed(p))).collect(); assert!(args.contains(&"-I".to_string())); assert!(args.contains(&"/usr/include".to_string())); assert!(args.contains(&"/opt/include".to_string())); } else { panic!("Expected compiler command"); } } /// Uses contains-checks instead of assert_command because environment /// variable processing order may not match HashMap iteration order. #[test] fn environment_variables_c_include_path() { let sut = CompilerInterpreter::default(); let mut env = HashMap::new(); env.insert("C_INCLUDE_PATH", "/usr/local/include"); let execution = create_execution_with_env( "gcc", vec!["gcc", "-c", "main.c", "-o", "main.o"], "/project", env, ); let result = sut.recognize(execution); if let RecognizeResult::Recognized(cmd) = result { assert_eq!(cmd.arguments.len(), 5); let args: Vec = cmd.arguments.iter().flat_map(|a| a.as_arguments(&|p| Cow::Borrowed(p))).collect(); assert!(args.contains(&"-isystem".to_string())); assert!(args.contains(&"/usr/local/include".to_string())); } else { panic!("Expected compiler command"); } } /// Uses contains-checks instead of assert_command because environment /// variable processing order may not match HashMap iteration order. #[test] fn environment_variables_cplus_include_path() { let sut = CompilerInterpreter::default(); let mut env = HashMap::new(); env.insert("CPLUS_INCLUDE_PATH", "/usr/include/c++/11"); let execution = create_execution_with_env( "g++", vec!["g++", "-c", "main.cpp", "-o", "main.o"], "/project", env, ); let result = sut.recognize(execution); if let RecognizeResult::Recognized(cmd) = result { assert_eq!(cmd.arguments.len(), 5); let args: Vec = cmd.arguments.iter().flat_map(|a| a.as_arguments(&|p| Cow::Borrowed(p))).collect(); assert!(args.contains(&"-isystem".to_string())); assert!(args.contains(&"/usr/include/c++/11".to_string())); } else { panic!("Expected compiler command"); } } /// Uses contains-checks instead of assert_command because environment /// variable processing order may not match HashMap iteration order. #[test] fn environment_variables_multiple() { let sut = CompilerInterpreter::default(); let cpath = create_path_string(&["/usr/include", "/opt/include"]); let mut env = HashMap::new(); env.insert("CPATH", cpath.as_str()); env.insert("C_INCLUDE_PATH", "/usr/local/include"); env.insert("CPLUS_INCLUDE_PATH", "/usr/include/c++/11"); let execution = create_execution_with_env( "gcc", vec!["gcc", "-c", "main.c", "-o", "main.o"], "/project", env, ); let result = sut.recognize(execution); if let RecognizeResult::Recognized(cmd) = result { assert_eq!(cmd.arguments.len(), 8); let args: Vec = cmd.arguments.iter().flat_map(|a| a.as_arguments(&|p| Cow::Borrowed(p))).collect(); assert!(args.contains(&"/usr/include".to_string())); assert!(args.contains(&"/opt/include".to_string())); assert!(args.contains(&"/usr/local/include".to_string())); assert!(args.contains(&"/usr/include/c++/11".to_string())); } else { panic!("Expected compiler command"); } } /// Uses contains-checks instead of assert_command because environment /// variable processing order may not match HashMap iteration order. #[test] fn environment_variables_objc_include_path() { let sut = CompilerInterpreter::default(); let objc_include_path = create_path_string(&["/System/Library/Frameworks", "/usr/local/objc"]); let mut env = HashMap::new(); env.insert("OBJC_INCLUDE_PATH", objc_include_path.as_str()); let execution = create_execution_with_env( "gcc", vec!["gcc", "-c", "main.m", "-o", "main.o"], "/project", env, ); let result = sut.recognize(execution); if let RecognizeResult::Recognized(cmd) = result { assert_eq!(cmd.arguments.len(), 6); let args: Vec = cmd.arguments.iter().flat_map(|a| a.as_arguments(&|p| Cow::Borrowed(p))).collect(); assert!(args.contains(&"-isystem".to_string())); assert!(args.contains(&"/System/Library/Frameworks".to_string())); assert!(args.contains(&"/usr/local/objc".to_string())); } else { panic!("Expected compiler command"); } } /// Uses contains-checks instead of assert_command because environment /// variable processing order may not match HashMap iteration order. #[test] fn environment_variables_all_types() { let sut = CompilerInterpreter::default(); let mut env = HashMap::new(); env.insert("CPATH", "/usr/include"); env.insert("C_INCLUDE_PATH", "/usr/local/include"); env.insert("CPLUS_INCLUDE_PATH", "/usr/include/c++/11"); env.insert("OBJC_INCLUDE_PATH", "/System/Library/Frameworks"); let execution = create_execution_with_env( "gcc", vec!["gcc", "-c", "main.c", "-o", "main.o"], "/project", env, ); let result = sut.recognize(execution); if let RecognizeResult::Recognized(cmd) = result { assert_eq!(cmd.arguments.len(), 8); let args: Vec = cmd.arguments.iter().flat_map(|a| a.as_arguments(&|p| Cow::Borrowed(p))).collect(); assert!(args.contains(&"/usr/include".to_string())); assert!(args.contains(&"/usr/local/include".to_string())); assert!(args.contains(&"/usr/include/c++/11".to_string())); assert!(args.contains(&"/System/Library/Frameworks".to_string())); assert!(args.contains(&"-I".to_string())); assert!(args.contains(&"-isystem".to_string())); } else { panic!("Expected compiler command"); } } #[test] fn environment_variables_empty_paths() { let sut = CompilerInterpreter::default(); let c_include_path = create_path_string(&["", "", "", ""]); let mut env = HashMap::new(); env.insert("CPATH", ""); env.insert("C_INCLUDE_PATH", c_include_path.as_str()); let execution = create_execution_with_env( "gcc", vec!["gcc", "-c", "main.c", "-o", "main.o"], "/project", env, ); let result = sut.recognize(execution); if let RecognizeResult::Recognized(cmd) = result { assert_eq!(cmd.arguments.len(), 4); } else { panic!("Expected compiler command"); } } #[test] fn preprocessor_comprehensive_flags() { let sut = CompilerInterpreter::default(); let execution = create_execution( "gcc", vec![ "gcc", "-E", "-C", "-CC", "-P", "-traditional", "-trigraphs", "-undef", "-Wp,-MD,deps.d", "-M", "-MM", "-MG", "-MP", "main.c", ], "/project", ); let result = sut.recognize(execution); if let RecognizeResult::Recognized(cmd) = result { // -E, -M and -MM stop the compiler after preprocessing (the // latter two imply -E); the rest only configure it. for i in [1, 9, 10] { assert_eq!(cmd.arguments[i].kind(), stops_at(CompilerPass::Preprocessing), "index {i}"); } for i in (2..9).chain(11..13) { assert_eq!(cmd.arguments[i].kind(), configures(CompilerPass::Preprocessing), "index {i}"); } } } #[test] fn internal_executables_are_ignored() { let sut = CompilerInterpreter::default(); let internal_cases = [ ("/usr/libexec/gcc/x86_64-linux-gnu/11/cc1", vec!["cc1", "-quiet", "test.c"]), ("/usr/lib/gcc/x86_64-linux-gnu/11/cc1plus", vec!["cc1plus", "-quiet", "test.cpp"]), ( "/usr/libexec/gcc/x86_64-linux-gnu/11/collect2", vec!["collect2", "-o", "program", "main.o"], ), ( "/usr/libexec/gcc/x86_64-redhat-linux/15/f951", vec!["f951", "fortran.f90", "-mtune=generic", "-march=x86-64", "-o", "/tmp/cc6kwJ3Y.s"], ), ]; for (exe, args) in &internal_cases { let execution = create_execution(exe, args.clone(), "/home/user"); assert_ignored(sut.recognize(execution), "internal executable"); } // Regular gcc should still be recognized as a compiler let gcc_execution = create_execution("/usr/bin/gcc", vec!["gcc", "-c", "-O2", "main.c"], "/home/user"); assert!(matches!(sut.recognize(gcc_execution), RecognizeResult::Recognized(_))); } #[test] fn linker_command_with_object_files() { let sut = CompilerInterpreter::default(); let execution = create_execution( "gcc", vec!["gcc", "-o", "a.out", "source1.o", "source2.o", "-lx", "-L/usr/local/lib"], "/project", ); assert_command( sut.recognize(execution), vec![ (Compiler, vec!["gcc"]), (Output, vec!["-o", "a.out"]), (Source { binary: true }, vec!["source1.o"]), (Source { binary: true }, vec!["source2.o"]), (configures(CompilerPass::Linking), vec!["-lx"]), (configures(CompilerPass::Linking), vec!["-L/usr/local/lib"]), ], ); } #[test] fn comprehensive_linker_scenarios() { let sut = CompilerInterpreter::default(); // Mixed compilation and linking let execution = create_execution( "gcc", vec![ "gcc", "-o", "myprogram", "main.o", "utils.o", "lib.a", "-L/usr/lib", "-L", "/opt/lib", "-lmath", "-l", "pthread", "-Wl,--as-needed", "-static", "-pie", ], "/project", ); let result = sut.recognize(execution); if let RecognizeResult::Recognized(cmd) = result { assert!(cmd.arguments.len() >= 10); let linking_count = cmd .arguments .iter() .filter(|a| matches!(a.kind(), Other(PassEffect::Configures(CompilerPass::Linking)))) .count(); assert_eq!(linking_count, 7); } // Pure linking command let pure_linking = create_execution( "gcc", vec![ "gcc", "-o", "final_program", "obj1.o", "obj2.o", "obj3.o", "libstatic.a", "-lssl", "-lcrypto", "-L/usr/local/ssl/lib", "-Wl,-rpath,/usr/local/ssl/lib", ], "/build", ); let result = sut.recognize(pure_linking); if let RecognizeResult::Recognized(cmd) = result { let object_files: Vec<_> = cmd .arguments .iter() .filter(|a| { let args = a.as_arguments(&|p| Cow::Borrowed(p)); args.len() == 1 && (args[0].ends_with(".o") || args[0].ends_with(".a")) }) .collect(); assert_eq!(object_files.len(), 4); for obj_file in object_files { assert_eq!(obj_file.kind(), Source { binary: true }); } } } #[test] fn arch_flag_preserves_argument() { let sut = CompilerInterpreter::default(); let execution = create_execution( "gcc", vec!["gcc", "-arch", "arm64", "-Wall", "-O2", "-c", "hello.c"], "/project", ); let result = sut.recognize(execution); if let RecognizeResult::Recognized(cmd) = result { let arch_arg = cmd.arguments.iter().find(|a| { let tokens = a.as_arguments(&|p| Cow::Borrowed(p)); tokens.len() == 2 && tokens[0] == "-arch" && tokens[1] == "arm64" }); assert!(arch_arg.is_some(), "-arch arm64 should be captured as a single argument pair"); assert_eq!(arch_arg.unwrap().kind(), configures(CompilerPass::Compiling)); let bad_source = cmd.arguments.iter().any(|a| { let tokens = a.as_arguments(&|p| Cow::Borrowed(p)); tokens.len() == 1 && tokens[0] == "arm64" }); assert!(!bad_source, "arm64 must not be misclassified as a source file"); } else { panic!("Expected compiler command"); } } // Requirements: recognition-cpp20-modules #[test] fn modules_ts_flag_recognizes_module_interface_source() { let sut = CompilerInterpreter::default(); let execution = create_execution( "g++", vec!["g++", "-std=c++20", "-fmodules-ts", "-c", "mod.cppm"], "/project", ); assert_command( sut.recognize(execution), vec![ (Compiler, vec!["g++"]), (configures(CompilerPass::Compiling), vec!["-std=c++20"]), (configures(CompilerPass::Compiling), vec!["-fmodules-ts"]), (stops_at(CompilerPass::Compiling), vec!["-c"]), (Source { binary: false }, vec!["mod.cppm"]), ], ); } } mod clang { use super::*; #[test] fn simple_compilation() { let sut = CompilerInterpreter::default(); let execution = create_execution("clang", vec!["clang", "-c", "-O2", "main.c"], "/project"); assert_command( sut.recognize(execution), vec![ (Compiler, vec!["clang"]), (stops_at(CompilerPass::Compiling), vec!["-c"]), (configures(CompilerPass::Compiling), vec!["-O2"]), (Source { binary: false }, vec!["main.c"]), ], ); } #[test] fn specific_flags() { let sut = CompilerInterpreter::default(); let execution = create_execution( "clang++", vec![ "clang++", "-Weverything", "--target", "x86_64-apple-darwin", "-fsanitize=address", "-std=c++20", "main.cpp", ], "/project", ); assert_command( sut.recognize(execution), vec![ (Compiler, vec!["clang++"]), (none(), vec!["-Weverything"]), (configures(CompilerPass::Compiling), vec!["--target", "x86_64-apple-darwin"]), (configures(CompilerPass::Compiling), vec!["-fsanitize=address"]), (configures(CompilerPass::Compiling), vec!["-std=c++20"]), (Source { binary: false }, vec!["main.cpp"]), ], ); } #[test] fn optimization_flags() { let sut = CompilerInterpreter::default(); let execution = create_execution( "clang", vec!["clang", "-O3", "-flto", "-fsave-optimization-record", "main.c"], "/project", ); assert_command( sut.recognize(execution), vec![ (Compiler, vec!["clang"]), (configures(CompilerPass::Compiling), vec!["-O3"]), (configures(CompilerPass::Compiling), vec!["-flto"]), (configures(CompilerPass::Compiling), vec!["-fsave-optimization-record"]), (Source { binary: false }, vec!["main.c"]), ], ); } #[test] fn target_flag_variations() { let sut = CompilerInterpreter::default(); for (args, expected_flag_args) in [ ( vec!["clang", "--target", "arm64-apple-macos", "main.c"], vec!["--target", "arm64-apple-macos"], ), ( vec!["clang", "-target", "arm64-apple-macos", "main.c"], vec!["-target", "arm64-apple-macos"], ), ] { let execution = create_execution("clang", args, "/project"); let result = sut.recognize(execution); if let RecognizeResult::Recognized(cmd) = result { assert_eq!(cmd.arguments.len(), 3); assert_eq!(cmd.arguments[1].as_arguments(&|p| Cow::Borrowed(p)), expected_flag_args); } } } #[test] fn sanitizer_flags() { let sut = CompilerInterpreter::default(); let execution = create_execution( "clang", vec![ "clang", "-fsanitize=address,undefined", "-fsanitize-recover=unsigned-integer-overflow", "-fsanitize-ignorelist=mylist.txt", "main.c", ], "/project", ); assert_command( sut.recognize(execution), vec![ (Compiler, vec!["clang"]), (configures(CompilerPass::Compiling), vec!["-fsanitize=address,undefined"]), ( configures(CompilerPass::Compiling), vec!["-fsanitize-recover=unsigned-integer-overflow"], ), (configures(CompilerPass::Compiling), vec!["-fsanitize-ignorelist=mylist.txt"]), (Source { binary: false }, vec!["main.c"]), ], ); } #[test] fn mllvm_flag() { let sut = CompilerInterpreter::default(); let execution = create_execution( "clang", vec!["clang", "-O2", "-mllvm", "-inline-threshold=100", "myfile.c"], "/project", ); assert_command( sut.recognize(execution), vec![ (Compiler, vec!["clang"]), (configures(CompilerPass::Compiling), vec!["-O2"]), (configures(CompilerPass::Compiling), vec!["-mllvm", "-inline-threshold=100"]), (Source { binary: false }, vec!["myfile.c"]), ], ); } #[test] fn mllvm_flag_equals_form() { let sut = CompilerInterpreter::default(); let execution = create_execution( "clang", vec!["clang", "-O2", "-mllvm=-inline-threshold=100", "myfile.c"], "/project", ); assert_command( sut.recognize(execution), vec![ (Compiler, vec!["clang"]), (configures(CompilerPass::Compiling), vec!["-O2"]), (configures(CompilerPass::Compiling), vec!["-mllvm=-inline-threshold=100"]), (Source { binary: false }, vec!["myfile.c"]), ], ); } #[test] fn comprehensive_flag_coverage() { let sut = CompilerInterpreter::default(); let execution = create_execution( "clang", vec![ "clang", "-c", "-Wall", "-Weverything", "-O2", "-g", "-fmodules", "-fcolor-diagnostics", "-I/usr/include", "-D_GNU_SOURCE", "--target=x86_64-linux-gnu", "-fsanitize=address", "main.c", ], "/project", ); if let RecognizeResult::Recognized(cmd) = sut.recognize(execution) { assert_eq!(cmd.arguments.len(), 13); for i in 1..12 { match cmd.arguments[i].kind() { Other(PassEffect::Configures(_)) | Other(PassEffect::StopsAt(_)) | Other(PassEffect::None) => {} other => panic!("Unexpected argument kind at index {}: {:?}", i, other), } } assert_eq!(cmd.arguments[12].kind(), Source { binary: false }); } else { panic!("Expected compiler command"); } } #[test] fn cross_compilation_flags() { let sut = CompilerInterpreter::default(); let execution = create_execution( "clang", vec![ "clang", "--target=aarch64-linux-gnu", "--gcc-toolchain=/opt/gcc-cross", "--gcc-install-dir=/opt/gcc", "-triple", "arm64-apple-ios", "main.c", ], "/project", ); if let RecognizeResult::Recognized(cmd) = sut.recognize(execution) { assert_eq!(cmd.arguments.len(), 6); for i in 1..5 { assert_eq!(cmd.arguments[i].kind(), configures(CompilerPass::Compiling)); } } else { panic!("Expected compiler command"); } } #[test] fn cuda_and_openmp_flags() { let sut = CompilerInterpreter::default(); let execution = create_execution( "clang", vec![ "clang", "--cuda-path=/usr/local/cuda", "--cuda-gpu-arch=sm_70", "-fcuda-rdc", "-fopenmp", "-fopenmp-targets=nvptx64", "main.cu", ], "/project", ); if let RecognizeResult::Recognized(cmd) = sut.recognize(execution) { assert_eq!(cmd.arguments.len(), 7); for i in 1..6 { assert_eq!(cmd.arguments[i].kind(), configures(CompilerPass::Compiling)); } assert_eq!(cmd.arguments[6].kind(), Source { binary: false }); } else { panic!("Expected compiler command"); } } #[test] fn framework_and_plugin_flags() { let sut = CompilerInterpreter::default(); let execution = create_execution( "clang", vec![ "clang", "-F/System/Library/Frameworks", "-framework", "Foundation", "-load", "/path/to/plugin.so", "-plugin", "my-plugin", "main.m", ], "/project", ); assert_command( sut.recognize(execution), vec![ (Compiler, vec!["clang"]), (configures(CompilerPass::Compiling), vec!["-F/System/Library/Frameworks"]), (configures(CompilerPass::Linking), vec!["-framework", "Foundation"]), (configures(CompilerPass::Compiling), vec!["-load", "/path/to/plugin.so"]), (configures(CompilerPass::Compiling), vec!["-plugin", "my-plugin"]), (Source { binary: false }, vec!["main.m"]), ], ); } #[test] fn analysis_and_codegen_flags() { let sut = CompilerInterpreter::default(); let execution = create_execution( "clang", vec![ "clang", "--analyze", "-Xanalyzer", "-analyzer-output=text", "-emit-llvm", "-fprofile-instr-generate", "main.c", ], "/project", ); assert_command( sut.recognize(execution), vec![ (Compiler, vec!["clang"]), (configures(CompilerPass::Compiling), vec!["--analyze"]), (configures(CompilerPass::Compiling), vec!["-Xanalyzer", "-analyzer-output=text"]), (configures(CompilerPass::Compiling), vec!["-emit-llvm"]), (configures(CompilerPass::Compiling), vec!["-fprofile-instr-generate"]), (Source { binary: false }, vec!["main.c"]), ], ); } #[test] fn compilation_database_flag() { let sut = CompilerInterpreter::default(); let execution = create_execution( "clang", vec!["clang", "-MJ", "compile_commands.json", "main.c"], "/project", ); assert_command( sut.recognize(execution), vec![ (Compiler, vec!["clang"]), (configures(CompilerPass::Preprocessing), vec!["-MJ", "compile_commands.json"]), (Source { binary: false }, vec!["main.c"]), ], ); } /// Uses contains-checks instead of assert_command because environment /// variable processing order may not match HashMap iteration order. #[test] fn environment_variables_cpath() { let sut = CompilerInterpreter::default(); let cpath = create_path_string(&["/usr/include", "/opt/include"]); let mut env = HashMap::new(); env.insert("CPATH", cpath.as_str()); let execution = create_execution_with_env( "clang", vec!["clang", "-c", "main.c", "-o", "main.o"], "/project", env, ); let result = sut.recognize(execution); if let RecognizeResult::Recognized(cmd) = result { assert_eq!(cmd.arguments.len(), 6); let args: Vec = cmd.arguments.iter().flat_map(|a| a.as_arguments(&|p| Cow::Borrowed(p))).collect(); assert!(args.contains(&"-I".to_string())); assert!(args.contains(&"/usr/include".to_string())); assert!(args.contains(&"/opt/include".to_string())); } else { panic!("Expected compiler command"); } } /// Uses contains-checks instead of assert_command because environment /// variable processing order may not match HashMap iteration order. #[test] fn environment_variables_c_include_path() { let sut = CompilerInterpreter::default(); let mut env = HashMap::new(); env.insert("C_INCLUDE_PATH", "/usr/local/include"); let execution = create_execution_with_env( "clang", vec!["clang", "-c", "main.c", "-o", "main.o"], "/project", env, ); let result = sut.recognize(execution); if let RecognizeResult::Recognized(cmd) = result { assert_eq!(cmd.arguments.len(), 5); let args: Vec = cmd.arguments.iter().flat_map(|a| a.as_arguments(&|p| Cow::Borrowed(p))).collect(); assert!(args.contains(&"-isystem".to_string())); assert!(args.contains(&"/usr/local/include".to_string())); } else { panic!("Expected compiler command"); } } /// Uses contains-checks instead of assert_command because environment /// variable processing order may not match HashMap iteration order. #[test] fn environment_variables_cplus_include_path() { let sut = CompilerInterpreter::default(); let mut env = HashMap::new(); env.insert("CPLUS_INCLUDE_PATH", "/usr/include/c++/11"); let execution = create_execution_with_env( "clang++", vec!["clang++", "-c", "main.cpp", "-o", "main.o"], "/project", env, ); let result = sut.recognize(execution); if let RecognizeResult::Recognized(cmd) = result { assert_eq!(cmd.arguments.len(), 5); let args: Vec = cmd.arguments.iter().flat_map(|a| a.as_arguments(&|p| Cow::Borrowed(p))).collect(); assert!(args.contains(&"-isystem".to_string())); assert!(args.contains(&"/usr/include/c++/11".to_string())); } else { panic!("Expected compiler command"); } } /// Uses contains-checks instead of assert_command because environment /// variable processing order may not match HashMap iteration order. #[test] fn environment_variables_multiple() { let sut = CompilerInterpreter::default(); let cpath = create_path_string(&["/usr/include", "/opt/include"]); let mut env = HashMap::new(); env.insert("CPATH", cpath.as_str()); env.insert("C_INCLUDE_PATH", "/usr/local/include"); env.insert("CPLUS_INCLUDE_PATH", "/usr/include/c++/11"); let execution = create_execution_with_env( "clang", vec!["clang", "-c", "main.c", "-o", "main.o"], "/project", env, ); let result = sut.recognize(execution); if let RecognizeResult::Recognized(cmd) = result { assert_eq!(cmd.arguments.len(), 8); let args: Vec = cmd.arguments.iter().flat_map(|a| a.as_arguments(&|p| Cow::Borrowed(p))).collect(); assert!(args.contains(&"/usr/include".to_string())); assert!(args.contains(&"/opt/include".to_string())); assert!(args.contains(&"/usr/local/include".to_string())); assert!(args.contains(&"/usr/include/c++/11".to_string())); } else { panic!("Expected compiler command"); } } #[test] fn environment_variables_empty_paths() { let sut = CompilerInterpreter::default(); let c_include_path = create_path_string(&["", "", "", ""]); let mut env = HashMap::new(); env.insert("CPATH", ""); env.insert("C_INCLUDE_PATH", c_include_path.as_str()); let execution = create_execution_with_env( "clang", vec!["clang", "-c", "main.c", "-o", "main.o"], "/project", env, ); let result = sut.recognize(execution); if let RecognizeResult::Recognized(cmd) = result { assert_eq!(cmd.arguments.len(), 4); } else { panic!("Expected compiler command"); } } /// Uses contains-checks instead of assert_command because environment /// variable processing order may not match HashMap iteration order. #[test] fn environment_variables_objc_include_path() { let sut = CompilerInterpreter::default(); let objc_include_path = create_path_string(&["/System/Library/Frameworks", "/usr/local/objc"]); let mut env = HashMap::new(); env.insert("OBJC_INCLUDE_PATH", objc_include_path.as_str()); let execution = create_execution_with_env( "clang", vec!["clang", "-c", "main.m", "-o", "main.o"], "/project", env, ); let result = sut.recognize(execution); if let RecognizeResult::Recognized(cmd) = result { assert_eq!(cmd.arguments.len(), 6); let args: Vec = cmd.arguments.iter().flat_map(|a| a.as_arguments(&|p| Cow::Borrowed(p))).collect(); assert!(args.contains(&"-isystem".to_string())); assert!(args.contains(&"/System/Library/Frameworks".to_string())); assert!(args.contains(&"/usr/local/objc".to_string())); } else { panic!("Expected compiler command"); } } /// Uses contains-checks instead of assert_command because environment /// variable processing order may not match HashMap iteration order. #[test] fn environment_variables_all_types() { let sut = CompilerInterpreter::default(); let mut env = HashMap::new(); env.insert("CPATH", "/usr/include"); env.insert("C_INCLUDE_PATH", "/usr/local/include"); env.insert("CPLUS_INCLUDE_PATH", "/usr/include/c++/11"); env.insert("OBJC_INCLUDE_PATH", "/System/Library/Frameworks"); let execution = create_execution_with_env( "clang", vec!["clang", "-c", "main.c", "-o", "main.o"], "/project", env, ); let result = sut.recognize(execution); if let RecognizeResult::Recognized(cmd) = result { assert_eq!(cmd.arguments.len(), 8); let args: Vec = cmd.arguments.iter().flat_map(|a| a.as_arguments(&|p| Cow::Borrowed(p))).collect(); assert!(args.contains(&"/usr/include".to_string())); assert!(args.contains(&"/usr/local/include".to_string())); assert!(args.contains(&"/usr/include/c++/11".to_string())); assert!(args.contains(&"/System/Library/Frameworks".to_string())); assert!(args.contains(&"-I".to_string())); assert!(args.contains(&"-isystem".to_string())); } else { panic!("Expected compiler command"); } } #[test] fn cc1_invocation_ignored() { let sut = CompilerInterpreter::default(); // User-facing clang command should be recognized let user_execution = create_execution( "clang++", vec!["clang++", "-c", "-std=c++23", "-o", "hello-world", "hello-world.cpp"], "/home/user/project", ); if let RecognizeResult::Recognized(cmd) = sut.recognize(user_execution) { assert_eq!(cmd.arguments.len(), 5); assert_eq!(cmd.arguments[0].kind(), Compiler); } else { panic!("Expected compiler command for user-facing invocation"); } // Internal -cc1 clang command should be ignored let cc1_execution = create_execution( "clang++", vec![ "clang++", "-cc1", "-triple", "x86_64-pc-linux-gnu", "-emit-obj", "-dumpdir", "hello-world-", "-disable-free", "-clear-ast-before-backend", "-disable-llvm-verifier", "-discard-value-names", "-main-file-name", "hello-world.cpp", "-mrelocation-model", "pic", "-pic-level", "2", "-pic-is-pie", "-mframe-pointer=all", "-fmath-errno", "-ffp-contract=on", "-fno-rounding-math", "-mconstructor-aliases", "-funwind-tables=2", "-target-cpu", "x86-64", "-tune-cpu", "generic", "-debugger-tuning=gdb", "-fdebug-compilation-dir=/home/user/project", "-fcoverage-compilation-dir=/home/user/project", "-resource-dir", "/usr/lib/clang/20", "-std=c++23", "-fdeprecated-macro", "-ferror-limit", "19", "-stack-protector", "2", "-fgnuc-version=4.2.1", "-fno-implicit-modules", "-fskip-odr-check-in-gmf", "-fcxx-exceptions", "-fexceptions", "-fcolor-diagnostics", "-faddrsig", "-D__GCC_HAVE_DWARF2_CFI_ASM=1", "-x", "c++", "-o", "/tmp/hello-world-bd186e.o", "hello-world.cpp", ], "/home/user/project", ); assert_ignored(sut.recognize(cc1_execution), "internal invocation"); } // Requirements: recognition-cpp20-modules #[test] fn precompile_module_interface_does_not_source_the_output() { let sut = CompilerInterpreter::default(); let execution = create_execution( "clang++", vec!["clang++", "--precompile", "-std=c++20", "foo.cppm", "-o", "foo.pcm"], "/project", ); assert_command( sut.recognize(execution), vec![ (Compiler, vec!["clang++"]), (stops_at(CompilerPass::Compiling), vec!["--precompile"]), (configures(CompilerPass::Compiling), vec!["-std=c++20"]), (Source { binary: false }, vec!["foo.cppm"]), (Output, vec!["-o", "foo.pcm"]), ], ); } // Requirements: recognition-cpp20-modules #[test] fn module_file_flag_consumes_precompiled_module_without_sourcing_it() { let sut = CompilerInterpreter::default(); let execution = create_execution( "clang++", vec!["clang++", "-std=c++20", "-fmodule-file=foo=foo.pcm", "-c", "main.cpp"], "/project", ); assert_command( sut.recognize(execution), vec![ (Compiler, vec!["clang++"]), (configures(CompilerPass::Compiling), vec!["-std=c++20"]), (configures(CompilerPass::Compiling), vec!["-fmodule-file=foo=foo.pcm"]), (stops_at(CompilerPass::Compiling), vec!["-c"]), (Source { binary: false }, vec!["main.cpp"]), ], ); } } mod flang { use super::*; #[test] fn basic() { let sut = CompilerInterpreter::default(); let execution = create_execution( "flang", vec!["flang", "-fbackslash", "-ffree-form", "-J/path/to/modules", "-cpp", "main.f90"], "/project", ); if let RecognizeResult::Recognized(cmd) = sut.recognize(execution) { assert_eq!(cmd.arguments.len(), 6); assert_eq!(cmd.arguments[1].kind(), configures(CompilerPass::Compiling)); assert_eq!(cmd.arguments[2].kind(), configures(CompilerPass::Compiling)); assert_eq!(cmd.arguments[3].kind(), configures(CompilerPass::Compiling)); assert_eq!(cmd.arguments[4].kind(), configures(CompilerPass::Preprocessing)); } else { panic!("Expected compiler command for Flang"); } } } mod cuda { use super::*; #[test] fn basic_compilation() { let sut = CompilerInterpreter::default(); let execution = create_execution("nvcc", vec!["nvcc", "-c", "kernel.cu", "-o", "kernel.o"], "/test"); assert_command( sut.recognize(execution), vec![ (Compiler, vec!["nvcc"]), (stops_at(CompilerPass::Compiling), vec!["-c"]), (Source { binary: false }, vec!["kernel.cu"]), (Output, vec!["-o", "kernel.o"]), ], ); } #[test] fn gpu_architecture_flags() { let sut = CompilerInterpreter::default(); let execution = create_execution( "nvcc", vec![ "nvcc", "--gpu-architecture=sm_80", "-arch=sm_70", "--gpu-code=sm_80,compute_80", "-c", "kernel.cu", ], "/test", ); assert_command( sut.recognize(execution), vec![ (Compiler, vec!["nvcc"]), (configures(CompilerPass::Compiling), vec!["--gpu-architecture=sm_80"]), (configures(CompilerPass::Compiling), vec!["-arch=sm_70"]), (configures(CompilerPass::Compiling), vec!["--gpu-code=sm_80,compute_80"]), (stops_at(CompilerPass::Compiling), vec!["-c"]), (Source { binary: false }, vec!["kernel.cu"]), ], ); } #[test] fn device_compilation_modes() { let sut = CompilerInterpreter::default(); let execution = create_execution( "nvcc", vec!["nvcc", "--device-c", "--relocatable-device-code=true", "kernel.cu", "-o", "kernel.o"], "/test", ); assert_command( sut.recognize(execution), vec![ (Compiler, vec!["nvcc"]), (stops_at(CompilerPass::Compiling), vec!["--device-c"]), (configures(CompilerPass::Compiling), vec!["--relocatable-device-code=true"]), (Source { binary: false }, vec!["kernel.cu"]), (Output, vec!["-o", "kernel.o"]), ], ); } #[test] fn host_compiler_passthrough() { let sut = CompilerInterpreter::default(); let execution = create_execution( "nvcc", vec!["nvcc", "-Xcompiler", "-Wall", "-Xlinker", "-rpath=/usr/lib", "main.cu"], "/test", ); assert_command( sut.recognize(execution), vec![ (Compiler, vec!["nvcc"]), (configures(CompilerPass::Compiling), vec!["-Xcompiler"]), (none(), vec!["-Wall"]), (configures(CompilerPass::Linking), vec!["-Xlinker"]), (none(), vec!["-rpath=/usr/lib"]), (Source { binary: false }, vec!["main.cu"]), ], ); } #[test] fn debug_and_optimization() { let sut = CompilerInterpreter::default(); let execution = create_execution( "nvcc", vec!["nvcc", "-G", "--generate-line-info", "-O2", "--use_fast_math", "kernel.cu"], "/test", ); assert_command( sut.recognize(execution), vec![ (Compiler, vec!["nvcc"]), (configures(CompilerPass::Compiling), vec!["-G"]), (configures(CompilerPass::Compiling), vec!["--generate-line-info"]), (configures(CompilerPass::Compiling), vec!["-O2"]), (configures(CompilerPass::Compiling), vec!["--use_fast_math"]), (Source { binary: false }, vec!["kernel.cu"]), ], ); } #[test] fn flag_formats() { let sut = CompilerInterpreter::default(); for args in [ vec!["nvcc", "--gpu-architecture=sm_80", "-c", "kernel.cu"], vec!["nvcc", "--gpu-architecture", "sm_80", "-c", "kernel.cu"], ] { let execution = create_execution("nvcc", args, "/test"); let result = sut.recognize(execution); if let RecognizeResult::Recognized(cmd) = result { assert_eq!(cmd.arguments.len(), 4); assert_eq!(cmd.arguments[1].kind(), configures(CompilerPass::Compiling)); } } } #[test] fn specific_flags() { let sut = CompilerInterpreter::default(); let execution = create_execution("nvcc", vec!["nvcc", "--compile", "kernel.cu"], "/test"); assert_command( sut.recognize(execution), vec![ (Compiler, vec!["nvcc"]), (stops_at(CompilerPass::Compiling), vec!["--compile"]), (Source { binary: false }, vec!["kernel.cu"]), ], ); } } mod intel_fortran { use super::*; #[test] fn basic_compilation() { let sut = CompilerInterpreter::default(); let execution = create_execution("ifort", vec!["ifort", "-c", "test.f90"], "/project"); let result = sut.recognize(execution); assert!(matches!(result, RecognizeResult::Recognized(_))); if let RecognizeResult::Recognized(parsed) = result { assert_eq!(parsed.arguments.len(), 3); assert_eq!(parsed.arguments[1].kind(), stops_at(CompilerPass::Compiling)); } } #[test] fn preprocessing_flags() { let sut = CompilerInterpreter::default(); let execution = create_execution( "ifort", vec!["ifort", "-fpp", "-DDEBUG", "-I/usr/include", "test.f90"], "/project", ); let result = sut.recognize(execution); assert!(matches!(result, RecognizeResult::Recognized(_))); if let RecognizeResult::Recognized(parsed) = result { assert_eq!(parsed.arguments[1].kind(), configures(CompilerPass::Preprocessing)); assert_eq!(parsed.arguments[2].kind(), configures(CompilerPass::Preprocessing)); } } #[test] fn linking_flags() { let sut = CompilerInterpreter::default(); let execution = create_execution("ifort", vec!["ifort", "-shared-intel", "-lm", "test.o"], "/project"); let result = sut.recognize(execution); assert!(matches!(result, RecognizeResult::Recognized(_))); if let RecognizeResult::Recognized(parsed) = result { assert_eq!(parsed.arguments[1].kind(), configures(CompilerPass::Linking)); assert_eq!(parsed.arguments[2].kind(), configures(CompilerPass::Linking)); } } #[test] fn info_flags() { let sut = CompilerInterpreter::default(); let execution = create_execution("ifort", vec!["ifort", "--version"], "/project"); let result = sut.recognize(execution); assert!(matches!(result, RecognizeResult::Recognized(_))); if let RecognizeResult::Recognized(parsed) = result { assert_eq!(parsed.arguments[1].kind(), info()); } } } mod cray_fortran { use super::*; #[test] fn basic_compilation() { let sut = CompilerInterpreter::default(); let execution = create_execution("crayftn", vec!["crayftn", "-c", "test.f90"], "/project"); let result = sut.recognize(execution); assert!(matches!(result, RecognizeResult::Recognized(_))); if let RecognizeResult::Recognized(parsed) = result { assert_eq!(parsed.arguments.len(), 3); assert_eq!(parsed.arguments[1].kind(), stops_at(CompilerPass::Compiling)); } } #[test] fn preprocessing_flags() { let sut = CompilerInterpreter::default(); let execution = create_execution( "crayftn", vec!["crayftn", "-DDEBUG", "-I/usr/include", "test.f90"], "/project", ); let result = sut.recognize(execution); assert!(matches!(result, RecognizeResult::Recognized(_))); if let RecognizeResult::Recognized(parsed) = result { assert_eq!(parsed.arguments[1].kind(), configures(CompilerPass::Preprocessing)); assert_eq!(parsed.arguments[2].kind(), configures(CompilerPass::Preprocessing)); } } #[test] fn linking_flags() { let sut = CompilerInterpreter::default(); let execution = create_execution("crayftn", vec!["crayftn", "-add-rpath", "-lm", "test.o"], "/project"); let result = sut.recognize(execution); assert!(matches!(result, RecognizeResult::Recognized(_))); if let RecognizeResult::Recognized(parsed) = result { assert_eq!(parsed.arguments[1].kind(), configures(CompilerPass::Linking)); assert_eq!(parsed.arguments[2].kind(), configures(CompilerPass::Linking)); } } #[test] fn cray_specific_flags() { let sut = CompilerInterpreter::default(); let execution = create_execution( "crayftn", vec!["crayftn", "-craylibs", "-target-cpu=x86_64", "test.f90"], "/project", ); let result = sut.recognize(execution); assert!(matches!(result, RecognizeResult::Recognized(_))); if let RecognizeResult::Recognized(parsed) = result { assert_eq!(parsed.arguments.len(), 4); assert_eq!(parsed.arguments[1].kind(), none()); assert_eq!(parsed.arguments[2].kind(), none()); } } #[test] fn openmp_flags() { let sut = CompilerInterpreter::default(); let execution = create_execution("crayftn", vec!["crayftn", "-openmp", "test.f90"], "/project"); let result = sut.recognize(execution); assert!(matches!(result, RecognizeResult::Recognized(_))); if let RecognizeResult::Recognized(parsed) = result { assert_eq!(parsed.arguments.len(), 3); assert_eq!(parsed.arguments[1].kind(), none()); } } } mod mpi { use super::*; // Requirements: recognition-compiler-names #[test] fn basic_compilation() { let sut = CompilerInterpreter::default(); let execution = create_execution("mpicc", vec!["mpicc", "-c", "hello.c"], "/project"); let result = sut.recognize(execution); assert!(matches!(result, RecognizeResult::Recognized(_))); if let RecognizeResult::Recognized(parsed) = result { assert_eq!(parsed.arguments.len(), 3); assert_eq!(parsed.arguments[1].kind(), stops_at(CompilerPass::Compiling)); assert_eq!(parsed.arguments[2].kind(), Source { binary: false }); } } /// Watch-out from the requirement: the glued form `-cc=gcc` must stay /// a single token, and must not swallow the source file that follows. // Requirements: recognition-compiler-names #[test] fn compiler_override_glued_form_keeps_single_token_and_source() { let sut = CompilerInterpreter::default(); let execution = create_execution("mpicc", vec!["mpicc", "-cc=gcc", "-c", "hello.c"], "/project"); let result = sut.recognize(execution); assert!(matches!(result, RecognizeResult::Recognized(_))); if let RecognizeResult::Recognized(parsed) = result { assert_eq!(parsed.arguments.len(), 4); assert_eq!(parsed.arguments[1].kind(), driver()); assert_eq!( parsed.arguments[1].as_arguments(&|p| Cow::Borrowed(p)), vec!["-cc=gcc".to_string()], "-cc=gcc must be retained as a single token" ); assert_eq!(parsed.arguments[2].kind(), stops_at(CompilerPass::Compiling)); assert_eq!(parsed.arguments[3].kind(), Source { binary: false }); assert_eq!( parsed.arguments[3].as_arguments(&|p| Cow::Borrowed(p)), vec!["hello.c".to_string()] ); } } /// The separate-token spelling (`-cc gcc`) must consume the value too, /// or "gcc" would be misread as a phantom source file. // Requirements: recognition-compiler-names #[test] fn compiler_override_separate_form_consumes_value() { let sut = CompilerInterpreter::default(); let execution = create_execution("mpicc", vec!["mpicc", "-cc", "gcc", "-c", "hello.c"], "/project"); let result = sut.recognize(execution); assert!(matches!(result, RecognizeResult::Recognized(_))); if let RecognizeResult::Recognized(parsed) = result { let source_count = parsed.arguments.iter().filter(|a| matches!(a.kind(), Source { .. })).count(); assert_eq!(source_count, 1, "only hello.c must be a source, got {:?}", parsed.arguments); assert_eq!(parsed.arguments[1].kind(), driver()); assert_eq!( parsed.arguments[1].as_arguments(&|p| Cow::Borrowed(p)), vec!["-cc".to_string(), "gcc".to_string()] ); } } /// Wrapper-info invocations classify as info-and-exit, which the /// output converter uses to skip emitting a database entry. // Requirements: recognition-compiler-names #[test] fn wrapper_info_flags_are_info_and_exit() { let sut = CompilerInterpreter::default(); for args in [vec!["mpicc", "-showme"], vec!["mpicc", "-show"], vec!["mpicc", "-compile_info"]] { let execution = create_execution("mpicc", args.clone(), "/project"); let result = sut.recognize(execution); assert!(matches!(result, RecognizeResult::Recognized(_)), "args: {:?}", args); if let RecognizeResult::Recognized(parsed) = result { assert_eq!(parsed.arguments[1].kind(), info(), "args: {:?}", args); } } } } mod qnx { use super::*; // Requirements: recognition-compiler-names #[test] fn basic_compilation() { let sut = CompilerInterpreter::default(); let execution = create_execution("qcc", vec!["qcc", "-c", "hello.c"], "/project"); let result = sut.recognize(execution); assert!(matches!(result, RecognizeResult::Recognized(_))); if let RecognizeResult::Recognized(parsed) = result { assert_eq!(parsed.arguments.len(), 3); assert_eq!(parsed.arguments[1].kind(), stops_at(CompilerPass::Compiling)); assert_eq!(parsed.arguments[2].kind(), Source { binary: false }); } } /// QNX's variant selector is attached-value only (`-Vgcc_ntoaarch64le`); /// a bare `-V` (no attached value) lists variants. Both spellings are /// modeled as a prefix pattern with 0 extra args, so the token is never /// split and never swallows a following source file. // Requirements: recognition-compiler-names #[test] fn variant_selector_is_retained_and_does_not_swallow_source() { let sut = CompilerInterpreter::default(); let execution = create_execution("qcc", vec!["qcc", "-Vgcc_ntoaarch64le", "-c", "hello.c"], "/project"); let result = sut.recognize(execution); assert!(matches!(result, RecognizeResult::Recognized(_))); if let RecognizeResult::Recognized(parsed) = result { assert_eq!(parsed.arguments.len(), 4); assert_eq!(parsed.arguments[1].kind(), driver()); assert_eq!( parsed.arguments[1].as_arguments(&|p| Cow::Borrowed(p)), vec!["-Vgcc_ntoaarch64le".to_string()], "-Vgcc_ntoaarch64le must be retained as a single token" ); assert_eq!(parsed.arguments[2].kind(), stops_at(CompilerPass::Compiling)); assert_eq!(parsed.arguments[3].kind(), Source { binary: false }); assert_eq!( parsed.arguments[3].as_arguments(&|p| Cow::Borrowed(p)), vec!["hello.c".to_string()] ); } } /// A bare `-V` (no attached value) must also be treated as a driver /// option, never as a source file. // Requirements: recognition-compiler-names #[test] fn bare_variant_listing_flag_is_a_driver_option() { let sut = CompilerInterpreter::default(); let execution = create_execution("qcc", vec!["qcc", "-V", "-c", "hello.c"], "/project"); let result = sut.recognize(execution); assert!(matches!(result, RecognizeResult::Recognized(_))); if let RecognizeResult::Recognized(parsed) = result { assert_eq!(parsed.arguments[1].kind(), driver()); let source_count = parsed.arguments.iter().filter(|a| matches!(a.kind(), Source { .. })).count(); assert_eq!(source_count, 1, "only hello.c must be a source, got {:?}", parsed.arguments); } } } mod nasm { use super::*; /// The separate-token form of `-f` (output format) must consume its /// value so it is never mis-classified as a second source file, and /// the `-o` output pair must not swallow the assembly source that /// precedes it. // Requirements: recognition-compiler-names #[test] fn separate_format_value_is_consumed_not_classified_as_source() { let sut = CompilerInterpreter::default(); let execution = create_execution( "nasm", vec!["nasm", "-f", "elf64", "hello.asm", "-o", "hello.o"], "/project", ); let result = sut.recognize(execution); assert_command( result, vec![ (ArgumentKind::Compiler, vec!["nasm"]), (configures(CompilerPass::Compiling), vec!["-f", "elf64"]), (Source { binary: false }, vec!["hello.asm"]), (ArgumentKind::Output, vec!["-o", "hello.o"]), ], ); } /// NASM's canonical lowercase `-d` define must consume its value even /// when that value ends in a source extension (`-d NAME=release.asm` /// parameterizes a %include); leaking it into source detection would /// fabricate a second compilation entry. // Requirements: recognition-compiler-names #[test] fn lowercase_define_value_with_source_extension_is_not_a_source() { let sut = CompilerInterpreter::default(); let execution = create_execution( "nasm", vec!["nasm", "-d", "CONFIG=release.asm", "-f", "elf64", "hello.asm"], "/project", ); let result = sut.recognize(execution); assert_command( result, vec![ (ArgumentKind::Compiler, vec!["nasm"]), (configures(CompilerPass::Preprocessing), vec!["-d", "CONFIG=release.asm"]), (configures(CompilerPass::Compiling), vec!["-f", "elf64"]), (Source { binary: false }, vec!["hello.asm"]), ], ); } /// The glued form (`-felf64`) must be recognized the same way as the /// separate-token form. // Requirements: recognition-compiler-names #[test] fn glued_format_value_is_consumed_not_classified_as_source() { let sut = CompilerInterpreter::default(); let execution = create_execution("nasm", vec!["nasm", "-felf64", "hello.asm"], "/project"); let result = sut.recognize(execution); assert_command( result, vec![ (ArgumentKind::Compiler, vec!["nasm"]), (configures(CompilerPass::Compiling), vec!["-felf64"]), (Source { binary: false }, vec!["hello.asm"]), ], ); } /// `nasm -v` prints version info and exits; there is no source /// argument, so no compilation entry can be synthesized from it. // Requirements: recognition-compiler-names #[test] fn version_flag_has_no_source_argument() { let sut = CompilerInterpreter::default(); let execution = create_execution("nasm", vec!["nasm", "-v"], "/project"); let result = sut.recognize(execution); assert!(matches!(result, RecognizeResult::Recognized(_))); if let RecognizeResult::Recognized(parsed) = result { assert_eq!(parsed.arguments[1].kind(), info()); assert!( !parsed.arguments.iter().any(|a| matches!(a.kind(), Source { .. })), "got {:?}", parsed.arguments ); } } /// YASM's long `--version` option is covered by the `--*` catch-all /// (no separate-token value ever follows a YASM long option), and /// still has no source argument to build an entry from. // Requirements: recognition-compiler-names #[test] fn yasm_long_version_option_has_no_source_argument() { let sut = CompilerInterpreter::default(); let execution = create_execution("yasm", vec!["yasm", "--version"], "/project"); let result = sut.recognize(execution); assert!(matches!(result, RecognizeResult::Recognized(_))); if let RecognizeResult::Recognized(parsed) = result { assert!( !parsed.arguments.iter().any(|a| matches!(a.kind(), Source { .. })), "got {:?}", parsed.arguments ); } } } mod fasm { use super::*; /// `-m`'s memory-limit value is always a separate token; it must be /// consumed so it is never mistaken for a source file, and the /// trailing output positional (`hello.o`, not a recognized source /// extension) must not turn into a second compilation entry. // Requirements: recognition-compiler-names #[test] fn memory_limit_value_is_consumed_and_output_positional_is_not_a_source() { let sut = CompilerInterpreter::default(); let execution = create_execution("fasm", vec!["fasm", "-m", "65536", "hello.asm", "hello.o"], "/project"); let result = sut.recognize(execution); assert!(matches!(result, RecognizeResult::Recognized(_))); if let RecognizeResult::Recognized(parsed) = result { assert_eq!(parsed.arguments.len(), 4); assert_eq!(parsed.arguments[1].kind(), none()); assert_eq!( parsed.arguments[1].as_arguments(&|p| Cow::Borrowed(p)), vec!["-m".to_string(), "65536".to_string()] ); assert_eq!(parsed.arguments[2].kind(), Source { binary: false }); assert_eq!(parsed.arguments[3].kind(), Source { binary: true }, "got {:?}", parsed.arguments); } } } mod swift { use super::*; // Requirements: recognition-compiler-names #[test] fn basic_compilation() { let sut = CompilerInterpreter::default(); let execution = create_execution("swiftc", vec!["swiftc", "-c", "hello.swift"], "/project"); let result = sut.recognize(execution); assert!(matches!(result, RecognizeResult::Recognized(_))); if let RecognizeResult::Recognized(parsed) = result { assert_eq!(parsed.arguments.len(), 3); assert_eq!(parsed.arguments[1].kind(), stops_at(CompilerPass::Compiling)); assert_eq!(parsed.arguments[2].kind(), Source { binary: false }); } } /// The motivating whole-module shape: several `.swift` sources in one /// invocation. Recognition/parsing must classify both as compilable /// sources and consume `-module-name`'s separate-token value without /// treating it (or "App") as a source; the converter (tested /// separately) is what fans this into one entry per source, each /// keeping every token. // Requirements: recognition-compiler-names #[test] fn whole_module_invocation_keeps_all_sources_and_flags() { let sut = CompilerInterpreter::default(); let execution = create_execution( "swiftc", vec!["swiftc", "-module-name", "App", "-emit-object", "a.swift", "b.swift"], "/project", ); assert_command( sut.recognize(execution), vec![ (Compiler, vec!["swiftc"]), (configures(CompilerPass::Compiling), vec!["-module-name", "App"]), (stops_at(CompilerPass::Compiling), vec!["-emit-object"]), (Source { binary: false }, vec!["a.swift"]), (Source { binary: false }, vec!["b.swift"]), ], ); } /// swiftc spawns per-file `swift-frontend` jobs the way gcc spawns /// `cc1`; the internal executable name is filtered via `ignore_when`. // Requirements: recognition-compiler-names #[test] fn swift_frontend_executable_is_ignored() { let sut = CompilerInterpreter::default(); let execution = create_execution( "swift-frontend", vec!["swift-frontend", "-frontend", "-c", "a.swift"], "/project", ); assert_ignored(sut.recognize(execution), "internal executable"); } /// A legacy toolchain re-invoking itself as `swiftc -frontend` must /// also be filtered, via the `ignore_when.flags` list. // Requirements: recognition-compiler-names #[test] fn frontend_flag_execution_is_ignored() { let sut = CompilerInterpreter::default(); let execution = create_execution("swiftc", vec!["swiftc", "-frontend", "-c", "a.swift"], "/project"); assert_ignored(sut.recognize(execution), "internal invocation"); } /// `swiftc --version`/`-version` print info and exit; there is no /// source argument, so no compilation entry can be synthesized. // Requirements: recognition-compiler-names #[test] fn version_flags_are_info_and_exit() { let sut = CompilerInterpreter::default(); for args in [vec!["swiftc", "--version"], vec!["swiftc", "-version"]] { let execution = create_execution("swiftc", args.clone(), "/project"); let result = sut.recognize(execution); assert!(matches!(result, RecognizeResult::Recognized(_)), "args: {:?}", args); if let RecognizeResult::Recognized(parsed) = result { assert_eq!(parsed.arguments[1].kind(), info(), "args: {:?}", args); assert!( !parsed.arguments.iter().any(|a| matches!(a.kind(), Source { .. })), "got {:?}", parsed.arguments ); } } } /// `-Xcc`/`-Xlinker`/`-Xfrontend` each forward exactly one separate /// token to a downstream tool; that token often starts with '-' /// (`-Xlinker -rpath`), so `count: 1` must consume it unconditionally /// or it would leak in as a phantom source. // Requirements: recognition-compiler-names #[test] fn forwarded_flags_consume_dash_prefixed_value_not_a_source() { let sut = CompilerInterpreter::default(); let execution = create_execution( "swiftc", vec![ "swiftc", "-Xlinker", "-rpath", "-Xcc", "-DFOO", "-Xfrontend", "-enable-cross-import-overlays", "a.swift", ], "/project", ); let result = sut.recognize(execution); assert!(matches!(result, RecognizeResult::Recognized(_))); if let RecognizeResult::Recognized(parsed) = result { let source_count = parsed.arguments.iter().filter(|a| matches!(a.kind(), Source { .. })).count(); assert_eq!(source_count, 1, "only a.swift must be a source, got {:?}", parsed.arguments); assert_eq!(parsed.arguments[1].kind(), configures(CompilerPass::Linking)); assert_eq!( parsed.arguments[1].as_arguments(&|p| Cow::Borrowed(p)), vec!["-Xlinker".to_string(), "-rpath".to_string()] ); assert_eq!(parsed.arguments[2].kind(), configures(CompilerPass::Compiling)); assert_eq!( parsed.arguments[2].as_arguments(&|p| Cow::Borrowed(p)), vec!["-Xcc".to_string(), "-DFOO".to_string()] ); assert_eq!(parsed.arguments[3].kind(), configures(CompilerPass::Compiling)); assert_eq!( parsed.arguments[3].as_arguments(&|p| Cow::Borrowed(p)), vec!["-Xfrontend".to_string(), "-enable-cross-import-overlays".to_string()] ); } } /// `-D`/`-I` are JoinedOrSeparate in swiftc (unlike the Separate-only /// `-target`/`-module-name`); both the glued and separate spellings /// must be recognized so their value never leaks in as a source. // Requirements: recognition-compiler-names #[test] fn define_and_import_path_accept_glued_or_separate_value() { let sut = CompilerInterpreter::default(); let execution = create_execution( "swiftc", vec!["swiftc", "-DDEBUG", "-I", "/usr/local/include", "a.swift"], "/project", ); assert_command( sut.recognize(execution), vec![ (Compiler, vec!["swiftc"]), (configures(CompilerPass::Preprocessing), vec!["-DDEBUG"]), (configures(CompilerPass::Preprocessing), vec!["-I", "/usr/local/include"]), (Source { binary: false }, vec!["a.swift"]), ], ); } /// `-import-objc-header`'s value is a `.h` path -- and `.h` IS a /// recognized source extension for other families (headers are /// translation units elsewhere), so an unmatched (0-arg) rule here /// would leak the bridging header in as a phantom compilable /// source, corrupting entry count. `count: 1` must consume it. // Requirements: recognition-compiler-names #[test] fn bridging_header_value_is_consumed_not_a_phantom_source() { let sut = CompilerInterpreter::default(); let execution = create_execution( "swiftc", vec!["swiftc", "-import-objc-header", "Bridging.h", "-module-name", "App", "a.swift"], "/project", ); let result = sut.recognize(execution); assert!(matches!(result, RecognizeResult::Recognized(_))); if let RecognizeResult::Recognized(parsed) = result { let source_count = parsed.arguments.iter().filter(|a| matches!(a.kind(), Source { .. })).count(); assert_eq!(source_count, 1, "only a.swift must be a source, got {:?}", parsed.arguments); assert_eq!(parsed.arguments[1].kind(), configures(CompilerPass::Compiling)); assert_eq!( parsed.arguments[1].as_arguments(&|p| Cow::Borrowed(p)), vec!["-import-objc-header".to_string(), "Bridging.h".to_string()] ); } } } } rizsotto-Bear-0c30b27/crates/semantic/src/interpreters/compilers/print.rs000066400000000000000000000232621527177720300267200ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Renders the compilers Bear recognizes as a human-readable list, for //! `bear semantic --print-compilers`. //! //! Pure formatting over the generated [`RECOGNITION_PATTERNS`] table: no //! I/O, no config lookup. Rows whose `description` is `None` are internal //! helper executables (e.g. `cc1`, `swift-frontend`) that exist only so the //! recognizer can route them to the right interpreter for ignoring; they //! are not user-facing compilers and are skipped here. Compiler-launcher //! (wrapper) rows arrive in the same table as every compiler row, with the //! literal type column `"wrapper"` and a blank displayed alias (see //! `build_rows`). use super::compiler_recognition::RECOGNITION_PATTERNS; /// Maximum output line width in columns. The executables column wraps on /// `, ` boundaries to keep every line within this budget. const WIDTH: usize = 100; /// One renderable row: a human description, the config `as:` alias, and /// the executable basenames that recognize it. struct Row { description: &'static str, alias: String, executables: &'static [&'static str], } impl Row { /// The literal `as ` label rendered in the second column. A /// blank alias (wrapper rows, which carry no identity) renders as an /// empty string -- no `"as "` prefix at all. fn alias_column(&self) -> String { if self.alias.is_empty() { String::new() } else { format!("as {}", self.alias) } } } /// Renders the entry lines only -- no header, no version banner, no /// trailing caveat; the caller (the CLI dispatcher) adds those. Ends with /// a trailing newline. pub fn print_compilers() -> String { let rows = build_rows(); let col1_width = rows.iter().map(|r| r.description.len()).max().unwrap_or(0) + 2; let col2_width = rows.iter().map(|r| r.alias_column().len()).max().unwrap_or(0) + 2; let mut out = String::new(); for row in &rows { render_row(row, col1_width, col2_width, &mut out); } out } /// Builds the row list from [`RECOGNITION_PATTERNS`], preserving its /// order (compiler rows first, then wrapper rows -- generation appends /// them in that order) and skipping internal-only (`description: None`) /// rows. The alias column shows the id verbatim, no cosmetic rewriting. /// Wrapper rows (`type_str == "wrapper"`) render with a blank alias: /// they carry no identity and no config `as:` spelling of their own. fn build_rows() -> Vec { RECOGNITION_PATTERNS .iter() .filter_map(|&(type_str, executables, _cross_compilation, _versioned, description)| { description.map(|desc| { let alias = if type_str == "wrapper" { String::new() } else { type_str.to_string() }; Row { description: desc, alias, executables } }) }) .collect() } /// Appends one row's rendered lines to `out`: the first line carries the /// description and alias columns, continuation lines (when the /// executables list wraps) are blank in those columns, indented with /// spaces to where the executables column begins. fn render_row(row: &Row, col1_width: usize, col2_width: usize, out: &mut String) { let indent = col1_width + col2_width; let avail = WIDTH.saturating_sub(indent); let wrapped = wrap_executables(row.executables, avail); for (i, line) in wrapped.iter().enumerate() { if i == 0 { out.push_str(&format!( "{:col1_width$}{:col2_width$}{}\n", row.description, row.alias_column(), line )); } else { out.push_str(&format!("{:indent$}{}\n", "", line)); } } } /// Greedily wraps `executables` (joined with `, `) into lines no longer /// than `avail` columns, breaking only on `, ` boundaries. Mirrors plain /// text wrapping: a line always holds at least one executable, even if /// that single name alone exceeds `avail`. fn wrap_executables(executables: &[&str], avail: usize) -> Vec { let mut lines = Vec::new(); let mut current = String::new(); for (i, exe) in executables.iter().enumerate() { let is_last = i + 1 == executables.len(); let piece = if is_last { (*exe).to_string() } else { format!("{}, ", exe) }; let tentative = format!("{current}{piece}"); if !current.is_empty() && tentative.trim_end().len() > avail { lines.push(current.trim_end().to_string()); current = piece; } else { current = tentative; } } if !current.is_empty() { lines.push(current.trim_end().to_string()); } lines } #[cfg(test)] mod tests { use super::super::identity::CompilerType; use super::*; #[test] fn lists_a_known_compiler_with_its_alias_and_executables() { let sut = print_compilers(); assert!(sut.contains("GCC"), "missing GCC description:\n{sut}"); assert!(sut.contains("as gcc"), "missing gcc alias:\n{sut}"); assert!(sut.contains("gcc, g++"), "missing gcc executables:\n{sut}"); } #[test] fn lists_the_launcher_rows_last_with_no_alias_text() { let sut = print_compilers(); assert!(sut.contains("Compiler cache"), "missing compiler-cache description:\n{sut}"); assert!(sut.contains("Distributed compiler"), "missing distributed-compiler description:\n{sut}"); assert!(sut.contains("ccache"), "missing launcher executable:\n{sut}"); // Still "last" as a group: every wrapper description's byte offset // comes after a known compiler description's. let gcc_offset = sut.find("GCC").expect("GCC row present"); let cache_offset = sut.find("Compiler cache").expect("Compiler cache row present"); let distributed_offset = sut.find("Distributed compiler").expect("Distributed compiler row present"); assert!(cache_offset > gcc_offset, "wrapper rows must come after compiler rows:\n{sut}"); assert!(distributed_offset > gcc_offset, "wrapper rows must come after compiler rows:\n{sut}"); // No "as " text anywhere on a wrapper row: check line-by-line so // column-alignment padding elsewhere in the table cannot false-positive. for line in sut.lines().filter(|line| line.contains("ccache") || line.contains("distcc")) { assert!(!line.contains("as "), "wrapper row must not render an alias column: {line:?}"); } } #[test] fn omits_internal_helper_executables_and_ambiguous_bare_names() { let sut = print_compilers(); // Internal-only rows (description: None) must not surface. assert!(!sut.contains("swift-frontend"), "internal swift helper leaked:\n{sut}"); assert!(!sut.contains("cc1"), "internal gcc helper leaked:\n{sut}"); // The probed ambiguous basenames (cc, c++, CC) have no regex entry // at all, so they can never appear as a standalone executable-list // entry; a bare "cc" would only ever show up as part of another // token (e.g. "gcc"), never as its own comma-delimited entry. assert!(!sut.contains(" cc,"), "bare 'cc' must not be listed:\n{sut}"); assert!(!sut.contains(" c++,"), "bare 'c++' must not be listed:\n{sut}"); } #[test] fn every_line_fits_within_the_configured_width() { let sut = print_compilers(); for line in sut.lines() { assert!(line.len() <= WIDTH, "line exceeds {WIDTH} columns ({} chars): {line:?}", line.len()); } } /// Round-trip guard: every alias this renderer displays (`as `) /// must be a value the config `as:` field actually accepts. If a new /// compiler family's `type:` string in YAML and its `CompilerType` /// spelling ever drift apart, this test -- not a confused user /// copy-pasting a broken config -- catches it. #[test] fn every_displayed_alias_deserializes_as_a_compiler_type() { let aliases: Vec = RECOGNITION_PATTERNS .iter() .filter_map(|&(type_str, _, _, _, description)| { description.map(|_| if type_str == "wrapper" { String::new() } else { type_str.to_string() }) }) .filter(|alias| !alias.is_empty()) .collect(); for alias in aliases { let sut = alias.parse::(); assert!(sut.is_ok(), "alias '{alias}' does not resolve to a CompilerType: {sut:?}"); } } /// Round-trip guard for the wrapper side: `--print-compilers` shows no /// alias text for wrapper rows (see `alias_column`), so the previous /// test's filter skips them -- but the actual launcher basenames /// (`ccache`, `distcc`, ...) must still resolve to /// `CompilerType::Wrapper` through the generated `WRAPPER_AS_NAMES` the /// spelling resolution consults (config `as:` aliases were dropped; /// these are the launcher tool names, not aliases of a shared id). This /// proves that generated list still covers every launcher name /// `RECOGNITION_PATTERNS` actually recognizes. #[test] fn every_wrapper_executable_deserializes_as_compiler_type_wrapper() { let executables: Vec<&str> = RECOGNITION_PATTERNS .iter() .filter(|&&(type_str, _, _, _, _)| type_str == "wrapper") .flat_map(|&(_, executables, _, _, _)| executables.iter().copied()) .collect(); assert!(!executables.is_empty(), "expected at least one wrapper row in RECOGNITION_PATTERNS"); for name in executables { let sut = name.parse::(); assert_eq!( sut.ok(), Some(CompilerType::Wrapper), "wrapper executable '{name}' does not resolve to CompilerType::Wrapper" ); } } } rizsotto-Bear-0c30b27/crates/semantic/src/interpreters/compilers/probe.rs000066400000000000000000000624451527177720300267010ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Lazy `--version` probe used to disambiguate compiler executables whose //! basename does not uniquely identify the underlying toolchain (notably //! `cc` and `c++`, which are GCC on most Linuxes and Clang on the BSDs and //! macOS). //! //! The trait is cross-platform; the real implementation [`VersionProbe`] //! is Unix-only because (a) the BSD/macOS `cc`/`c++` ambiguity is the //! reason the probe exists in the first place, and (b) the watchdog needs //! `setsid` + `killpg` to take down a misbehaving subprocess tree, which //! has no portable equivalent on Windows. On non-Unix targets the //! recognizer wires up [`NoProbe`] instead and relies on the regex layer, //! which already classifies Windows toolchain names (`cl.exe`, //! `clang-cl`, `gcc.exe`) unambiguously. //! //! Safety properties of the Unix probe: //! - stdin is closed (`Stdio::null`) so probed binaries that read input //! (e.g. `bash` if it lands in `PATH`) cannot deadlock the call. //! - stdout/stderr are captured with no size cap from the OS but with a //! bounded timeout, so a chatty binary cannot stall recognition. //! - `LD_PRELOAD` and `DYLD_INSERT_LIBRARIES` are stripped from the probe //! environment so the probe is not itself intercepted by Bear. //! - A watchdog thread `SIGKILL`s the child's process group if //! `--version` does not return within the configured timeout. //! - A spawn that fails with `ETXTBSY` (the probed file transiently held //! open for writing across another fork+exec) is retried briefly //! before the probe declines. use super::identity::CompilerType; use std::path::Path; #[cfg(unix)] use std::cell::RefCell; #[cfg(unix)] use std::collections::HashMap; #[cfg(unix)] use std::path::PathBuf; /// `Send` (and not `Sync`) for the same reason as [`crate::Interpreter`]: /// the probe lives inside the interpreter that is moved into the single /// consumer thread, and is never shared across threads. pub trait CompilerProbe: Send { /// Classify `executable_path` by running `--version` on it. /// Returns `None` if the binary does not produce a recognizable signature /// (timeout, spawn failure, non-zero exit, garbage output, etc.). fn probe(&self, executable_path: &Path) -> Option; } /// Decorator that memoizes a probe's verdict per executable path. Used to /// wrap [`VersionProbe`] so a single recognizer never fork-execs the same /// compiler twice. Keyed by the path it receives -- the recognizer /// canonicalizes before calling, so the same compiler under different /// argv spellings collapses to one cache entry. /// /// Cache hits return both `Some(ty)` (a successful classification) and /// `None` (an inconclusive probe), so a binary that doesn't understand /// `--version` is probed once, not on every recognize() call. /// /// The cache is a `RefCell` because `probe()` takes `&self` (it is called /// through shared references all the way down from `Box`) /// but a miss must write. Recognition runs on the single consumer thread /// (see [`CompilerProbe`]'s `Send`-only bound), so no locking is needed; /// the borrow is released before the inner-probe call, which forks and /// can block for the probe timeout. /// /// Gated to Unix because the only probe worth caching is `VersionProbe`, /// which is itself Unix-only. On Windows `default_probe` returns /// `NoProbe` directly -- caching a constant `None` would be pure /// overhead. #[cfg(unix)] pub(crate) struct CachingProbe { inner: P, cache: RefCell>>, } #[cfg(unix)] impl CachingProbe

{ pub(crate) fn new(inner: P) -> Self { Self { inner, cache: RefCell::new(HashMap::new()) } } } #[cfg(unix)] impl CompilerProbe for CachingProbe

{ fn probe(&self, executable_path: &Path) -> Option { if let Some(&hit) = self.cache.borrow().get(executable_path) { return hit; } let result = self.inner.probe(executable_path); self.cache.borrow_mut().insert(executable_path.to_path_buf(), result); result } } /// Probe that always declines to classify. Used in two places: /// - on non-Unix targets, where `VersionProbe` does not exist and the /// recognizer falls back to its regex layer for every name; /// - in unit tests of `CompilerRecognizer` that want to exercise the /// regex/hint layer deterministically without depending on whatever /// `cc`/`c++` resolve to on the host. #[cfg_attr(all(unix, not(test)), allow(dead_code, reason = "on a Unix lib build neither use applies"))] pub(crate) struct NoProbe; impl CompilerProbe for NoProbe { fn probe(&self, _: &Path) -> Option { None } } #[cfg(unix)] pub(crate) use unix::VersionProbe; /// The default probe for the current target. On Unix wraps a real /// `VersionProbe` in a [`CachingProbe`] so each unique compiler path is /// only probed once per process. On Windows where the BSD/macOS `cc`/`c++` /// ambiguity does not exist (compiler basenames are uniquely classified /// by the regex layer) returns a `NoProbe`; caching a no-op would just be /// overhead. pub(crate) fn default_probe() -> Box { #[cfg(unix)] { Box::new(CachingProbe::new(VersionProbe::new())) } #[cfg(not(unix))] { Box::new(NoProbe) } } #[cfg(all(unix, test))] mod caching_tests { //! Requirements: recognition-ambiguous-name-probe use super::*; use std::cell::Cell; /// Probe that returns canned answers and counts how many times it ran. /// Used to verify that [`CachingProbe`] collapses repeated lookups for /// the same path into a single inner call. struct CountingProbe { answers: HashMap, calls: Cell, } impl CountingProbe { fn empty() -> Self { Self { answers: HashMap::new(), calls: Cell::new(0) } } fn with_answer(p: &str, t: CompilerType) -> Self { let mut answers = HashMap::new(); answers.insert(PathBuf::from(p), t); Self { answers, calls: Cell::new(0) } } fn calls(&self) -> usize { self.calls.get() } } impl CompilerProbe for CountingProbe { fn probe(&self, p: &Path) -> Option { self.calls.set(self.calls.get() + 1); self.answers.get(p).copied() } } #[test] fn caches_successful_classification_per_path() { let inner = CountingProbe::with_answer("cc", CompilerType::compiler("clang")); let cached = CachingProbe::new(inner); for _ in 0..100 { assert_eq!(cached.probe(Path::new("cc")), Some(CompilerType::compiler("clang"))); } // The inner CompilerProbe is moved into CachingProbe, so we have // to reach through the wrapper to read the call count. assert_eq!(cached.inner.calls(), 1, "inner probe must run at most once per path"); } #[test] fn caches_inconclusive_results_too() { // A binary that doesn't understand --version returns None; without // None-caching we'd burn a fork on every recognize() call. let inner = CountingProbe::empty(); let cached = CachingProbe::new(inner); for _ in 0..50 { assert_eq!(cached.probe(Path::new("cc")), None); } assert_eq!(cached.inner.calls(), 1, "None results must be cached too"); } #[test] fn distinct_paths_are_cached_independently() { let mut answers = HashMap::new(); answers.insert(PathBuf::from("/usr/bin/cc"), CompilerType::compiler("gcc")); answers.insert(PathBuf::from("/usr/local/bin/cc"), CompilerType::compiler("clang")); let inner = CountingProbe { answers, calls: Cell::new(0) }; let cached = CachingProbe::new(inner); assert_eq!(cached.probe(Path::new("/usr/bin/cc")), Some(CompilerType::compiler("gcc"))); assert_eq!(cached.probe(Path::new("/usr/local/bin/cc")), Some(CompilerType::compiler("clang"))); // Repeat to confirm both entries are stable in the cache. assert_eq!(cached.probe(Path::new("/usr/bin/cc")), Some(CompilerType::compiler("gcc"))); assert_eq!(cached.probe(Path::new("/usr/local/bin/cc")), Some(CompilerType::compiler("clang"))); assert_eq!(cached.inner.calls(), 2, "each distinct path triggers exactly one inner call"); } } #[cfg(unix)] mod unix { use super::super::identity::CompilerType; use super::CompilerProbe; use intercept::environment::{KEY_OS_MACOS_PRELOAD_PATH, KEY_OS_PRELOAD_PATH}; use std::os::unix::process::CommandExt; use std::path::Path; use std::process::{Command, Stdio}; use std::sync::mpsc; use std::thread; use std::time::Duration; /// Runs ` --version`, classifies the output. Default timeout: 2 seconds. pub(crate) struct VersionProbe { timeout: Duration, } impl VersionProbe { pub(crate) fn new() -> Self { Self { timeout: Duration::from_secs(2) } } #[cfg(test)] pub(crate) fn with_timeout(timeout: Duration) -> Self { Self { timeout } } } impl Default for VersionProbe { fn default() -> Self { Self::new() } } impl CompilerProbe for VersionProbe { fn probe(&self, executable_path: &Path) -> Option { let mut cmd = Command::new(executable_path); cmd.arg("--version") .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .env_remove(KEY_OS_PRELOAD_PATH) .env_remove(KEY_OS_MACOS_PRELOAD_PATH); // Put the child in its own process group so the watchdog can kill // not just the immediate child but the entire process tree it // spawns. Without this, SIGKILL on a `#!/bin/sh ; sleep 30` script // would kill sh but leave sleep holding the stdout/stderr pipes // open, blocking wait_with_output() for the script's full runtime. // Use setsid, not the safe process_group(0) the build supervisor // uses. process_group(0) leaves the probe a background group in // the caller's terminal session, and under the parallel probe // tests it gave intermittent misclassification (3 of 8 runs // failed, against 0 of 13 with setsid). // SAFETY: the closure runs in the forked child before exec. It // calls only setsid(), which is async-signal-safe, and builds an // io::Error from errno without allocating. It touches no locks or // shared state. unsafe { cmd.pre_exec(|| { if libc::setsid() == -1 { return Err(std::io::Error::last_os_error()); } Ok(()) }); } // exec fails with ETXTBSY while any process holds a write fd to // the probed file. The pre_exec closure above forces the // fork+exec spawn path, and a child forked elsewhere (by another // thread here, or by a build tool that just wrote a compiler // wrapper script) holds every inherited fd until its own exec. // That window is transient, so retry briefly; a persistently // busy file still declines as inconclusive. let mut attempts = 0; let child = loop { match cmd.spawn() { Ok(c) => break c, Err(err) if err.raw_os_error() == Some(libc::ETXTBSY) && attempts < 10 => { attempts += 1; thread::sleep(Duration::from_millis(10)); } Err(err) => { log::debug!("probe: spawn failed for {}: {err}", executable_path.display()); return None; } } }; let pid = child.id(); // Watchdog: SIGKILL the child's process group if --version does // not return in time. The cancel channel lets the main thread // tell the watchdog to exit cleanly when the child has already // finished. recv_timeout returns Err(Timeout) when nothing is // sent within the deadline; that's our signal to kill. let (cancel_tx, cancel_rx) = mpsc::channel::<()>(); let timeout = self.timeout; let watchdog = thread::spawn(move || { if cancel_rx.recv_timeout(timeout).is_err() { // SAFETY: killpg() is async-signal-safe; ESRCH on an // already-reaped group is benign. We signal the whole // process group (set up by setsid in pre_exec) to take // down any grandchildren that would otherwise keep // stdout/stderr pipes open. unsafe { libc::killpg(pid as libc::pid_t, libc::SIGKILL); } } }); let output = child.wait_with_output(); let _ = cancel_tx.send(()); let _ = watchdog.join(); let output = match output { Ok(o) => o, Err(err) => { log::debug!("probe: wait failed for {}: {err}", executable_path.display()); return None; } }; // A killed-by-watchdog process exits with a signal, not status code 0. // Treat any non-success as inconclusive. if !output.status.success() { log::debug!("probe: {} exited with {:?}", executable_path.display(), output.status); return None; } let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); classify_version_output(&stdout, &stderr) } } /// Classify the combined `--version` output of a candidate compiler. /// /// The rule set is intentionally narrow: a misclassification corrupts /// the compilation database (wrong flag-arity table), while a non- /// classification just falls back to the regex layer. We accept Clang /// and GCC and reject everything else. fn classify_version_output(stdout: &str, stderr: &str) -> Option { // Compilers print to stdout in practice, but the doc's PR #695 used // stderr; Apple clang has historically used stderr too. Check both. for haystack in [stdout, stderr] { // Apple clang prints "Apple clang version ...", upstream LLVM prints // "clang version ..."; both contain the substring "clang version". if haystack.contains("clang version") { return Some(CompilerType::compiler("clang")); } // GNU gcc prints a "Copyright (C) ... Free Software Foundation, // Inc." line in every build. The leading "(GCC)" vendor tag is // NOT reliable: distro and BSD-ports builds rewrite it to their // own tag ("(Alpine 15.2.0)", "(Ubuntu 13.3.0-...)", "(FreeBSD // Ports Collection)"), and when gcc is invoked as `cc` -- the // case this probe exists for -- the string "gcc" does not appear // at all. The FSF copyright is the only portable GCC signal. // Clang is excluded above (it prints LLVM license text, never the // FSF copyright), so this does not misclassify clang as gcc. See // issue #711. if haystack.contains("Free Software Foundation") { return Some(CompilerType::compiler("gcc")); } } None } #[cfg(test)] mod tests { //! Requirements: recognition-ambiguous-name-probe //! //! Every test in this module protects the ambiguous-name probe //! requirement. The classify_* and reject_* tests cover the //! classification rule (acceptance criterion: "classify it as GCC or //! Clang"); the fork_exec submodule covers the runtime safety //! properties (timeout, stdin closed, exit codes, missing executables). use super::*; #[test] fn classifies_upstream_clang() { let stdout = "clang version 17.0.6\nTarget: x86_64-pc-linux-gnu\n"; assert_eq!(classify_version_output(stdout, ""), Some(CompilerType::compiler("clang"))); } #[test] fn classifies_apple_clang() { let stdout = "Apple clang version 15.0.0 (clang-1500.3.9.4)\n"; assert_eq!(classify_version_output(stdout, ""), Some(CompilerType::compiler("clang"))); } #[test] fn classifies_gcc_across_vendor_builds() { // The leading "(...)" vendor tag varies per distro and BSD ports; // when gcc is invoked as `cc` the string "gcc" is absent entirely. // The FSF copyright line is the only marker common to every build, // so every one of these must classify as GCC. See issue #711. let cases = [ // Upstream FSF / Red Hat (the originally-recognized form). "gcc (GCC) 13.2.1 20231011 (Red Hat 13.2.1-4)\n\ Copyright (C) 2023 Free Software Foundation, Inc.\n", // Alpine Linux (the reported case). "gcc (Alpine 15.2.0) 15.2.0\n\ Copyright (C) 2025 Free Software Foundation, Inc.\n", // Ubuntu. "gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0\n\ Copyright (C) 2023 Free Software Foundation, Inc.\n", // FreeBSD ports. "gcc14 (FreeBSD Ports Collection) 14.2.0\n\ Copyright (C) 2024 Free Software Foundation, Inc.\n", // gcc invoked as `cc`: no "gcc" token anywhere in the output. "cc (Debian 12.2.0-14) 12.2.0\n\ Copyright (C) 2022 Free Software Foundation, Inc.\n", ]; for stdout in cases { assert_eq!( classify_version_output(stdout, ""), Some(CompilerType::compiler("gcc")), "expected GCC for output: {stdout:?}" ); } } #[test] fn classifies_via_stderr() { // Some toolchains print --version to stderr. Apple clang has done this // historically. let stderr = "Apple clang version 12.0.0\n"; assert_eq!(classify_version_output("", stderr), Some(CompilerType::compiler("clang"))); } #[test] fn rejects_garbage() { assert_eq!(classify_version_output("I am not a compiler\n", ""), None); } #[test] fn rejects_partial_gcc_marker() { // "gcc" appears but not the FSF copyright -> conservative reject. assert_eq!(classify_version_output("my-gcc-wrapper version 1.0\n", ""), None); } // ----- Real fork-exec behavior tests --------------------------- mod fork_exec { use super::*; use std::io::Write; use std::os::unix::fs::PermissionsExt; use std::time::Instant; /// Write `body` to a uniquely-named executable script in `dir`. fn write_script(dir: &std::path::Path, name: &str, body: &str) -> std::path::PathBuf { let path = dir.join(name); let mut f = std::fs::File::create(&path).unwrap(); f.write_all(body.as_bytes()).unwrap(); std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap(); path } #[test] fn classifies_a_script_that_emits_clang_signature() { let dir = tempfile::tempdir().unwrap(); let script = write_script( dir.path(), "fake-clang", "#!/bin/sh\necho 'clang version 17.0.0 (Fedora 17.0.0-1)'\n", ); assert_eq!(VersionProbe::new().probe(&script), Some(CompilerType::compiler("clang"))); } #[test] fn classifies_a_script_that_emits_gcc_signature() { // Alpine-style signature: a vendor tag instead of "(GCC)". // See issue #711. let dir = tempfile::tempdir().unwrap(); let script = write_script( dir.path(), "fake-gcc", "#!/bin/sh\n\ echo 'gcc (Alpine 15.2.0) 15.2.0'\n\ echo 'Copyright (C) 2025 Free Software Foundation, Inc.'\n", ); assert_eq!(VersionProbe::new().probe(&script), Some(CompilerType::compiler("gcc"))); } #[test] fn returns_none_for_garbage_output() { let dir = tempfile::tempdir().unwrap(); let script = write_script(dir.path(), "garbage", "#!/bin/sh\necho 'i am not a compiler'\n"); assert_eq!(VersionProbe::new().probe(&script), None); } #[test] fn returns_none_when_executable_exits_nonzero() { let dir = tempfile::tempdir().unwrap(); let script = write_script(dir.path(), "failing", "#!/bin/sh\necho 'clang version 1.0'\nexit 7\n"); // Even though the output looks like clang, a non-zero exit is // treated as inconclusive: a real compiler returns 0 on --version. assert_eq!(VersionProbe::new().probe(&script), None); } #[test] fn returns_none_when_executable_does_not_exist() { let probe = VersionProbe::new(); let result = probe.probe(std::path::Path::new("/nonexistent/path/to/cc-xxxx")); assert_eq!(result, None); } #[test] fn returns_within_budget_for_a_hung_process() { // The watchdog must SIGKILL a child that never completes // --version. We use a 200ms timeout and assert the call returns // in well under a second. let dir = tempfile::tempdir().unwrap(); let script = write_script(dir.path(), "hangs", "#!/bin/sh\nsleep 30\n"); let probe = VersionProbe::with_timeout(std::time::Duration::from_millis(200)); let started = Instant::now(); let result = probe.probe(&script); let elapsed = started.elapsed(); assert_eq!(result, None); assert!( elapsed < std::time::Duration::from_secs(2), "probe took {elapsed:?}, expected <2s; watchdog did not fire" ); } #[test] fn retries_exec_while_script_is_transiently_open_for_write() { // exec of a script fails with ETXTBSY while any process holds // a write fd to it. That happens transiently under parallel // fork+exec: a child forked by another thread inherits the // write fd of a script still being created and holds it until // its own exec. Hold the write handle open past the probe's // first spawn attempt and assert the retry absorbs it. let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("busy-clang"); let mut writer = std::fs::File::create(&path).unwrap(); writer.write_all(b"#!/bin/sh\necho 'clang version 1.0'\n").unwrap(); std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap(); let release = std::thread::spawn(move || { std::thread::sleep(std::time::Duration::from_millis(30)); drop(writer); }); let sut = VersionProbe::new().probe(&path); release.join().unwrap(); assert_eq!(sut, Some(CompilerType::compiler("clang"))); } #[test] fn does_not_block_on_a_binary_that_reads_stdin() { // If stdin were inherited (the default), this script would block // forever on `read`. The probe must close stdin so `read` sees // EOF immediately and the script exits. let dir = tempfile::tempdir().unwrap(); let script = write_script( dir.path(), "reads-stdin", "#!/bin/sh\nread line\necho 'clang version 1.0'\n", ); let probe = VersionProbe::with_timeout(std::time::Duration::from_secs(2)); let started = Instant::now(); let result = probe.probe(&script); let elapsed = started.elapsed(); // The script exits cleanly after reading EOF, prints the clang // signature, exits 0. Probe should classify it as Clang well // before the timeout. assert_eq!(result, Some(CompilerType::compiler("clang"))); assert!( elapsed < std::time::Duration::from_secs(1), "probe took {elapsed:?}; stdin was not closed" ); } } } } rizsotto-Bear-0c30b27/crates/semantic/src/interpreters/compilers/response_file.rs000066400000000000000000000351071527177720300304220ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Response-file (`@file`) inlining for intercepted compiler invocations. //! //! Some build systems (notably Xcode) keep part of a compiler's flags in a //! separate "response file" and pass it as `@/path/to/args.resp`. When the //! user opts in (`format.arguments.from_response_files`), [`expand`] replaces //! each `@file` token with the file's tokenized contents *before* flag //! classification, so the inlined tokens are classified, link-stripped, and //! split per source exactly like any other argument. //! //! Tokenization follows the compiler family Bear already identified for the //! entry; no new detection is introduced here. See the //! `output-response-file-inlining` requirement for the contract. use super::identity::CompilerType; use intercept::Execution; use log::warn; use std::path::{Path, PathBuf}; /// Maximum number of nested `@file` levels to follow. Guards against cycles /// (a response file that references itself, directly or transitively). const MAX_DEPTH: usize = 16; /// Tokenization conventions for a response file's contents. #[derive(Clone, Copy)] pub(super) enum Syntax { /// GCC/Clang family: whitespace separates tokens; single or double quotes /// group whitespace; a backslash quotes the next character. GnuClang, /// MSVC and clang-cl: Windows command-line rules; only double quotes group /// tokens; backslash escaping is positional, meaningful next to quotes. Msvc, } /// Selects the tokenization syntax for the compiler family Bear identified, /// reading the family's `response_file_syntax` from the generated /// [`super::flag_based::FAMILIES`] registry. A wrapper (no family id) and any /// id absent from the registry default to GNU/Clang syntax. pub(super) fn syntax_for(compiler_type: CompilerType) -> Syntax { match compiler_type { CompilerType::Compiler(id) => super::flag_based::FAMILIES .iter() .find(|def| def.id == id.as_str()) .map(|def| def.response_file_syntax) .unwrap_or(Syntax::GnuClang), CompilerType::Wrapper => Syntax::GnuClang, } } /// Replaces every `@file` argument with the file's tokenized contents. /// /// The compiler executable at index 0 is never treated as a response file. /// `@file` paths are resolved relative to the invocation's working directory, /// matching how the compiler itself opens them. Missing/unreadable files and /// references past [`MAX_DEPTH`] are left literal with a warning, so a single /// stale build artefact never fails the whole database. pub(super) fn expand(execution: Execution, syntax: Syntax) -> Execution { let Execution { executable, arguments, working_dir, environment } = execution; let mut expanded = Vec::with_capacity(arguments.len()); for (index, argument) in arguments.into_iter().enumerate() { match argument.strip_prefix('@') { Some(path) if index != 0 && !path.is_empty() => { expand_reference(&mut expanded, path, &working_dir, syntax, 1); } _ => expanded.push(argument), } } Execution { executable, arguments: expanded, working_dir, environment } } /// Reads, tokenizes, and recursively expands a single `@file` reference into /// `out`. On any failure the `@path` token is appended unchanged. fn expand_reference(out: &mut Vec, raw_path: &str, base: &Path, syntax: Syntax, depth: usize) { if depth > MAX_DEPTH { warn!( "response file expansion exceeded depth limit ({MAX_DEPTH}); leaving @{raw_path} literal (possible cycle)" ); out.push(format!("@{raw_path}")); return; } let path = resolve(base, raw_path); let content = match std::fs::read_to_string(&path) { Ok(content) => content, Err(error) => { warn!("cannot read response file {}: {error}; leaving @{raw_path} literal", path.display()); out.push(format!("@{raw_path}")); return; } }; for token in tokenize(&content, syntax) { match token.strip_prefix('@') { Some(nested) if !nested.is_empty() => { expand_reference(out, nested, base, syntax, depth + 1); } _ => out.push(token), } } } /// Resolves a response-file path relative to the invocation's working /// directory. Absolute paths are used as-is. fn resolve(base: &Path, raw_path: &str) -> PathBuf { let path = Path::new(raw_path); if path.is_absolute() { path.to_path_buf() } else { base.join(path) } } fn tokenize(content: &str, syntax: Syntax) -> Vec { match syntax { Syntax::GnuClang => tokenize_gnu(content), Syntax::Msvc => tokenize_msvc(content), } } /// Tokenizes GCC/Clang-style response-file text: whitespace separates tokens, /// single and double quotes group whitespace, and a backslash quotes the next /// character. Matching libiberty/clang, a backslash is literal inside single /// quotes and, inside double quotes, only escapes a following `"` or `\`. fn tokenize_gnu(content: &str) -> Vec { enum Quote { None, Single, Double, } let mut tokens = Vec::new(); let mut current = String::new(); let mut started = false; let mut quote = Quote::None; let mut chars = content.chars().peekable(); while let Some(c) = chars.next() { match quote { Quote::Single => { if c == '\'' { quote = Quote::None; } else { current.push(c); } } Quote::Double => match c { '"' => quote = Quote::None, '\\' => match chars.peek() { Some('"') | Some('\\') => current.push(chars.next().expect("peeked")), _ => current.push('\\'), }, _ => current.push(c), }, Quote::None => { if c.is_whitespace() { if started { tokens.push(std::mem::take(&mut current)); started = false; } continue; } started = true; match c { '\'' => quote = Quote::Single, '"' => quote = Quote::Double, '\\' => match chars.next() { Some(next) => current.push(next), None => current.push('\\'), }, _ => current.push(c), } } } } if started { tokens.push(current); } tokens } /// Tokenizes MSVC/clang-cl response-file text following Windows /// `CommandLineToArgv` rules: whitespace separates tokens, double quotes group, /// and backslashes are literal except when they precede a quote (`2n` -> `n` /// backslashes and a quote toggle; `2n+1` -> `n` backslashes and a literal /// quote). pub(super) fn tokenize_msvc(content: &str) -> Vec { let chars: Vec = content.chars().collect(); let mut tokens = Vec::new(); let mut current = String::new(); let mut started = false; let mut in_quotes = false; let mut i = 0; while i < chars.len() { let c = chars[i]; if c == '\\' { let mut backslashes = 0; while i < chars.len() && chars[i] == '\\' { backslashes += 1; i += 1; } if i < chars.len() && chars[i] == '"' { for _ in 0..backslashes / 2 { current.push('\\'); } started = true; if backslashes % 2 == 1 { current.push('"'); } else { in_quotes = !in_quotes; } i += 1; } else { for _ in 0..backslashes { current.push('\\'); } started = true; } } else if c == '"' { started = true; // Inside a quoted segment, a doubled quote ("") is a literal quote // and keeps the segment open (CommandLineToArgv rule). if in_quotes && i + 1 < chars.len() && chars[i + 1] == '"' { current.push('"'); i += 2; } else { in_quotes = !in_quotes; i += 1; } } else if c.is_whitespace() && !in_quotes { if started { tokens.push(std::mem::take(&mut current)); started = false; } i += 1; } else { current.push(c); started = true; i += 1; } } if started { tokens.push(current); } tokens } #[cfg(test)] mod tests { //! Requirements: output-response-file-inlining use super::*; use std::collections::HashMap; use std::fs; fn execution(arguments: Vec<&str>, working_dir: &Path) -> Execution { Execution::from_strings(arguments[0], arguments, working_dir.to_str().unwrap(), HashMap::new()) } fn args_of(execution: &Execution) -> Vec { execution.arguments.clone() } #[test] fn gnu_tokenizer_splits_on_whitespace() { let sut = tokenize_gnu("-I/opt/include -DEXTRA=2"); assert_eq!(sut, vec!["-I/opt/include", "-DEXTRA=2"]); } #[test] fn gnu_tokenizer_strips_quotes_and_groups() { let sut = tokenize_gnu("'-std=gnu++20' -fmodules"); assert_eq!(sut, vec!["-std=gnu++20", "-fmodules"]); } #[test] fn gnu_tokenizer_double_quotes_group_whitespace() { let sut = tokenize_gnu("-I \"/opt/my includes\""); assert_eq!(sut, vec!["-I", "/opt/my includes"]); } #[test] fn gnu_tokenizer_backslash_quotes_next_char() { let sut = tokenize_gnu(r"-DName=a\ b"); assert_eq!(sut, vec!["-DName=a b"]); } #[test] fn msvc_tokenizer_applies_windows_rules() { let sut = tokenize_msvc(r#"/I "C:\Program Files\inc" /DFOO=1"#); assert_eq!(sut, vec!["/I", r"C:\Program Files\inc", "/DFOO=1"]); } #[test] fn msvc_tokenizer_doubled_quote_is_literal_inside_quotes() { // CommandLineToArgv: "" inside a quoted segment yields a literal " and // keeps the segment open. let sut = tokenize_msvc(r#"/DGREETING="say ""hi"""#); assert_eq!(sut, vec![r#"/DGREETING=say "hi""#]); } #[test] fn msvc_tokenizer_backslash_before_quote_escapes() { // Backslashes not before a quote are literal (a\\b -> a\\b); an odd // run before a quote (\") yields a literal quote. let sut = tokenize_msvc(r#"/DPATH="a\\b" \"x\""#); assert_eq!(sut, vec![r"/DPATH=a\\b", r#""x""#]); } #[test] fn gnu_tokenizer_double_quote_backslash_only_escapes_quote_and_backslash() { // Inside double quotes \" -> ", \\ -> \, but \d (any other char) keeps // the backslash literal (libiberty/clang). let sut = tokenize_gnu(r#""a\"b\\c\d""#); assert_eq!(sut, vec![r#"a"b\c\d"#]); } #[test] fn syntax_selection_follows_compiler_family() { for ty in [CompilerType::compiler("msvc"), CompilerType::compiler("clang_cl")] { assert!(matches!(syntax_for(ty), Syntax::Msvc), "{ty} should use MSVC syntax"); } for ty in [ CompilerType::compiler("gcc"), CompilerType::compiler("clang"), CompilerType::compiler("cuda"), CompilerType::compiler("vala"), ] { assert!(matches!(syntax_for(ty), Syntax::GnuClang), "{ty} should use GNU/Clang syntax"); } } #[test] fn expand_inlines_response_file_at_token_position() { let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("flags.resp"), "-I/opt/include -DEXTRA=2").unwrap(); let input = execution(vec!["cc", "-DBASE=1", "@flags.resp", "-c", "src.c"], dir.path()); let sut = args_of(&expand(input, Syntax::GnuClang)); assert_eq!(sut, vec!["cc", "-DBASE=1", "-I/opt/include", "-DEXTRA=2", "-c", "src.c"]); assert!(sut.iter().all(|a| !a.starts_with('@'))); } #[test] fn expand_ignores_executable_at_index_zero() { let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("x"), "should-not-read").unwrap(); let input = execution(vec!["@x", "-c", "src.c"], dir.path()); let sut = args_of(&expand(input, Syntax::GnuClang)); assert_eq!(sut[0], "@x"); } #[test] fn expand_resolves_absolute_paths_as_is() { let dir = tempfile::tempdir().unwrap(); let resp = dir.path().join("abs.resp"); fs::write(&resp, "-DONLY").unwrap(); let input = execution(vec!["cc", &format!("@{}", resp.display()), "src.c"], Path::new("/elsewhere")); let sut = args_of(&expand(input, Syntax::GnuClang)); assert_eq!(sut, vec!["cc", "-DONLY", "src.c"]); } #[test] fn expand_follows_nested_references() { let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("outer.resp"), "-DOUTER @inner.resp -DTAIL").unwrap(); fs::write(dir.path().join("inner.resp"), "-DINNER").unwrap(); let input = execution(vec!["cc", "@outer.resp", "src.c"], dir.path()); let sut = args_of(&expand(input, Syntax::GnuClang)); assert_eq!(sut, vec!["cc", "-DOUTER", "-DINNER", "-DTAIL", "src.c"]); assert!(sut.iter().all(|a| !a.starts_with('@'))); } #[test] fn expand_keeps_missing_file_literal() { let dir = tempfile::tempdir().unwrap(); let input = execution(vec!["cc", "@gone.resp", "src.c"], dir.path()); let sut = args_of(&expand(input, Syntax::GnuClang)); assert_eq!(sut, vec!["cc", "@gone.resp", "src.c"]); } #[test] fn expand_stops_at_depth_limit_on_cycle() { let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("loop.resp"), "-DBEFORE @loop.resp").unwrap(); let input = execution(vec!["cc", "@loop.resp", "src.c"], dir.path()); let sut = args_of(&expand(input, Syntax::GnuClang)); // The self-reference is left literal once the depth limit is hit; the // tokens before it on each level are still emitted, and the trailing // source argument is preserved. assert_eq!(sut.first().map(String::as_str), Some("cc")); assert_eq!(sut.last().map(String::as_str), Some("src.c")); assert!(sut.iter().any(|a| a == "@loop.resp"), "offending token should remain literal: {sut:?}"); assert!(sut.iter().filter(|a| *a == "-DBEFORE").count() <= MAX_DEPTH); } } rizsotto-Bear-0c30b27/crates/semantic/src/interpreters/compilers/wrapper.rs000066400000000000000000000237611527177720300272500ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Compiler wrapper handling for ccache, distcc, sccache, and icecc. //! //! Wrappers sit between the build system and the real compiler: //! `ccache gcc -c main.c`. The job here is small: detect the wrapper by //! basename, locate the real compiler in argv (skipping wrapper-specific //! flags like `distcc -j 4`), and produce a fresh [`Execution`] that //! names the real compiler. The caller (`CompilerInterpreter::recognize`) //! then dispatches that execution as if the wrapper had never been there. //! //! This module is the authority on what counts as a wrapper: //! [`WRAPPER_NAMES`] is shared with `compiler_recognition`, which consults //! it to skip the `--version` probe for a wrapper's own basename (the //! recognizer's regex pattern itself comes from `RECOGNITION_PATTERNS`, //! generated from each wrapper file's own `recognize` entry). Both //! `WRAPPER_NAMES` and `WRAPPER_OPTIONS` are generated from //! `crates/semantic/compilers/*.yaml` (the `type: wrapper` files) -- see //! `build-support/compilers-codegen`. use super::compiler_recognition::CompilerRecognizer; use super::identity::CompilerType; use intercept::Execution; use std::path::{Path, PathBuf}; include!(concat!(env!("OUT_DIR"), "/wrappers.rs")); /// Try to strip a wrapper from `execution`, returning the inner compiler /// invocation along with its recognized [`CompilerType`]. /// /// Returns `Ok((inner, ty))` when `execution` is a wrapper invocation we /// recognize and the inner argv names a real (non-wrapper) compiler; /// `ty` is the inner compiler's type so the caller can dispatch without /// re-running the recognizer. /// /// Returns `Err(execution)` -- handing the original execution back -- in /// every other case: not a wrapper, missing inner argument, inner not a /// compiler, or wrapper-of-wrapper (e.g. `ccache distcc gcc`). The Err /// arm lets the caller surface `RecognizeResult::NotRecognized` without /// re-cloning the execution. pub(super) fn unwrap( execution: Execution, recognizer: &CompilerRecognizer, ) -> Result<(Execution, CompilerType), Execution> { let Some(wrapper_name) = detect_wrapper_name(&execution.executable) else { return Err(execution); }; let Some((real_compiler, filtered_args)) = extract_real_compiler(wrapper_name, &execution.arguments) else { return Err(execution); }; // The inner argv must name a real compiler; reject wrapper-of-wrapper // (which would otherwise loop) and unknown executables. let inner_type = match recognizer.recognize(&real_compiler) { Some(CompilerType::Wrapper) | None => return Err(execution), Some(ty) => ty, }; Ok(( Execution { executable: real_compiler, arguments: filtered_args, working_dir: execution.working_dir, environment: execution.environment, }, inner_type, )) } /// Identify the wrapper by basename. Returns the static name string so /// callers can branch on it without allocating. fn detect_wrapper_name(executable: &Path) -> Option<&'static str> { let name = executable.file_stem()?.to_str()?; WRAPPER_NAMES.iter().copied().find(|&w| w == name) } /// Locate the real compiler in a wrapper invocation's argv and return the /// surviving argv slice (compiler at index 0). Pure argv parsing -- does /// not consult the recognizer; callers are responsible for validating that /// the returned path is actually a compiler. /// /// Generic across every wrapper: skip `wrapper_name`'s own options (each /// consuming `1 + arity` argv slots), then the next slot is the real /// compiler. Most wrappers (ccache, sccache, icecc) have no options of /// their own, so the loop body never matches and `i` stays 1 -- argv[1] is /// the compiler, exactly as before this became data-driven. fn extract_real_compiler(wrapper_name: &str, args: &[String]) -> Option<(PathBuf, Vec)> { let opts = wrapper_options(wrapper_name); let mut i = 1; while i < args.len() { match opts.iter().find(|(flag, _)| *flag == args[i]) { Some((_, arity)) => i += 1 + *arity as usize, None => break, } } let inner = args.get(i)?; Some((PathBuf::from(inner), args[i..].to_vec())) } /// The `(option, arity)` pairs `wrapper_name` accepts before its inner /// compiler argv, or an empty slice if it declares none. fn wrapper_options(wrapper_name: &str) -> &'static [(&'static str, u32)] { WRAPPER_OPTIONS.iter().find(|(name, _)| *name == wrapper_name).map(|(_, opts)| *opts).unwrap_or(&[]) } #[cfg(test)] mod tests { use super::*; use std::collections::HashMap; fn create_execution(args: Vec<&str>) -> Execution { Execution::from_strings(args[0], args, "/project", HashMap::new()) } // Requirements: recognition-compiler-launchers #[test] fn test_detect_wrapper_name() { let sut = |path_str| detect_wrapper_name(Path::new(path_str)); assert_eq!(sut("/usr/bin/ccache"), Some("ccache")); assert_eq!(sut("/opt/distcc"), Some("distcc")); assert_eq!(sut("sccache"), Some("sccache")); assert_eq!(sut("/usr/lib/icecc/bin/icecc"), Some("icecc")); assert_eq!(sut("/usr/bin/gcc"), None); assert_eq!(sut("make"), None); // icerun launches arbitrary commands on the icecream cluster, not // compiler invocations -- it is deliberately not a launcher here. assert_eq!(sut("icerun"), None); } #[test] fn test_distcc_options_have_expected_arity() { let opts = wrapper_options("distcc"); let cases: &[(&str, u32)] = &[ ("-j", 1), ("--jobs", 1), ("-v", 0), ("--verbose", 0), ("-i", 0), ("--show-hosts", 0), ("--scan-avail", 0), ("--show-principal", 0), ]; for (flag, expected_arity) in cases { let arity = opts.iter().find(|(f, _)| f == flag).map(|(_, a)| *a); assert_eq!(arity, Some(*expected_arity), "flag: {}", flag); } // Unknown flags (including compiler-own flags like -c/-Wall/--output // that distcc never claims) are not in the table. for unknown in ["-c", "-Wall", "--output"] { assert!(opts.iter().find(|(f, _)| *f == unknown).is_none(), "flag: {}", unknown); } } // Requirements: recognition-compiler-launchers #[test] fn test_unwrap_extracts_real_compiler_for_valid_wrapper_calls() { let recognizer = CompilerRecognizer::new(); let cases: Vec<(Vec<&str>, &str)> = vec![ (vec!["ccache", "gcc", "-c", "main.c"], "gcc"), (vec!["/usr/bin/ccache", "gcc", "-c", "main.c"], "gcc"), (vec!["ccache", "/usr/bin/gcc", "-c", "main.c"], "/usr/bin/gcc"), (vec!["ccache", "clang", "-c", "main.c"], "clang"), (vec!["ccache", "/usr/bin/clang", "-c", "main.c"], "/usr/bin/clang"), (vec!["sccache", "gcc", "-c", "main.c"], "gcc"), (vec!["sccache", "clang", "-c", "main.c"], "clang"), (vec!["distcc", "-j", "4", "gcc", "-c", "main.c"], "gcc"), (vec!["distcc", "clang", "-c", "main.c"], "clang"), (vec!["icecc", "gcc", "-c", "main.c"], "gcc"), (vec!["/usr/bin/icecc", "clang", "-c", "main.c"], "clang"), ]; for (args, expected_inner) in cases { let exec = create_execution(args.clone()); let (inner, _ty) = unwrap(exec, &recognizer).unwrap_or_else(|_| panic!("unwrap should succeed for {:?}", args)); assert_eq!(inner.executable, PathBuf::from(expected_inner)); } } // Requirements: recognition-compiler-launchers #[test] fn test_unwrap_rejects_non_wrapper_or_invalid_calls() { let recognizer = CompilerRecognizer::new(); let cases: Vec> = vec![ vec!["gcc", "-c", "main.c"], // not a wrapper at all vec!["make", "all"], // not a wrapper vec!["ccache"], // wrapper without inner argv vec!["ccache", "make", "all"], // inner is not a compiler vec!["ccache", "distcc", "gcc", "-c", "main.c"], // wrapper-of-wrapper vec!["icecc"], // launcher without inner argv vec!["icecc", "make", "all"], // inner is not a compiler vec!["icecc", "ccache", "gcc", "-c", "main.c"], // launcher-of-launcher ]; for args in cases { let exec = create_execution(args.clone()); assert!(unwrap(exec, &recognizer).is_err(), "unwrap should reject {:?}", args); } } // Requirements: recognition-compiler-launchers #[test] fn test_unwrap_preserves_working_dir_and_environment() { let recognizer = CompilerRecognizer::new(); let mut env = HashMap::new(); env.insert("CC", "gcc"); let exec = Execution::from_strings( "/usr/bin/ccache", vec!["ccache", "gcc", "-c", "main.c"], "/custom/dir", env, ); let (inner, ty) = unwrap(exec, &recognizer).expect("unwrap should succeed"); assert_eq!(inner.working_dir, PathBuf::from("/custom/dir")); assert_eq!(inner.environment.get("CC"), Some(&"gcc".to_string())); assert_eq!(ty, CompilerType::compiler("gcc")); } // Requirements: recognition-compiler-launchers #[test] fn test_unwrap_strips_distcc_flags_from_filtered_args() { let recognizer = CompilerRecognizer::new(); let exec = Execution::from_strings( "/usr/bin/distcc", vec!["distcc", "-j", "4", "gcc", "-c", "main.c", "-o", "main.o"], "/project", HashMap::new(), ); let (inner, _ty) = unwrap(exec, &recognizer).expect("unwrap should succeed"); assert_eq!(inner.executable, PathBuf::from("gcc")); assert_eq!(inner.arguments, vec!["gcc", "-c", "main.c", "-o", "main.o"]); } } rizsotto-Bear-0c30b27/crates/semantic/src/interpreters/ignore.rs000066400000000000000000000107361527177720300250540ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later use std::collections::HashSet; use std::ffi::OsString; use crate::{Execution, Interpreter, RecognizeResult}; const COREUTILS_MESSAGE: &str = "coreutils executable"; const COMPILER_MESSAGE: &str = "compiler specified in config to ignore"; /// A tool to ignore a command execution by executable filename. /// /// Matches against the filename component of the executable path, /// so `/usr/bin/ls` and `/usr/local/bin/ls` both match `ls`. pub(super) struct IgnoreByPath { filenames: HashSet, reason: &'static str, } impl IgnoreByPath { pub(super) fn new() -> Self { let filenames = COREUTILS_FILES.iter().map(OsString::from).collect(); Self { filenames, reason: COREUTILS_MESSAGE } } pub(super) fn from(compilers: impl IntoIterator>) -> Self { let filenames = compilers.into_iter().filter_map(|p| p.as_ref().file_name().map(OsString::from)).collect(); Self { filenames, reason: COMPILER_MESSAGE } } } impl Default for IgnoreByPath { fn default() -> Self { Self::new() } } impl Interpreter for IgnoreByPath { fn recognize(&self, execution: Execution) -> RecognizeResult { if execution.executable.file_name().is_some_and(|f| self.filenames.contains(f)) { return RecognizeResult::Ignored(self.reason); } RecognizeResult::NotRecognized(execution) } } const COREUTILS_FILES: [&str; 106] = [ "[", "arch", "b2sum", "base32", "base64", "basename", "basenc", "cat", "chcon", "chgrp", "chmod", "chown", "cksum", "comm", "cp", "csplit", "cut", "date", "dd", "df", "dir", "dircolors", "dirname", "du", "echo", "env", "expand", "expr", "factor", "false", "fmt", "fold", "groups", "head", "hostid", "id", "install", "join", "link", "ln", "logname", "ls", "md5sum", "mkdir", "mkfifo", "mknod", "mktemp", "mv", "nice", "nl", "nohup", "nproc", "numfmt", "od", "paste", "pathchk", "pinky", "pr", "printenv", "printf", "ptx", "pwd", "readlink", "realpath", "rm", "rmdir", "runcon", "seq", "sha1sum", "sha224sum", "sha256sum", "sha384sum", "sha512sum", "shred", "shuf", "sleep", "sort", "split", "stat", "stdbuf", "stty", "sum", "sync", "tac", "tail", "tee", "test", "timeout", "touch", "tr", "true", "truncate", "tsort", "tty", "uname", "unexpand", "uniq", "unlink", "users", "vdir", "wc", "who", "whoami", "yes", "make", "gmake", ]; #[cfg(test)] mod test { use std::collections::HashMap; use std::path::PathBuf; use super::*; #[test] fn test_executions_are_ignored_by_executable_name() { let input = Execution::from_strings( "/usr/bin/ls", vec!["ls", "/home/user/build"], "/home/user", HashMap::new(), ); let sut = IgnoreByPath::new(); assert!(matches!(sut.recognize(input), RecognizeResult::Ignored(_))); } #[test] fn test_executions_are_ignored_regardless_of_path_prefix() { let input = Execution::from_strings( "/usr/local/bin/ls", vec!["ls", "/home/user/build"], "/home/user", HashMap::new(), ); let sut = IgnoreByPath::new(); assert!(matches!(sut.recognize(input), RecognizeResult::Ignored(_))); } #[test] fn test_not_known_executables_are_not_recognized() { let input = Execution::from_strings( "/usr/bin/unknown", vec!["unknown", "/home/user/build"], "/home/user", HashMap::new(), ); let sut = IgnoreByPath::new(); assert!(matches!(sut.recognize(input), RecognizeResult::NotRecognized(_))); } #[test] fn test_compiler_ignore_matches_on_filename() { let compilers = vec![PathBuf::from("/usr/bin/gcc")]; let sut = IgnoreByPath::from(&compilers); let input = Execution::from_strings( "/opt/toolchain/bin/gcc", vec!["gcc", "-c", "foo.c"], "/home/user", HashMap::new(), ); assert!(matches!(sut.recognize(input), RecognizeResult::Ignored(_))); } } rizsotto-Bear-0c30b27/crates/semantic/src/interpreters/matchers/000077500000000000000000000000001527177720300250225ustar00rootroot00000000000000rizsotto-Bear-0c30b27/crates/semantic/src/interpreters/matchers/mod.rs000066400000000000000000000347131527177720300261570ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Compiler flag pattern matching and semantic analysis system. //! //! This module provides a comprehensive framework for recognizing and categorizing //! compiler command-line flags. It separates the concerns of flag pattern matching //! (HOW to consume arguments) from semantic meaning (WHAT the flag represents). //! //! The system supports various flag patterns including exact matches, prefix matches, //! and flags that require additional arguments in different forms (separate, glued, etc.). pub(super) mod source; use crate::ArgumentKind; pub use source::{is_c_family_source, is_header_file, looks_like_a_source_file}; /// Flag pattern definitions that describe HOW to consume arguments from the command line. /// These patterns define the syntactic structure of compiler flags and their arguments. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(super) enum FlagPattern { /// Match the flag exactly with specified number of required separate arguments: "-c", "-o file" /// The u32 represents how many additional arguments are required (0 for no additional args) Exactly(&'static str, u32), /// Match the flag exactly with 1 required argument glued with '=': "-std=c99" ExactlyWithEq(&'static str), /// Match exactly with 1 required argument, glued with '=' OR separate: "-std=c99", "-std c99" ExactlyWithEqOrSep(&'static str), /// Match the flag exactly with 1 required argument glued with ':': "/std:c++20" ExactlyWithColon(&'static str), /// Match exactly with 1 required argument, glued with ':' OR separate: "/std:c++20", "/std c++20" ExactlyWithColonOrSep(&'static str), /// Match exactly with 1 required argument in any form: "-Dname=value", "-D name=value", "-Dname", "-D name" ExactlyWithGluedOrSep(&'static str), /// Match as prefix with specified number of additional arguments: anything starting with flag /// The u32 represents how many additional arguments to consume beyond the flag itself Prefix(&'static str, u32), } impl FlagPattern { /// Get the flag string from the pattern pub fn flag(&self) -> &'static str { match self { FlagPattern::Exactly(flag, _) => flag, FlagPattern::ExactlyWithEq(flag) => flag, FlagPattern::ExactlyWithEqOrSep(flag) => flag, FlagPattern::ExactlyWithColon(flag) => flag, FlagPattern::ExactlyWithColonOrSep(flag) => flag, FlagPattern::ExactlyWithGluedOrSep(flag) => flag, FlagPattern::Prefix(flag, _) => flag, } } } /// A flag definition combining the flag pattern and argument kind. /// This combines syntactic pattern matching with semantic meaning for compiler flags. #[derive(Debug, Clone)] pub(super) struct FlagRule { /// The flag pattern including name and matching instruction pub pattern: FlagPattern, /// What this flag represents semantically pub kind: ArgumentKind, } /// Result of matching a flag against command line arguments. /// Contains only metadata (which rule matched and how many args it consumed), /// avoiding string clones in the matching phase. #[derive(Debug, Clone)] pub(super) struct FlagMatch { /// The flag definition that matched pub rule: FlagRule, /// Number of arguments consumed from the command line (including the flag itself) pub consumed_count: usize, } /// Check if `arg` starts with `prefix` followed by '='. /// Zero-allocation alternative to `arg.starts_with(&format!("{}=", prefix))`. fn starts_with_eq(arg: &str, prefix: &str) -> bool { arg.len() > prefix.len() && arg.as_bytes()[prefix.len()] == b'=' && arg.starts_with(prefix) } /// Check if `arg` starts with `prefix` followed by ':'. /// Zero-allocation alternative to `arg.starts_with(&format!("{}:", prefix))`. fn starts_with_colon(arg: &str, prefix: &str) -> bool { arg.len() > prefix.len() && arg.as_bytes()[prefix.len()] == b':' && arg.starts_with(prefix) } /// A flag matcher that contains flag definitions for a specific compiler pub(super) struct FlagAnalyzer { /// All flag definitions, sorted by priority (longer flags first for better matching) rules: &'static [FlagRule], } impl FlagAnalyzer { /// Create a new flag matcher from pre-sorted flag definitions /// /// The rules slice must already be sorted by flag length in descending order /// for proper matching behavior (longer flags matched first). pub fn new(rules: &'static [FlagRule]) -> Self { Self { rules } } /// Try to match a flag against the remaining command line arguments pub fn match_flag(&self, args: &[String]) -> Option { if args.is_empty() { return None; } for definition in self.rules { if let Some(result) = self.try_match_definition(definition, args) { return Some(result); } } None } /// Try to match a specific flag definition against the arguments fn try_match_definition(&self, definition: &FlagRule, args: &[String]) -> Option { let current_arg = &args[0]; let flag = definition.pattern.flag(); match &definition.pattern { FlagPattern::Exactly(_, required_count) => { let required_args = *required_count as usize; if current_arg == flag { if required_args == 0 { Some(FlagMatch { rule: definition.clone(), consumed_count: 1 }) } else if args.len() > required_args { Some(FlagMatch { rule: definition.clone(), consumed_count: 1 + required_args }) } else { None } } else { None } } FlagPattern::ExactlyWithEq(_) => { if starts_with_eq(current_arg, flag) { Some(FlagMatch { rule: definition.clone(), consumed_count: 1 }) } else { None } } FlagPattern::ExactlyWithEqOrSep(_) => { if current_arg == flag && args.len() > 1 { Some(FlagMatch { rule: definition.clone(), consumed_count: 2 }) } else if starts_with_eq(current_arg, flag) { Some(FlagMatch { rule: definition.clone(), consumed_count: 1 }) } else { None } } FlagPattern::ExactlyWithColon(_) => { if starts_with_colon(current_arg, flag) { Some(FlagMatch { rule: definition.clone(), consumed_count: 1 }) } else { None } } FlagPattern::ExactlyWithColonOrSep(_) => { if current_arg == flag && args.len() > 1 { Some(FlagMatch { rule: definition.clone(), consumed_count: 2 }) } else if starts_with_colon(current_arg, flag) { Some(FlagMatch { rule: definition.clone(), consumed_count: 1 }) } else { None } } FlagPattern::ExactlyWithGluedOrSep(_) => { if current_arg == flag && args.len() > 1 { Some(FlagMatch { rule: definition.clone(), consumed_count: 2 }) } else if current_arg.starts_with(flag) && current_arg.len() > flag.len() { Some(FlagMatch { rule: definition.clone(), consumed_count: 1 }) } else { None } } FlagPattern::Prefix(_, required_count) => { if current_arg.starts_with(flag) { let required_args = *required_count as usize; if required_args == 0 { Some(FlagMatch { rule: definition.clone(), consumed_count: 1 }) } else if args.len() > required_args { let all_args_valid = args.iter().take(required_args + 1).skip(1).all(|arg| !arg.starts_with('-')); if all_args_valid { Some(FlagMatch { rule: definition.clone(), consumed_count: 1 + required_args }) } else { Some(FlagMatch { rule: definition.clone(), consumed_count: 1 }) } } else { None } } else { None } } } } } impl FlagRule { /// Create a new flag definition pub const fn new(pattern: FlagPattern, kind: ArgumentKind) -> Self { Self { pattern, kind } } } /// Separator type for splitting environment variable values. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(super) enum EnvSeparator { /// Platform path separator (`:` on Unix, `;` on Windows) via `std::env::split_paths`. Path, /// Fixed separator string. Fixed(&'static str), } /// Position for expanded environment variable arguments relative to command-line args. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(super) enum EnvPosition { Prepend, Append, } /// How an environment variable value maps to compiler arguments. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(super) enum EnvMapping { /// Split value by separator, emit `flag ` per element. Flag { flag: &'static str, separator: EnvSeparator }, /// Shell-split value and insert as raw arguments at the given position. Expand { position: EnvPosition }, } /// A rule mapping an environment variable to compiler arguments. #[derive(Debug, Clone)] pub(super) struct EnvRule { pub variable: &'static str, pub mapping: EnvMapping, pub kind: ArgumentKind, } impl EnvRule { pub const fn new(variable: &'static str, mapping: EnvMapping, kind: ArgumentKind) -> Self { Self { variable, mapping, kind } } } #[cfg(test)] mod tests { use super::*; use crate::{CompilerPass, PassEffect}; use std::sync::LazyLock; // Create test-specific flags (not compiler-specific) static TEST_FLAGS: LazyLock> = LazyLock::new(|| { let mut flags = vec![ FlagRule::new( FlagPattern::Exactly("-c", 0), ArgumentKind::Other(PassEffect::StopsAt(CompilerPass::Compiling)), ), FlagRule::new( FlagPattern::ExactlyWithEqOrSep("-std"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling)), ), FlagRule::new( FlagPattern::ExactlyWithGluedOrSep("-I"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Preprocessing)), ), FlagRule::new(FlagPattern::Prefix("-W", 0), ArgumentKind::Other(PassEffect::None)), FlagRule::new(FlagPattern::Exactly("-o", 1), ArgumentKind::Output), ]; // Sort by flag length descending to ensure longer matches are tried first flags.sort_by_key(|b| std::cmp::Reverse(b.pattern.flag().len())); flags }); fn test_flags() -> &'static [FlagRule] { &TEST_FLAGS } #[test] fn test_exactly_matching() { let matcher = FlagAnalyzer::new(test_flags()); let args = vec!["-c".to_string()]; let result = matcher.match_flag(&args).unwrap(); assert_eq!(result.consumed_count, 1); } #[test] fn test_equal_matching() { let matcher = FlagAnalyzer::new(test_flags()); // Test glued with = let args = vec!["-std=c99".to_string()]; let result = matcher.match_flag(&args).unwrap(); assert_eq!(result.consumed_count, 1); // Test separate form let args = vec!["-std".to_string(), "c99".to_string()]; let result = matcher.match_flag(&args).unwrap(); assert_eq!(result.consumed_count, 2); } #[test] fn test_glued_or_separate_matching() { let matcher = FlagAnalyzer::new(test_flags()); // Test glued form let args = vec!["-I/usr/include".to_string()]; let result = matcher.match_flag(&args).unwrap(); assert_eq!(result.consumed_count, 1); // Test separate form let args = vec!["-I".to_string(), "/usr/include".to_string()]; let result = matcher.match_flag(&args).unwrap(); assert_eq!(result.consumed_count, 2); // Test flag only (should not match since -I requires an argument) let args = vec!["-I".to_string()]; let result = matcher.match_flag(&args); assert!(result.is_none()); } #[test] fn test_prefix_matching() { let matcher = FlagAnalyzer::new(test_flags()); let args = vec!["-Wall".to_string()]; let result = matcher.match_flag(&args).unwrap(); assert_eq!(result.consumed_count, 1); } #[test] fn test_colon_matching() { use std::sync::LazyLock; static COLON_FLAGS: LazyLock> = LazyLock::new(|| { let mut flags = vec![ FlagRule::new( FlagPattern::ExactlyWithColon("/std"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling)), ), FlagRule::new(FlagPattern::ExactlyWithColonOrSep("/Fe"), ArgumentKind::Output), ]; flags.sort_by_key(|b| std::cmp::Reverse(b.pattern.flag().len())); flags }); let matcher = FlagAnalyzer::new(&COLON_FLAGS); // Test colon-glued form let args = vec!["/std:c++20".to_string()]; let result = matcher.match_flag(&args).unwrap(); assert_eq!(result.consumed_count, 1); // Test that plain /std does NOT match ExactlyWithColon let args = vec!["/std".to_string()]; let result = matcher.match_flag(&args); assert!(result.is_none()); // Test colon-or-sep glued form let args = vec!["/Fe:prog.exe".to_string()]; let result = matcher.match_flag(&args).unwrap(); assert_eq!(result.consumed_count, 1); // Test colon-or-sep separate form let args = vec!["/Fe".to_string(), "prog.exe".to_string()]; let result = matcher.match_flag(&args).unwrap(); assert_eq!(result.consumed_count, 2); } #[test] fn test_no_match() { let matcher = FlagAnalyzer::new(test_flags()); let args = vec!["--unknown".to_string()]; let result = matcher.match_flag(&args); assert!(result.is_none()); } } rizsotto-Bear-0c30b27/crates/semantic/src/interpreters/matchers/source.rs000066400000000000000000000132711527177720300266740ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later use std::path::Path; #[derive(Copy, Clone, Debug, Eq, PartialEq)] enum FileKind { CFamilyHeader, CFamilySource, ObjCSource, CxxModule, OtherCompilable, } #[rustfmt::skip] fn file_kind(extension: &str) -> Option { match extension { // header files "h" | "hh" | "H" | "hp" | "hxx" | "hpp" | "HPP" | "h++" | "tcc" => Some(FileKind::CFamilyHeader), // C / C++ "c" | "C" | "cc" | "CC" | "c++" | "C++" | "cxx" | "cpp" | "cp" => Some(FileKind::CFamilySource), // ObjectiveC "m" | "mi" | "mm" | "M" | "mii" => Some(FileKind::ObjCSource), // C++20 module interfaces "cppm" | "ixx" | "mxx" | "ccm" | "cxxm" | "c++m" => Some(FileKind::CxxModule), // Preprocessed "i" | "ii" | // CUDA "cu" | // Assembly "s" | "S" | "sx" | "asm" | // Fortran "f" | "for" | "fpp" | "ftn" | "F" | "FOR" | "FPP" | "FTN" | "f90" | "f95" | "f03" | "f08" | "F90" | "F95" | "F03" | "F08" | // go "go" | // brig "brig" | // D "d" | "di" | "dd" | // Ada "ads" | "adb" | // Vala / Genie (valac translation units; .vapi/.gir are bindings, not TUs) "vala" | "gs" | // Swift "swift" => Some(FileKind::OtherCompilable), _ => None, } } pub fn looks_like_a_source_file(argument: &str) -> bool { argument.rsplit_once('.').is_some_and(|(_, extension)| file_kind(extension).is_some()) } /// True when the path's extension is a C-family header (.h, .hpp, ...). pub fn is_header_file(path: &Path) -> bool { matches!(kind_of(path), Some(FileKind::CFamilyHeader)) } /// True when the path is a C, C++, or Objective-C translation unit - the file /// types eligible to donate compile flags to a synthesized header entry. /// /// C++20 module interfaces (`.cppm`, `.ixx`, ...) are intentionally excluded: /// a module-interface compile carries module-specific flags (e.g. /// `--precompile`) that would be wrong to clone onto a plain header entry, so /// they never donate. pub fn is_c_family_source(path: &Path) -> bool { matches!(kind_of(path), Some(FileKind::CFamilySource | FileKind::ObjCSource)) } fn kind_of(path: &Path) -> Option { path.extension().and_then(|ext| ext.to_str()).and_then(file_kind) } #[cfg(test)] mod test { use super::*; // Requirements: recognition-cpp20-modules #[test] fn test_filenames() { assert!(looks_like_a_source_file("source.c")); assert!(looks_like_a_source_file("source.cpp")); assert!(looks_like_a_source_file("source.cxx")); assert!(looks_like_a_source_file("source.cc")); assert!(looks_like_a_source_file("source.h")); assert!(looks_like_a_source_file("source.hpp")); assert!(looks_like_a_source_file("mod.cppm")); assert!(looks_like_a_source_file("mod.ixx")); // Precompiled module artifacts must never be treated as sources. assert!(!looks_like_a_source_file("foo.pcm")); assert!(looks_like_a_source_file("source.vala")); assert!(looks_like_a_source_file("source.gs")); // .vapi/.gir are bindings consumed by valac, not translation units assert!(!looks_like_a_source_file("gio-2.0.vapi")); assert!(!looks_like_a_source_file("Gtk.gir")); assert!(looks_like_a_source_file("source.swift")); assert!(!looks_like_a_source_file("gcc")); assert!(!looks_like_a_source_file("clang")); assert!(!looks_like_a_source_file("-o")); assert!(!looks_like_a_source_file("-Wall")); assert!(!looks_like_a_source_file("/o")); } // Requirements: recognition-cpp20-modules #[test] fn test_file_kind() { let cases = [ ("h", Some(FileKind::CFamilyHeader)), ("hpp", Some(FileKind::CFamilyHeader)), ("cpp", Some(FileKind::CFamilySource)), ("C", Some(FileKind::CFamilySource)), ("c", Some(FileKind::CFamilySource)), ("mm", Some(FileKind::ObjCSource)), ("m", Some(FileKind::ObjCSource)), ("cppm", Some(FileKind::CxxModule)), ("ixx", Some(FileKind::CxxModule)), ("mxx", Some(FileKind::CxxModule)), ("ccm", Some(FileKind::CxxModule)), ("cxxm", Some(FileKind::CxxModule)), ("c++m", Some(FileKind::CxxModule)), ("swift", Some(FileKind::OtherCompilable)), ("cu", Some(FileKind::OtherCompilable)), ("f90", Some(FileKind::OtherCompilable)), ("o", None), ("vapi", None), ("pcm", None), ]; for (extension, expected) in cases { let sut = file_kind(extension); assert_eq!(sut, expected, "extension: {extension}"); } } // Requirements: recognition-cpp20-modules #[test] fn test_is_header_file_and_is_c_family_source() { let cases = [ ("util.h", true, false), ("main.c", false, true), ("a.mm", false, true), ("a.swift", false, false), ("a.o", false, false), // Module interfaces are recognized as sources (looks_like_a_source_file) // but are deliberately excluded as header-synthesis donors. ("mod.cppm", false, false), ]; for (path, expected_header, expected_source) in cases { let sut = std::path::Path::new(path); assert_eq!(is_header_file(sut), expected_header, "path: {path}"); assert_eq!(is_c_family_source(sut), expected_source, "path: {path}"); } } } rizsotto-Bear-0c30b27/crates/semantic/src/interpreters/mod.rs000066400000000000000000000134771527177720300243550ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! This module provides the main entry point for creating interpreters to //! recognize compiler calls. The driver supplies the already-resolved //! parameters, and this module sets up the interpreter chain to include or //! exclude specific compilers. mod combinators; pub(crate) mod compilers; mod ignore; pub(crate) mod matchers; use super::Interpreter; use combinators::{Any, InputLogger, OutputLogger}; use compilers::CompilerInterpreter; use compilers::compiler_recognition::CompilerHints; use ignore::IgnoreByPath; use std::path::PathBuf; /// Creates an interpreter to recognize the compiler calls. /// /// The chain composition is a semantic concern, so it stays here rather than /// in the driver: `Any`, the loggers and `IgnoreByPath` remain private. /// The driver decides only what goes in, in semantic terms: /// /// * `hints` - classification overrides for known compiler paths /// * `ignored` - paths whose invocations are dropped, matched by filename /// * `from_response_files` - inline `@file` references before parsing /// * `from_environment` - fold compiler environment variables into arguments /// /// The interpreter chain is built as follows: /// 1. Generic programs to exclude /// 2. Compilers specified to exclude /// 3. All other compilers to include pub fn create<'a>( hints: CompilerHints, ignored: Vec, from_response_files: bool, from_environment: bool, ) -> impl Interpreter + 'a { // Build the base interpreter chain let mut interpreters: Vec> = vec![ // ignore executables which are not compilers, Box::new(OutputLogger::new(IgnoreByPath::default(), "coreutils_to_ignore")), ]; if !ignored.is_empty() { let tool = OutputLogger::new(IgnoreByPath::from(ignored), "compilers_to_ignore"); interpreters.push(Box::new(tool)); } // Add compiler interpreter that handles recognition and delegation let tool = CompilerInterpreter::new_with_format(hints, from_response_files, from_environment); interpreters.push(Box::new(tool)); // The outer interpreter is logging the inputs InputLogger::new(Any::new(interpreters)) } #[cfg(test)] mod test { use std::collections::HashMap; use std::path::PathBuf; use super::*; use crate::RecognizeResult; use intercept::Execution; /// The default wiring: no hints, nothing ignored, and the default /// argument handling (`format.arguments`: response-file inlining off, /// environment-flag folding on). fn create_default<'a>() -> impl Interpreter + 'a { create(CompilerHints::new(), vec![], false, true) } #[test] fn test_create_interpreter_with_default_config() { let _ = create_default(); } #[test] fn test_create_interpreter_recognizes_compiler() { let interpreter = create_default(); let execution = Execution::from_strings( "/usr/bin/gcc", vec!["gcc", "-c", "-Wall", "main.c"], "/home/user", HashMap::new(), ); assert!(matches!(interpreter.recognize(execution), RecognizeResult::Recognized(_))); } #[test] fn test_create_interpreter_ignores_coreutils() { let interpreter = create_default(); let execution = Execution::from_strings("/usr/bin/ls", vec!["ls", "-la"], "/home/user", HashMap::new()); assert!(matches!(interpreter.recognize(execution), RecognizeResult::Ignored(_))); } #[test] fn test_create_interpreter_with_compilers_to_exclude() { let interpreter = create(CompilerHints::new(), vec![PathBuf::from("/usr/bin/gcc")], false, true); let execution = Execution::from_strings( "/usr/bin/gcc", vec!["gcc", "-c", "main.c"], "/home/user", HashMap::new(), ); assert!(matches!(interpreter.recognize(execution), RecognizeResult::Ignored(_))); } #[test] fn test_compilers_to_exclude_ignores_only_flagged() { // Only the ignored path reaches `create`; the other configured // compiler contributes a hint, which does not suppress recognition. let mut hints = CompilerHints::new(); hints.add(&PathBuf::from("/usr/bin/clang"), None).unwrap(); let interpreter = create(hints, vec![PathBuf::from("/usr/bin/gcc")], false, true); let gcc = Execution::from_strings("/usr/bin/gcc", vec!["gcc", "-c", "test.c"], "/tmp", HashMap::new()); assert!(matches!(interpreter.recognize(gcc), RecognizeResult::Ignored(_))); let clang = Execution::from_strings("/usr/bin/clang", vec!["clang", "-c", "test.c"], "/tmp", HashMap::new()); assert!(matches!(interpreter.recognize(clang), RecognizeResult::Recognized(_))); } #[test] fn test_windows_gcc_exe_regression() { let interpreter = create_default(); let execution = Execution::from_strings( "gcc.exe", vec!["gcc.exe", "-fplugin=libexample.so", "-c", "test.c"], "/tmp", HashMap::new(), ); assert!(matches!(interpreter.recognize(execution), RecognizeResult::Recognized(_))); } #[test] fn test_various_windows_exe_compilers() { let interpreter = create_default(); let test_cases = vec!["gcc.exe", "g++.exe", "clang.exe", "clang++.exe", "gfortran.exe", "nvcc.exe"]; for executable_name in test_cases { let execution = Execution::from_strings( executable_name, vec![executable_name, "-c", "test.c"], "/tmp", HashMap::new(), ); assert!( matches!(interpreter.recognize(execution), RecognizeResult::Recognized(_)), "{} should be recognized", executable_name ); } } } rizsotto-Bear-0c30b27/crates/semantic/src/lib.rs000066400000000000000000000212211527177720300216000ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later //! Compiler recognition and command line flag parsing. //! //! Decides whether an intercepted [`Execution`] is a compiler invocation, and //! if so parses its command line into classified arguments. This crate is the //! analysis core: it knows about compilers, not about how Bear intercepts a //! build or how it writes its output. //! //! # Pipeline //! //! 1. **Recognition** ([`Interpreter`]) - classifies an [`Execution`] as a //! recognized compiler call, an ignored program, or neither //! ([`RecognizeResult`]). Build the configured chain with //! [`interpreters::create`]. //! //! 2. **Parsing** - a recognized call becomes a [`Command`]: its source files, //! its [`CompilerPass`], and its [`Argument`] list. //! //! # Core types //! //! - [`Command`] - a structured compiler invocation //! - [`Argument`] - one command line argument with its classification //! - [`ArgumentKind`] - what an argument means to the compiler //! - [`PassEffect`] - how an argument affects the compilation pipeline //! - [`CompilerHints`] - caller-supplied overrides for which executables are //! which compiler, built from the `compilers:` configuration section pub mod interpreters; /// Command factories and comparison helpers for tests. Compiled only for this /// crate's own tests and for dependents that turn on the `testing` feature. #[cfg(any(test, feature = "testing"))] pub mod testing; pub use interpreters::compilers::compiler_recognition::{CompilerHints, CompilerRecognizer}; pub use interpreters::compilers::identity::SpellingError; pub use interpreters::compilers::print_compilers; pub use interpreters::matchers::{is_c_family_source, is_header_file, looks_like_a_source_file}; use intercept::Execution; use std::borrow::Cow; use std::path::{Path, PathBuf}; /// Responsible for recognizing the semantic meaning of an executed command. /// /// `Send` because the interpreter is moved into the consumer thread that /// drains the interception channel (see `modes::execution::Interceptor`). /// Recognition runs on that one thread only, so `Sync` is deliberately /// not required: implementations may use single-threaded interior /// mutability (e.g. `RefCell`) for caches. #[cfg_attr(test, mockall::automock)] pub trait Interpreter: Send { fn recognize(&self, execution: Execution) -> RecognizeResult; } /// Result of semantic recognition of an executed command. #[derive(Debug)] pub enum RecognizeResult { /// A recognized compiler invocation with parsed, classified arguments. Recognized(Command), /// A command that is intentionally ignored (e.g. coreutils, excluded compilers). Ignored(&'static str), /// The interpreter did not recognize this execution. Ownership is returned. NotRecognized(Execution), } /// Represents a full compiler command invocation. #[derive(Debug)] pub struct Command { pub working_dir: PathBuf, pub executable: PathBuf, pub arguments: Vec, pub source_mode: SourceMode, } /// A compiler command-line argument with semantic classification. #[derive(Debug, Clone, PartialEq)] pub enum Argument { /// Flags and other non-file arguments (e.g. `-c`, `-Wall`, `-I /usr/include`). Other { arguments: Vec, kind: ArgumentKind }, /// A source or object file argument. Source { path: String, binary: bool }, /// An output file argument (e.g. `-o main.o`). The flag and the path are /// stored apart so the path stays individually addressable, and /// [`OutputSpelling`] remembers how the build wrote them so they can be /// spelled back the way the compiler accepts. Output { flag: String, path: String, spelling: OutputSpelling }, } /// How an output flag and its value were written on the observed command line. /// /// Compilers do not all accept the same spelling: `cl.exe` documents that /// `/Fo` takes no space before its value, and TI's long options glue their /// value after `=`. Re-spelling one form as another produces a database entry /// the compiler would reject, so the observed form is kept. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum OutputSpelling { /// Two tokens: `-o main.o`. Separate, /// One token, value glued straight onto the flag: `-omain.o`, `/FoX.obj`. Glued, /// One token, flag and value joined by `=`: `--output_file=x.obj`. Equals, /// One token, flag and value joined by `:`: `/Fo:X.obj`. Colon, } impl OutputSpelling { /// Writes `flag` and `path` back as the command line tokens this spelling /// stands for: one joined token, or the two the build passed separately. fn as_arguments(self, flag: &str, path: &Path) -> Vec { let path = path.to_string_lossy(); match self { Self::Separate => vec![flag.to_string(), path.to_string()], Self::Glued => vec![format!("{flag}{path}")], Self::Equals => vec![format!("{flag}={path}")], Self::Colon => vec![format!("{flag}:{path}")], } } } impl Argument { /// Creates a Source variant, auto-detecting binary vs compilable from extension. pub fn new_source(path: String) -> Self { let binary = !looks_like_a_source_file(&path); Self::Source { path, binary } } pub fn kind(&self) -> ArgumentKind { match self { Self::Other { kind, .. } => *kind, Self::Source { binary, .. } => ArgumentKind::Source { binary: *binary }, Self::Output { .. } => ArgumentKind::Output, } } pub fn as_arguments(&self, path_updater: &dyn Fn(&Path) -> Cow) -> Vec { match self { Self::Other { arguments, .. } => arguments.clone(), Self::Source { path, .. } => { let p = Path::new(path); let updated = path_updater(p); vec![updated.to_string_lossy().to_string()] } Self::Output { flag, path, spelling } => { let updated = path_updater(Path::new(path)); spelling.as_arguments(flag, &updated) } } } pub fn as_file(&self, path_updater: &dyn Fn(&Path) -> Cow) -> Option { match self { Self::Other { .. } => None, Self::Source { path, .. } => { let p = Path::new(path); Some(path_updater(p).to_path_buf()) } Self::Output { path, .. } => { let p = Path::new(path); Some(path_updater(p).to_path_buf()) } } } } /// Represents the meaning of the argument in the compiler call. #[derive(Clone, Copy, Debug, PartialEq)] pub enum ArgumentKind { Compiler, Source { binary: bool }, Output, Other(PassEffect), } /// Represents how an argument affects the compilation pipeline. #[derive(Clone, Copy, Debug, PartialEq)] pub enum PassEffect { Configures(CompilerPass), StopsAt(CompilerPass), InfoAndExit, DriverOption, /// Indicates remaining arguments should be passed through without interpretation. /// Used for flags like MSVC's `/link` that forward all subsequent args to a different tool. PassThrough, None, } /// Represents different compiler passes that an argument might affect. #[derive(Clone, Copy, Debug, PartialEq)] pub enum CompilerPass { Preprocessing, Compiling, Assembling, Linking, } /// How the sources named in one invocation map to compilation database /// entries. Set per compiler family by the family YAML's top-level /// `source_mode:` key (default `per-source-stripped`). It is not used at /// parse time; the converter consumes it (post-parse). /// See the `output-compilation-entries` requirement for the full contract. #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum SourceMode { /// Each source is a separable translation unit: the converter emits one /// entry per source, and each entry's arguments keep only that source /// (sibling sources in the same invocation are stripped). The default /// for GCC, Clang, and most other families. PerSourceStripped, /// Every source in the invocation is analyzed together as a single /// "whole module", but per-file consuming tooling still looks up a /// compile command by file path: the converter emits one entry per /// source, and every entry's arguments are the complete invocation (no /// sibling stripping). Used by Swift's whole-module compilation /// (`swiftc`). PerSourceFull, /// All sources in the invocation form a single translation unit and /// produce one combined output: the converter emits exactly one entry /// per invocation, with `file` set to the first source and every source /// retained. Used by `valac`. Combined, } rizsotto-Bear-0c30b27/crates/semantic/src/testing.rs000066400000000000000000000156731527177720300225250ustar00rootroot00000000000000// SPDX-License-Identifier: GPL-3.0-or-later use super::{Argument, ArgumentKind, Command, OutputSpelling, SourceMode}; use std::path::PathBuf; impl Command { /// Create a Command from string arguments for testing. /// /// An `ArgumentKind::Output` entry spelled as two tokens (`["-o", "a.o"]`) /// becomes the separate form; one token becomes the glued form, which is /// the spelling that token would re-emit as. pub fn from_strings( working_dir: &str, executable: &str, arguments: Vec<(ArgumentKind, Vec<&str>)>, ) -> Self { Self { working_dir: PathBuf::from(working_dir), executable: PathBuf::from(executable), source_mode: SourceMode::PerSourceStripped, arguments: arguments .into_iter() .map(|(kind, args)| match kind { ArgumentKind::Source { binary } => Argument::Source { path: args[0].to_string(), binary }, ArgumentKind::Output => Argument::Output { flag: args[0].to_string(), path: args.get(1).unwrap_or(&"").to_string(), spelling: if args.len() > 1 { OutputSpelling::Separate } else { OutputSpelling::Glued }, }, other_kind => Argument::Other { arguments: args.into_iter().map(String::from).collect(), kind: other_kind, }, }) .collect(), } } /// Compare two CompilerCommands by their arguments for testing. pub fn has_same_arguments(&self, other: &Command) -> bool { self.arguments == other.arguments } } #[cfg(test)] mod tests { use super::*; use crate::PassEffect; use std::borrow::Cow; use std::path::Path; #[test] fn test_compiler_command_comparison() { let cmd1 = Command::from_strings( "/home/user", "/usr/bin/gcc", vec![ (ArgumentKind::Source { binary: false }, vec!["main.c"]), (ArgumentKind::Output, vec!["-o", "main.o"]), ], ); let cmd2 = Command::from_strings( "/home/user", "/usr/bin/gcc", vec![ (ArgumentKind::Source { binary: false }, vec!["main.c"]), (ArgumentKind::Output, vec!["-o", "main.o"]), ], ); let cmd3 = Command::from_strings( "/home/user", "/usr/bin/gcc", vec![ (ArgumentKind::Source { binary: false }, vec!["other.c"]), (ArgumentKind::Output, vec!["-o", "other.o"]), ], ); assert!(cmd1.has_same_arguments(&cmd2)); assert!(!cmd1.has_same_arguments(&cmd3)); } #[test] fn test_arguments_with_different_kinds() { let cmd1 = Command::from_strings( "/home/user", "/usr/bin/gcc", vec![(ArgumentKind::Source { binary: false }, vec!["main.c"])], ); let cmd2 = Command::from_strings("/home/user", "/usr/bin/gcc", vec![(ArgumentKind::Output, vec!["main.c"])]); assert!(!cmd1.has_same_arguments(&cmd2)); } #[test] fn test_arguments_with_different_lengths() { let cmd1 = Command::from_strings( "/home/user", "/usr/bin/gcc", vec![ (ArgumentKind::Source { binary: false }, vec!["main.c"]), (ArgumentKind::Output, vec!["-o", "main.o"]), ], ); let cmd2 = Command::from_strings( "/home/user", "/usr/bin/gcc", vec![(ArgumentKind::Source { binary: false }, vec!["main.c"])], ); assert!(!cmd1.has_same_arguments(&cmd2)); } #[test] fn test_argument_enum_implementations() { let source_arg = Argument::Source { path: "main.c".to_string(), binary: false }; let output_arg = Argument::Output { flag: "-o".to_string(), path: "main.o".to_string(), spelling: OutputSpelling::Separate, }; let other_arg = Argument::Other { arguments: vec!["-Wall".to_string()], kind: ArgumentKind::Other(PassEffect::None), }; assert_eq!(source_arg.kind(), ArgumentKind::Source { binary: false }); assert_eq!(output_arg.kind(), ArgumentKind::Output); assert_eq!(other_arg.kind(), ArgumentKind::Other(PassEffect::None)); let path_updater: &dyn Fn(&Path) -> Cow = &|path: &Path| Cow::Borrowed(path); assert_eq!(source_arg.as_arguments(path_updater), vec!["main.c"]); assert_eq!(output_arg.as_arguments(path_updater), vec!["-o", "main.o"]); assert_eq!(other_arg.as_arguments(path_updater), vec!["-Wall"]); assert_eq!(source_arg.as_file(path_updater), Some(PathBuf::from("main.c"))); assert_eq!(output_arg.as_file(path_updater), Some(PathBuf::from("main.o"))); assert_eq!(other_arg.as_file(path_updater), None); } /// Each spelling writes back the tokens it stands for: re-joining a glued /// or `=`/`:` written value as two tokens would produce a command line the /// originating compiler rejects. #[test] fn output_spelling_writes_the_tokens_it_stands_for() { // arrange let cases = [ (OutputSpelling::Separate, "-o", vec!["-o", "main.o"]), (OutputSpelling::Glued, "-o", vec!["-omain.o"]), (OutputSpelling::Equals, "--output_file", vec!["--output_file=main.o"]), (OutputSpelling::Colon, "/Fo", vec!["/Fo:main.o"]), ]; for (spelling, flag, expected) in cases { // act let sut = spelling.as_arguments(flag, Path::new("main.o")); // assert assert_eq!(sut, expected, "case {:?}", spelling); } } /// The path stays individually addressable inside a glued token: the /// updater rewrites the path and the flag is left alone. #[test] fn output_argument_path_updater_reaches_a_glued_path() { // arrange let sut = Argument::Output { flag: "-o".to_string(), path: "main.o".to_string(), spelling: OutputSpelling::Glued, }; let path_updater: &dyn Fn(&Path) -> Cow = &|path: &Path| Cow::Owned(Path::new("/build").join(path)); // Spelled with the platform's own separator: `Path::join` writes a // backslash on Windows, and the point of the test is where the path // lands, not how the platform spells it. let rewritten = Path::new("/build").join("main.o"); // act let actual = sut.as_arguments(path_updater); // assert assert_eq!(actual, vec![format!("-o{}", rewritten.display())]); assert_eq!(sut.as_file(path_updater), Some(rewritten)); } } rizsotto-Bear-0c30b27/docs/000077500000000000000000000000001527177720300155435ustar00rootroot00000000000000rizsotto-Bear-0c30b27/docs/rationale/000077500000000000000000000000001527177720300175215ustar00rootroot00000000000000rizsotto-Bear-0c30b27/docs/rationale/CLAUDE.md000066400000000000000000000037321527177720300210050ustar00rootroot00000000000000## Rationale directory Each file here is one design decision: the *why* behind a part of Bear. ## When to write an entry Write one when a decision is not obvious and a future reader would otherwise have to rebuild the reasoning. Typically: - You chose one approach among several with real trade-offs, and the code does not make the choice self-evident. - You rejected an alternative that someone will propose again. - Outside research (a compiler quirk, a kernel behavior, a file format) shaped the decision and would otherwise be lost in a PR thread. Does not belong here: - A detail obvious from the code. That goes in a code comment. - A restatement of an acceptance criterion. Link the requirement instead. - To-dos and open work. Those go in the task tracker. ## Files A file is named `.md`. The name without `.md` is the entry's ID. No number prefix: git history gives the order, and a name links better than `0007`. Fill [`TEMPLATE.md`](TEMPLATE.md) for the format. When a decision changes, rewrite the entry in place so it states the current decision. Do not add "Update:" notes. Keep the filename so links still resolve. Git history keeps the old version. ## Linking Every reference in an entry, in the References section and in prose, is a markdown link with a relative path: `[output-append](../requirements/output-append.md)` for a requirement, `[parse-sh-producer](parse-sh-producer.md)` for another entry. Issues and outside docs get a link to their URL. An entry links each requirement it supports. Add the link back in that requirement's Rationale section in the same change. ## Entry vs rejected requirement An idea declined before it had a requirement file goes straight to an entry here. It never was a contract, so it gets no requirement file. A requirement file that already exists stays when the user declines it or it is withdrawn. Its status becomes `rejected`, and the file is a tombstone that may link an entry here for the full reasoning. rizsotto-Bear-0c30b27/docs/rationale/TEMPLATE.md000066400000000000000000000016231527177720300212600ustar00rootroot00000000000000# <!-- Copy to <short-kebab-case-title>.md. No front matter. Keep it short. --> ## Context The problem and the forces at play: constraints and what we knew. Enough that a reader who was not in the room can judge the decision. The options considered, each with the reason it was not chosen. This is the one place for rejected alternatives. Do not add a separate section for them. ## Decision What we chose, in one or two plain sentences. ## Consequences What this makes easy, what it makes hard, what it puts out of scope, and when to revisit. ## References - [<requirement-id>](../requirements/<requirement-id>.md), the requirement this supports - [<entry-id>](<entry-id>.md), a related entry - [#NNN](https://github.com/rizsotto/Bear/issues/NNN), an issue or an outside doc Link source files only when there is no other way. Paths drift, and entries should outlast them. �������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/rationale/ambiguous-cc-version-probe.md����������������������������������0000664�0000000�0000000�00000010236�15271777203�0025213�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Disambiguating cc/c++: a cached version probe as sole classifier ## Context `cc` and `c++` name different toolchains per OS - GCC on most Linux, Clang on the BSDs and macOS. Misclassifying them corrupts the database silently, because GCC and Clang have different flag-arity tables (e.g. Clang's `-Xclang <arg>` consumes the next argv slot and GCC's does not), so source/output detection mis-parses. A masquerade link (`/usr/lib/ccache/cc` -> `ccache`) adds a twist. It canonicalizes to the wrapper, but the recognition regex sees only the invoked basename (`cc`), and the launcher interpreter cannot unwrap it, because argv has no inner compiler token. If the probe does not classify it, the event is dropped. A masquerade binary answers `--version` in the name it was called by and passes the flag through to the underlying compiler. Its own banner appears only when it is invoked as itself, which is never an ambiguous name. Options considered: - **Per-OS defaults**: hard-code "cc means Clang on BSD/macOS, GCC elsewhere". Rejected: wrong whenever a host installs the other toolchain under `cc`, which is exactly the case users hit. - **Regex with a probe fallback (probe-then-regex)**: on probe failure, fall back to the recognition regex, which defaults `cc` to GCC. Rejected: it re-introduced the very bug on the platforms the probe was meant to fix. - **Per-invocation probe** ([PR #695](https://github.com/rizsotto/Bear/pull/695)): run `--version` on every invocation. Rejected: it costs a `fork+exec` per compiler call, where a cache costs one or two per build. - **Do not probe masquerade links**, for fear of the wrapper's own banner. Rejected: the fear is unfounded (see above), and the events would be silently dropped. - **Cached version probe as the sole classifier**: run the executable once with `--version`, cache the result per canonical path, and do not fall back to the regex for ambiguous names. Chosen. ## Decision Use the cached version probe as the sole classifier for ambiguous names. The recognition regex deliberately does not list `cc`/`c++`, so when the probe declines (timeout, unrecognizable output, failed spawn, non-zero exit) recognition returns `NotRecognized` rather than guessing. A masquerade link is probed as invoked and cached under the invoked path, because its answer depends on the name it was called by. ## Consequences - The failure mode is **loud, not wrong**: a missing entry is visible and debuggable; a mis-classified entry corrupts the database silently. The GCC compiler definition carries a comment recording why `cc`/`c++` are absent from its recognize list. - The user override is per-path and explicit: declaring a path under `compilers:` takes priority over the probe and is the only way to recover recognition for a quirky `cc` whose `--version` output the probe cannot read. There is no process-wide off switch, by request. - Caching per canonical path keeps the cost at one or two `fork+exec` pairs per clean build rather than per invocation. Masquerade links are the one exception: the canonical key would collapse different masquerade names (`cc`, `c++`) onto the wrapper binary whose behavior depends on argv[0], so those cache under the invoked path. - Probing through a masquerade link keeps the loud-not-wrong property: unrecognizable passthrough output (an exotic wrapper printing its own banner) still declines. - The ambiguous-names list is intentionally minimal (`cc`, `c++`, `CC`). Cross-prefixed variants like `aarch64-linux-gnu-cc` are not probed and not recognized: the probe matches the exact name, and the GCC definition does not list `cc`. The user sets their family in the configuration. The list can grow if a real cross-toolchain case appears. - The HPE Cray PrgEnv wrapper `CC` is probed for the same reason as `cc`. A static mapping would be wrong in every programming environment but the one it picked, and on a case-insensitive filesystem it would shadow `cc`. ## References - [recognition-ambiguous-name-probe](../requirements/recognition-ambiguous-name-probe.md), the requirement this supports - [PR #695](https://github.com/rizsotto/Bear/pull/695), the per-invocation probe variant, not taken ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/rationale/append-default-migration.md������������������������������������0000664�0000000�0000000�00000006604�15271777203�0024731�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Naming the overwrite behaviour before it stops being the default ## Context Bear overwrites `compile_commands.json` by default and accumulates only when asked. That default has one bad failure mode: a run that produces no compiler invocations, because the build was already up to date or because interception failed, replaces a good database with an empty one. The user loses working data by running the tool successfully. The intended fix is to make accumulation the default, so a run that captures nothing leaves the previous database alone. Flipping a default is a compatibility break, and the scripts it breaks are exactly those that never named the behaviour they depend on: a packaging script or a CI job that runs Bear plainly and expects a fresh database each time has no flag to point at. Nothing in the current command line lets such a script say what it means. Until one exists, the break has no migration path at all - the advice would be "wait for the new release, then edit your scripts", with a window in which correct scripts are silently wrong. So the flag has to exist before the default moves, which means shipping a flag that changes nothing. Its whole value is time in the wild: it must land early enough that a script can adopt it, be released, and reach users, all before the default flips. Three naming options were on the table. `--no-append` pairs with the existing flag and needs no new concept, but it names the new behaviour as the absence of the old one, which reads backwards once accumulation is the default. `--truncate` is the most precise verb, since it describes what happens to the file rather than to the entries, but no build tool spells it that way and it suggests a maintenance operation on the database. `--overwrite` is what `compiledb` and `kubectl` use for the same idea, so it is the spelling a user is most likely to guess. ## Decision Ship `--overwrite` one release ahead of the default change, accepted wherever `--append` is accepted and documented as naming the current default. Reject the two together as a usage error rather than giving one precedence over the other. There is no short form. `-o` is Bear's output path, so the letter `compiledb` uses is unavailable, and inventing a different letter would give users a short flag to misremember. ## Consequences A script can become forward-compatible today, and it keeps working unchanged across the default change. `--append` stays accepted permanently afterwards, naming the new default, so a script that never adapts also keeps working. The only invocations whose behaviour changes are those that relied on the unnamed default. Rejecting the combination costs a script that passes both flags an immediate failure. That is the point: the two flags state opposite intents, and a precedence rule would let the contradiction survive the default change, where it would quietly start losing entries or quietly start keeping stale ones depending on which rule was chosen. The cost is a release in which a documented flag does nothing. A reader of the code will find a parsed argument that is never inspected, which looks like a bug; the parsing site carries a comment saying otherwise. Revisit when the default flips: at that point `--overwrite` becomes load-bearing, and this entry becomes the record of why it predates the behaviour it selects. ## References - [output-append](../requirements/output-append.md), the requirement this supports ����������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/rationale/compilation-entry-shapes.md������������������������������������0000664�0000000�0000000�00000003636�15271777203�0025011�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Entry shapes for clang tooling ## Context Build systems often compile several sources in one compiler call, or compile and link in one command. Tools that read the compilation database (clangd, clang-tidy, and similar) look up a compile command by file path and expect it to compile that one file on its own. Link flags and sibling sources in that command are noise to them. These tools are also order-sensitive. Include search paths, macro definitions, and warning options depend on their relative order, and a `-x <lang>` override applies to the sources after it. Reordering the arguments would change what the command means. Not every compiler fits the one-source-per-entry shape. Vala's `valac` compiles all sources of a target as one translation unit and produces one combined output. Swift's `swiftc` in whole-module mode analyzes all sources together, but editors still need a command per file. ## Decision By default, one entry per source, with the sibling sources and the link-only flags removed and the rest kept in the original order. A single-translation-unit compiler gets one entry per invocation, keyed on its first source, with every source kept. A whole-module compiler gets one entry per source, each with the whole invocation's sources. ## Consequences For a single-translation-unit compiler, duplicate detection sees one entry per invocation, not several entries that all claim the same combined output. Only the first source of such an invocation can be looked up by path. A new compiler family must pick its shape from how its consumers look up files. The Swift trade-offs are in [swift-whole-module-entries](swift-whole-module-entries.md). ## References - [output-compilation-entries](../requirements/output-compilation-entries.md), the requirement this supports - [vala-transpiler-database](vala-transpiler-database.md), the Vala case - [swift-whole-module-entries](swift-whole-module-entries.md), the Swift case ��������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/rationale/compiler-env-flag-parsing.md�����������������������������������0000664�0000000�0000000�00000003333�15271777203�0025015�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Compiler env var parsing: whitespace split, no shell parser ## Context Compiler environment variables often carry a trailing flag or two (`CC="gcc -std=c11"`). To register a wrapper, Bear must separate the program token from the flags. The supported shape is narrow. The common Make/Autoconf convention is effectively `$(firstword $(CC))` plus the rest as flags. Anything more elaborate (flags with embedded whitespace, quoting, metacharacters, command substitution) is explicitly redirected to `CFLAGS` / `CXXFLAGS` / `LDFLAGS`, so the parser does not need to handle it. Options considered: - **Plain whitespace split**: first token is the program, the rest are flags. Chosen. - **POSIX shell-word parsing (`shell_words::split`)**: handles quoted program names and paths containing spaces. Rejected: it adds quote-aware edge cases (malformed quotes, backslash-as-escape on Windows paths) and test surface with no matching user need. - **Full shell expansion (`sh -c 'command -v "$CC"'`)**: resolves the value exactly as the build system's shell would. Rejected: it spawns a shell for a result that whitespace splitting already gives. ## Decision Split the value on whitespace and go no further. No shell parser, no subprocess. ## Consequences Whitespace splitting gives exactly the shape the contract supports, with no parser dependency and no subprocess. A value with quoting or embedded whitespace is not split the way a shell would split it. If a real need for richer parsing appears, this is the decision to revisit. ## References - [interception-cc-env-var-flags](../requirements/interception-cc-env-var-flags.md), the requirement this supports - [#686](https://github.com/rizsotto/Bear/issues/686), bare-name CC resolution �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/rationale/compiler-family-definition.md����������������������������������0000664�0000000�0000000�00000010333�15271777203�0025262�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# A compiler family is defined by its YAML file ## Context Bear defines each compiler family and launcher in a YAML file, and codegen turns the files into static tables at build time. The YAML always held the flag tables. The rest of a family was spread across hand-written Rust: a list of the files, a `CompilerType` enum with one variant per family (the config `as:` surface), a string-to-enum map that panicked on an unknown value, a constructor and a registration call per family, and the `source_mode` and response-file syntax set inside those constructors. Adding a family took six edits across two crates. Each missed edit failed in its own way: a silent drop, a compile error, or a runtime panic. The config `as:` value had its own problem. The enum took many spellings per family through hand-written serde aliases, plus whatever `rename_all = "lowercase"` made of a variant name (`clangcl` for `ClangCl`). Nobody designed that surface. It grew as families were added, and there was no record of who used which spelling. Options we did not take: - **Keep the per-family enum.** It was a hand-synced copy of the ids codegen already emits. Its one reason to exist was the alias surface, which this decision drops. Dispatch never relied on Rust exhaustiveness: it is a runtime registry with a completeness test. - **Generate the enum from the data.** Codegen would learn an id-to-variant mapping, and the enum would keep its per-variant ceremony while still copying the data. Dropping the type is simpler. - **A YAML `aliases:` list per family.** It generates a bigger copy of a surface nobody designed. No user had asked for any alias. - **Hyphenated ids for display** (`clang-cl` for `clang_cl` in `--print-compilers`). Once there are no aliases, any other display prints a value that differs from the one users must type. - **A recognition `priority` field.** The only real order rule is "a specialization before its base", and that is the `extends` link a vendor variant declares anyway for flag inheritance. A separate number would be one more thing to forget. ## Decision A compiler family is defined only by its YAML file. Codegen finds the files by scanning the directory, derives every generated name from the file stem, and emits the family registry and the id list. `CompilerType` has two variants: a compiler carrying an id checked against the generated list, or a wrapper. A family that `extends` another is recognized before it. `source_mode` and `response_file_syntax` are YAML data; what each value means stays in code. Each family accepts one `as:` spelling: its `compiler.id`, verbatim, in the YAML, the generated data, `--print-compilers`, and config parsing. A launcher has no `compiler.id`, so its basename is the accepted spelling. The basenames are generated from the launcher YAML too, and resolve to the wrapper kind like `wrapper` does. The config schema keeps the string the user wrote. Bear resolves it once at startup, in every mode that reads configuration, against the generated ids. So a typo fails before any build runs or input is read, and the error lists every accepted value. ## Consequences - Adding a family or a launcher is a YAML file plus accepted snapshots. No Rust edit, and no parallel list left to drift. - The `as` column of `--print-compilers` is exactly what a user types in `as:`. There is no display transform to keep in sync. - Configs that used a dropped alias (`llvm`, `gnu`, `clangcl`, `intel-cc`, and others) now fail at startup. - What stays code, on purpose: the meaning of each `source_mode` and response-file syntax value, the `cc`/`c++` version probe, the ambiguous-name set, and the source-extension table. These are behavior, not per-family data. - Revisit only with real user demand for a second spelling. Both aliases and display spellings were drafted in full and dropped. ## References - [recognition-compiler-names](../requirements/recognition-compiler-names.md), the contract over whatever the data defines - [recognition-compiler-launchers](../requirements/recognition-compiler-launchers.md), the launcher contract over the launcher data - [workspace-crate-layout](workspace-crate-layout.md), why the conversion from config string to compiler identity lives in the application crate �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/rationale/completions-own-crate.md���������������������������������������0000664�0000000�0000000�00000002153�15271777203�0024275�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# The completions generator is its own crate ## Context Bear ships shell-completion scripts. A small binary writes them from the `clap` definitions of the command line, using `clap_complete`. Nothing else in Bear needs `clap_complete`. The obvious place for that binary is the application crate, next to the `clap` definitions it reads. But a binary inside that crate puts `clap_complete` in the crate's dependency graph for every build. That includes the driver and wrapper binaries users run, which never need it, and it adds compile time to each of them. ## Decision The generator lives in its own crate. It depends on the application crate for the `clap` builder, and nothing depends on it. ## Consequences - `clap_complete` is built only when the generator is built. - The dependency edge points one way, from the generator to the application crate. - A plain `cargo build` of the main install does not make completions. Making them is a separate step. ## References - [CLAUDE.md](../../CLAUDE.md), the crate roles - [bear-completions/CLAUDE.md](../../crates/bear-completions/CLAUDE.md), how the generator is run ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/rationale/docs-site-over-wiki.md�����������������������������������������0000664�0000000�0000000�00000010245�15271777203�0023651�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# A documentation site instead of the wiki ## Context Bear's user documentation lived in the [GitHub wiki](https://github.com/rizsotto/Bear/wiki): 14 pages on installing, usage, configuration, platforms, troubleshooting, and an FAQ. The problem it did not solve is discovery. The query a likely user types ("generate compile_commands.json for a Makefile project") did not surface Bear at all. The wiki cannot fix that. Checked on 2026-07-06: wiki pages can be crawled (no `robots.txt` block, no `noindex`), but they cannot rank. The wiki gives no control over the HTML title, has no sitemap, cannot be a Google Search Console property, and builds no link authority of its own. It is also outside the pull-request flow, so it drifts from the code with nothing in CI to catch it. It still had 3.x-era text years after 4.x shipped. The forces on the replacement: the toolchain is Rust and CI already has cargo; the minimal-dependency rule applies to doc tooling too; the set of pages was small (about 15); and the wiki has inbound links from issues, forums and other projects' docs that must not start to 404. Options considered: - Expand the wiki. Cheapest to write, but it cannot be made to rank, and it stays outside code review. - In-repo Markdown only, no site. Free and reviewed, but `github.com/robots.txt` disallows `/*/tree/`, so search engines do not index those pages at all. - mkdocs-material. Better search and SEO features out of the box than mdBook, but it brings a Python toolchain and a dependency tree into a project that has neither. Not worth it for about 15 pages. - [mdBook](https://rust-lang.github.io/mdBook/), with no plugins. Chosen: one static binary from the Rust ecosystem, pinned in CI. For structure, the four [Diataxis](https://diataxis.fr/) types (tutorial, how-to, reference, explanation) were adopted. First the contents were organized purely by task, with the types only an authoring rule. That failed as pages grew: a reference page sat among how-tos and an explanation page sat next to a recipe, and readers could see the mismatch. So the types now separate the sections. Diataxis's own section names were rejected: "How-to guides" and "Explanation" are words for authors, not readers. Nobody arrives wanting an "Explanation". Section names also matter for search, because mdBook renders a page title as "<chapter name> - <book title>". ## Decision The user documentation is an mdBook book built from `site/` and served from GitHub Pages at <https://rizsotto.github.io/Bear/>. Each page is one Diataxis type, and the sections are named "Tutorials", "Guides", "Reference" and "Understanding Bear", plus "Platform notes". The wiki stays, with each page cut to a pointer to its replacement. Installing sits above the sections, ungrouped, because it comes before anything a reader can do with the tool. ## Consequences - Pages are reviewed with the code in pull requests. Every claim can be checked against the man page, the compiler snapshot, or a debug build before it ships. - The site controls its titles, sitemap and canonical URLs, and is a Search Console property. This makes ranking possible; it does not make it happen. No lift is assumed. - Documentation is a build step. A broken internal link or a page missing from `SUMMARY.md` fails CI. That cost is intended. - Wiki page titles stay, so old inbound links still land somewhere useful. Content is never forked between the wiki and the site. - One type per page forces splits a wiki page used to absorb. The old Usage page was a tutorial, several how-tos and a flag reference at once. - The reference section is generated where it can be: the command-line page from the binary's help, the compiler list from the compiler definitions. That is the only way a second copy of a contract stays true. `man/bear.1.md` stays the offline copy. ## References - [site/CLAUDE.md](../../site/CLAUDE.md), the authoring rules for the site - [structured-data-scope](structured-data-scope.md), what the site emits for search engines - [Diataxis](https://diataxis.fr/), the four documentation types - [mdBook](https://rust-lang.github.io/mdBook/), the site generator - [Bear wiki](https://github.com/rizsotto/Bear/wiki), the old location �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/rationale/duplicate-latest-flags-win.md����������������������������������0000664�0000000�0000000�00000006245�15271777203�0025203�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Latest compilation wins for a rebuilt file ## Context Bear is often run with `--append` for partial or incremental builds: each run adds the freshly compiled files to an existing `compile_commands.json`. GitHub discussion [#712](https://github.com/rizsotto/Bear/discussions/712) reported that when a file's compiler flags change between runs, the database ends up with two entries for that file - the stale one from the previous run and the new one - because the two behaviours in place at the time worked against the user: - Default duplicate detection matched on `directory`, `file` and `arguments`. Changed flags mean different arguments, so the new entry was not seen as a duplicate and both were kept. - Append emitted the existing database's entries first, so even when the flags were identical the first-occurrence rule kept the old entry. The build system could not force a clean rebuild, and deleting `compile_commands.json` before an append run defeats the point of appending. Two options were on the table: - **Opt-in `--update` flag** (the reporter's original request, and the shape of the withdrawn [PR #497](https://github.com/rizsotto/Bear/pull/497)): keep the current defaults and add a flag that replaces matching entries. Preserves today's behaviour for everyone, but every affected user has to discover and set it, and the stale-duplicate default is rarely what anyone actually wants. - **Change the default**: drop `arguments` from the default match set and emit new entries before existing ones, so the newest invocation of a file wins automatically, no flag required. ## Decision Change the default. Duplicate detection defaults to matching on `directory` and `file` only, and `--append` emits new entries ahead of the existing ones. Together with the existing first-occurrence rule, a file rebuilt with new flags has its entry replaced rather than duplicated. Recording several configurations for one file - the clang spec's stated reason for allowing repeats - remains available by adding `arguments` back to `duplicates.match_on`. ## Consequences Incremental `--append` builds self-heal when flags change: no manual deletion, no extra flag, and the database always reflects the latest build. The costs: - Within a single build, a file compiled twice from the same directory with different flags now collapses to one entry (the first seen), where before both were kept. This is rare and recoverable by widening `duplicates.match_on`. - Append output ordering changes (new entries first). Tools that assume insertion order may notice, though the compilation database format does not promise an order. Revisit if keeping per-configuration entries by default becomes a real need - that would argue for a distinct output mode rather than reverting the dedup default. ## References - [output-duplicate-detection](../requirements/output-duplicate-detection.md), a requirement this supports - [output-append](../requirements/output-append.md), a requirement this supports - [#712](https://github.com/rizsotto/Bear/discussions/712), the discussion that reported the stale duplicates - [PR #497](https://github.com/rizsotto/Bear/pull/497), the withdrawn opt-in `--update` flag �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/rationale/env-vars-rewritten-by-wrapper.md�������������������������������0000664�0000000�0000000�00000003453�15271777203�0025720�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Wrapper mechanism rewrites program variables only ## Context The wrapper mechanism points environment variables at the wrapper executables. Two kinds of variable could be in scope: variables that name a program (`CC`, `CXX`, `CUDACXX`), and variables that carry flags (`CFLAGS`, `LDFLAGS`, `RUSTFLAGS`). Flag variables are not read by compilers. Make expands them into the command line, so Bear sees the flags in the arguments already. Capturing them only fills the recorded environment with data nobody uses. The cargo variables (`CARGO`, `RUSTC`, `RUSTC_WRAPPER`) made the wrapper mechanism wrap cargo and rustc. Bear does not record rustc calls, so the wrapping gave no entry and only added a hop to every Rust build step. The program variable list follows make's implicit variables and CMake's language variables. CMake reads them on first configure to pick the compiler, so a build that sets them needs the wrapper there. ## Decision Rewrite make's program variables and CMake's language program variables. Do not rewrite or capture flag, architecture, or target variables, and do not rewrite cargo variables. ## Consequences Cargo builds run untouched. A new build system with a program variable means one more line in the list. The recorded environment is advisory only (see [interception-events-format](../requirements/interception-events-format.md)), so dropping flag variables from it loses no database content. ## References - [interception-wrapper-mechanism](../requirements/interception-wrapper-mechanism.md), the requirement this supports - [interception-events-format](../requirements/interception-events-format.md), the recorded environment is advisory - [CMake environment variables for languages](https://cmake.org/cmake/help/latest/manual/cmake-env-variables.7.html#environment-variables-for-languages) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/rationale/event-file-defaults.md�����������������������������������������0000664�0000000�0000000�00000007220�15271777203�0023707�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# No implicit event-file name: intercept names it, semantic reads stdin ## Context Through Bear 4.1.x, `bear intercept` wrote its event stream to `events.json` by default and `bear semantic` read `events.json` by default. The paired defaults made the split two-step workflow flagless. The price was a magic rendezvous file in the current directory: the two subcommands met at a name the user never chose and no outside tool reads. A stale `events.json` from an earlier run could be silently processed again by a bare `bear semantic`. Nothing in either command line showed that the two were connected. Piping a producer into `bear semantic` also failed, with "Event file not found: events.json", unless the user passed `--input -`. Seen as a UNIX tool, `semantic` is a pure filter: it runs no build, and stdout is free. Filters read standard input when no file is named. `intercept` is the opposite case. It can never use standard output, because the build owns that stream, so it has no natural default at all. The two file names are not equal. `compile_commands.json` is an ecosystem contract: Clang tooling finds the database by that exact name, so a default to it carries meaning. `events.json` was a name Bear made up, and no outside contract depends on it. Options considered: - Keep the paired defaults and add a "reading from a pipe? pass --input -" hint to the missing-file error. Rejected: it treats the symptom and keeps both the coupling and the stale-file hazard. - Default `semantic --input` to `-` only when stdin is not a TTY. Rejected: the same command line would behave differently in a terminal and in CI. CI stdin is often an empty pipe even when the user meant a file, which gives a silent empty database. - Give `intercept` a stream default by sending the build's stdout to stderr. Rejected: it changes how the build behaves. Tools that detect a tty or write results to stdout would break. - Require `--input` on `semantic` too, to match `intercept`. Rejected: a filter that will not read stdin by default fights the convention, and it would differ from `parse-sh`, whose input also defaults to `-`. Require the flag where no natural default exists; use the usual default where one does. - A positional file operand (`bear semantic [FILE]`, stdin when omitted). Not taken: `-i/--input` defaulting to `-` is just as conventional and matches the `parse-sh` flags. ## Decision `bear intercept` requires an explicit `--output`, and `-` is not accepted. There is no default event-file name. `bear semantic --input` defaults to `-` and reads the event stream from standard input. The `compile_commands.json` output default stays everywhere. Two guards soften the filter footguns: `semantic` warns on stderr when it is about to read events from a terminal, and when the event stream is empty. ## Consequences - Piping a producer into `bear semantic` needs no flags. The confusing missing-file failure goes away instead of getting a hint. - The stale-`events.json` hazard is gone. An event file is only read because the user named it. - The old two-step habit breaks loudly and early: `bear intercept` without `--output` is a usage error before the build runs. - Scripts written for the 4.1.x split workflow must add `--output` and `--input`. This is a deliberate breaking change in 4.2.0. It only gets more expensive later. - A bare `bear semantic` on a terminal blocks on stdin, as `sort` or `jq` would. The terminal warning names the way out. ## References - [interception-events-format](../requirements/interception-events-format.md), the seam whose endpoints this names - [parse-sh-producer](parse-sh-producer.md), the other mode whose input defaults to stdin ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/rationale/failed-build-still-publishes.md��������������������������������0000664�0000000�0000000�00000002050�15271777203�0025502�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# A failed build still publishes its output ## Context Bear writes each output file all or nothing: it keeps the previous file unless the run succeeds. A build that runs and exits non-zero raises the question of what counts as success. One option was to treat a failed build like any other failure and keep the previous output. That throws away the entries the build did produce, so a build that fails in one file would give the user nothing. ## Decision A build that runs and fails still publishes. Its exit status is the build's result, not a failure of Bear. Only an error of Bear's own keeps the previous output. ## Consequences The database reflects what the build compiled, even when the build failed. A build that fails before it compiles anything replaces a good database with an empty one, unless the run is appending. ## References - [output-atomic-write](../requirements/output-atomic-write.md), the requirement this supports - [append-default-migration](append-default-migration.md), the related risk of an empty run replacing a good database ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/rationale/generated-file-filter-globs.md���������������������������������0000664�0000000�0000000�00000003731�15271777203�0025311�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Generated-file filtering uses user-supplied globs ## Context Users repeatedly ask to keep machine-generated sources - Qt `moc_*.cpp`, protobuf `*.pb.cc`, and the output of other code generators - out of `compile_commands.json`, so that clang-tidy and editor tooling run only over hand-written code. Two shapes were considered: 1. Bear ships built-in knowledge of common generator outputs and filters them automatically. This is wrong the day a project uses a generator Bear has never heard of, and it silently drops files a user may want kept. 2. The user supplies filename patterns in configuration, mirroring the existing directory-rule mechanism. Glob matching needs a dependency. The candidates were `glob` (rust-lang owned, no transitive dependencies) and `globset` (faster, but pulls in `regex-automata` and its dependencies). Bear's source filtering runs once per entry over a set that is small relative to a whole build; matching speed is not a bottleneck. ## Decision User-supplied glob rules, evaluated with the same last-match-wins semantics as the directory rules. Use the `glob` crate for its zero-transitive-dependency footprint. Split basename-vs-full-path matching on whether the pattern contains a path separator, following the gitignore convention so that users' intuition carries over. ## Consequences There is no out-of-the-box filtering: a user writes two lines of configuration to exclude a generator's output. The man page carries the `moc`/protobuf recipes so the common cases are copy-paste. Directory rules and file-pattern rules compose (both must accept an entry), so the two mechanisms mix without surprising interactions. Revisit the `globset` choice only if profiling ever shows glob matching to be a real cost. ## References - [output-generated-file-filter](../requirements/output-generated-file-filter.md), the requirement this supports - [source-filter-last-match-wins](source-filter-last-match-wins.md), the directory rules whose semantics this mirrors ���������������������������������������rizsotto-Bear-0c30b27/docs/rationale/header-synthesis-sibling-clone.md������������������������������0000664�0000000�0000000�00000010643�15271777203�0026051�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Header entries: clone a sibling by default, read dependency files on request ## Context Headers need compile flags too, but the build never compiles them on their own. Five ways to get their flags were considered: 1. Bear runs the compiler with a make-dependencies flag for every translation unit. Accurate, but it runs compilers during analysis and ties Bear to the compiler being there. Rejected. 2. Read the dependency files the build already wrote. Accurate, works across directories, and runs no compiler. But the build must have written those files, and Bear needs a make-syntax parser and a way to find each translation unit's dependency file. 3. Parse `#include` directives. That is a hand-made preprocessor, and it gets conditional includes wrong. Rejected. 4. Clone a sibling, as the `compdb` tool does and clangd does in a variant inside: a header gets the entry of a translation unit compiled in the same directory. No compiler run, no parsing, no prerequisites. But it misses headers in directories with no compiled source, which is common in a split `include/` + `src/` layout. 5. Scan a translation unit's own `-I`/`-iquote` directories for headers to clone flags onto. Meant to reach the split layout without dependency files. It was built and then removed. A flat scan misses headers nested under an include root, which is the norm. A source-to-header path guess (strip leading parts of the source path and look for a same-stem header in each include directory) needs the layout to pair headers with sources. To judge option 5, four real projects were surveyed, working back from a nested header to its source: - zlib (flat): a header sits next to its source (`deflate.c`, `deflate.h`), and `-I` is the source directory. The umbrella `zlib.h` has no source. Sibling cloning already covers it. - Firefox: public headers are copied into a flat `dist/include` tree under chosen names (`mozilla/dom/Element.h`) unrelated to the source path (`dom/base/Element.h`). `-I` points at that copy, so no source-relative guess can rebuild the include path. On disk the header sits next to its `.cpp`, so sibling cloning finds it. - Linux: public headers live in a separate tree (`include/linux/slab.h`) from the code (`mm/slub.c`), the stem often differs (`slab` and `slub`, `sched.h` and `core.c`), and most headers have no single implementing `.c`. No path or name guess pairs them. - LLVM: a clean `include/<project>/...` and `lib/...` mirror, but the include tree adds a project directory, so a source-suffix guess (`IR/Function.h` under `-I .../include`) misses `include/llvm/IR/Function.h`. Much of ADT and Support is header-only. So where a header pairs cleanly with a source, it is almost always a sibling on disk, which option 4 handles. In the split layouts option 5 was for, the pairing is either missing (Linux) or project-specific (LLVM). And many headers have no source at all (interface, umbrella, UAPI, header-only templates), which no pairing can reach. Option 5 added configuration and code while covering at best what option 4 already covers. Option 2 handles all of these cases from the compiler's own output. ## Decision Bear ships two strategies. The default clones a same-directory sibling (option 4) and accepts approximate flags; an opt-in strategy reads the build's dependency files (option 2) for exact, cross-directory headers. ## Consequences - The default needs nothing from the build, but its flags are wrong when a header's real flags differ from its siblings', and it finds no header in a directory without compiled sources. - The dependency-file strategy needs a build that writes those files, and it brings a make-syntax parser and the logic to find each file. - Dependency-file headers are limited to the compilation's own working directory. Duplicate detection and entry checks do not drop system headers, so without the limit a file written with `-MD` (which lists system headers) would flood the database. The limit uses each compilation's working directory, not a global project root, because that is the frame the compiler used to resolve relative paths. ## References - [output-header-entries](../requirements/output-header-entries.md), the requirement this supports - [source-filter-last-match-wins](source-filter-last-match-wins.md), the source filter, which runs before header synthesis - [compdb](https://github.com/Sarcasm/compdb), the sibling-cloning prior art ���������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/rationale/mpi-wrappers-as-compilers.md�����������������������������������0000664�0000000�0000000�00000006035�15271777203�0025071�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# MPI wrappers are recorded as compilers, not expanded ## Context MPI wrapper commands (`mpicc`, `mpicxx`, `mpifort`, ... from Open MPI and MPICH; `mpiicc`, `mpiifort`, ... from Intel MPI) wrap the real compiler, but they are a different kind of wrapper than ccache/distcc/ sccache: the real compiler is not an argument of the invocation. The underlying compiler and the extra `-I`/`-L` flags are baked into the wrapper at MPI-installation time and are discoverable only by running the wrapper itself (`mpicc -show` on MPICH, `mpicc -showme` on Open MPI). Bear's wrapper-unwrapping mechanism therefore cannot apply. Two options were on the table: - **(a) Treat the wrapper itself as the compiler** and record the invocation verbatim, parsing its flags with the flag table of the compiler family it fronts. - **(b) Expand at analysis time** by running `mpicc -show` (or `-showme`) and recording the underlying compiler command with the baked-in flags inlined. Option (b) executes tools during semantic analysis, which is slow (one fork+exec per invocation or at least per wrapper path), couples Bear to the presence and health of the MPI installation at analysis time, and differs per MPI vendor (`-show` vs `-showme` vs `-compile_info`). The expanded command is also not necessarily what clang tooling needs: clangd already solves exactly this problem on its side with `--query-driver`, which asks the wrapper for its implicit include paths. ## Decision Option (a): record the wrapper invocation verbatim. Flag parsing uses the GCC flag table (via table inheritance), because the wrappers pass their arguments through to a gcc-compatible driver. Intel MPI wrappers are the exception: they are recognized as the Intel compilers themselves (extra basenames on the existing Intel families), because `mpiicc` is `icc`/`icx` with MPI paths baked in and Intel's flag table has arity entries (e.g. `-debug` takes an argument) that the GCC table would mis-parse. ## Consequences - No process execution during analysis; recognition stays pure and works even when the MPI toolchain is not installed on the machine that runs `bear semantic`. - Clang tooling users may need `--query-driver` (clangd) or an equivalent mechanism to learn the wrapper's baked-in include paths. Revisit expansion only if users ask for it. - Wrapper-info invocations (`-showme`, `-show`, `-compile_info`, `-link_info`) classify as info-and-exit, so probing the wrapper does not pollute the database. - With the preload mechanism the wrapper's child compiler exec is intercepted too; the default duplicate filter (directory+file) collapses the pair and the wrapper entry wins because its event comes first. ## References - [recognition-compiler-names](../requirements/recognition-compiler-names.md), the requirement this supports - [Open MPI wrapper docs](https://docs.open-mpi.org/en/main/building-apps/customizing-wrappers.html), how the wrappers carry their flags - [MPICH `mpicc(1)`](https://www.mpich.org/static/docs/latest/www1/mpicc.html), the `-show`, `-compile_info` and `-link_info` options ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/rationale/names-deliberately-not-recognized.md���������������������������0000664�0000000�0000000�00000003574�15271777203�0026547�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Some compiler-adjacent names are deliberately not recognized ## Context Several executables look like compilers, or sit next to them, but recording them would add wrong or useless entries. Users and contributors propose them from time to time. - **`as`, the GNU assembler.** GCC and Clang spawn it on a temporary `.s` file for every ordinary compile. Each run would add a throwaway entry keyed on a name that changes every run (`/tmp/cc<random>.s`), which duplicate detection cannot collapse. Assembly compiled through a driver (`gcc -c foo.s`) is already recorded through the driver's own entry. - **`mpirun`, `mpiexec`, and icecream's `icerun`.** They run arbitrary programs, not compilations. Recognizing them would record non-compilations. - **`swift` and `zig`.** They dispatch on a subcommand (`swift build`, `zig cc`, `zig build`). The subcommand, not the executable name, selects the compiler. Recognition works by executable name, so this model does not fit. - **`swift-frontend` and `swiftc -frontend`.** These are the Swift driver's internal per-file jobs. The user-facing `swiftc` invocation is the one to record. - **`amdgpu-arch`.** It shares the `amd` prefix but reports GPU architectures; it does not compile. - **`ml`, `ml64` (MASM).** Windows-only, and no user has asked for them. ## Decision These names are not recognized, and invocations of them yield no entry. ## Consequences - Revisit `swift` and `zig` only with subcommand-aware recognition. - Revisit MASM when a user needs it. - The other exclusions follow from what the tools do. They should stay. ## References - [recognition-compiler-names](../requirements/recognition-compiler-names.md), the exclusion list - [recognition-compiler-launchers](../requirements/recognition-compiler-launchers.md), the `icerun` exclusion - [#146](https://github.com/rizsotto/Bear/issues/146), compile-then-assemble ������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/rationale/parse-sh-producer.md�������������������������������������������0000664�0000000�0000000�00000013623�15271777203�0023413�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# parse-sh: a quarantined, best-effort parser whose only product is the database ## Context Users have asked for years for a compilation database built from the text a build system prints in dry-run mode (`make -n` and friends), without paying for a full build. See [#284](https://github.com/rizsotto/Bear/issues/284), [#287](https://github.com/rizsotto/Bear/issues/287), [#219](https://github.com/rizsotto/Bear/issues/219), [#644](https://github.com/rizsotto/Bear/issues/644), [#456](https://github.com/rizsotto/Bear/issues/456) and [#404](https://github.com/rizsotto/Bear/issues/404). Every one of them asks for `compile_commands.json`. None asks for an intermediate stream. The prior art they cite, [compiledb](https://github.com/nickdiego/compiledb), regex-scrapes `make -n` output straight into a database. Bear once declined the closest form of this. [#644](https://github.com/rizsotto/Bear/issues/644) asked for a build-log post-processor and was rejected as build-system-specific. A shell-text parser is still best-effort and build-system-flavored. What changed is that Bear now has a clean seam: the semantic stage consumes executions and does not care who produced them. A parser that yields executions needs nothing else from the core. Options considered: - A flag on the interception path (`bear --dry-run`). Rejected: Bear cannot intercept without running the build. Interception observes real `exec()` calls, so the flag would misrepresent what the tool does. - A monolithic parse-to-database tool like compiledb. Rejected: it would duplicate the semantic and output stack and tie the fragile parser to compiler recognition and formatting. - Regex scraping. Rejected: regexes mis-handle quoting, `VAR=value` prefixes, redirections and separators. They produce plausible but wrong commands with no signal. A real lexer over an explicit subset can skip what it cannot parse, and say so. - A separate executable. Rejected: `intercept` and `semantic` already set the pattern of stages as subcommands. A new binary adds install, man page and completions work for no gain. - The name `dryrun`. Rejected as dishonest: the build system dry-runs, this tool only parses the text it printed. `parse-sh` names the verb and the input. - Emit the execution-event stream and document a two-stage pipe, `make -n | bear parse-sh | bear semantic`. Rejected: it makes an implementer's seam the user interface. Bear's usual shape is one command in, database out (`bear -- make`). - A flag that selects events or database output. Rejected: it would flip both the meaning and the default of the output destination, and make config validity depend on another flag. - A sibling `parse-sh-events` subcommand, shown or hidden from help. Rejected: no user asked for the raw stream. A hidden one would also make help, man page and completions disagree. - Keep the stream for caching or debugging. Rejected: an intercepted event file is worth keeping because a rebuild is expensive, but dry-run text is cheap to keep and parse again. Skip reports on stderr and debug logging already show what the parser saw. - Support subshell groups `( ... )`. Rejected: a child shell's state must not leak back out, and modeling a child shell is the growth the skip path exists to avoid. Adding a public events output later is a compatible change. Shipping one and removing it is a breaking change. So the smaller surface wins. ## Decision `parse-sh` is a `bear` subcommand. It lexes a small, explicit POSIX `sh` subset from text (stdin by default) and writes `compile_commands.json` (the default destination). Config, append and stdout output work as in the other database modes. The reconstructed executions feed the unchanged semantic stage inside the process and are never written out. Text outside the subset is skipped with a line number and reason, never guessed at. Interception stays the recommended high-fidelity path. Inside the subset, make's `Entering directory` / `Leaving directory` markers are recognized, since they would hit the skip path anyway and they give correct directory tracking. Brace groups `{ ...; }` are supported because they run in the current shell, so a `cd` inside one persists. ## Consequences - The headline usage is one stage: `make -n | bear parse-sh`. - The [#644](https://github.com/rizsotto/Bear/issues/644) rejection is narrowed, not reversed. The parser sits behind the execution seam. The core stays parser-agnostic and can take other producers (xcodebuild logs, strace) without change. - `bear intercept` is the only Bear producer of the event file. The format keeps its value for third-party producers feeding `bear semantic`. - Fidelity is bounded by the dry-run text, by design. The best-effort framing must stay loud in user docs, or users will read a partial database as a Bear bug. - Bear carries a shell lexer. It is small and closed, which caps the cost. Do not grow it toward a full shell. The skip path, not more grammar, is the pressure valve. - Revisit the events output if real demand appears, for example link commands once linker support exists. It can be added compatibly. ## References - [interception-shell-text-parsing](../requirements/interception-shell-text-parsing.md), the contract this supports - [interception-events-format](../requirements/interception-events-format.md), the execution model the parser targets - [parse-sh-single-tokenizer](parse-sh-single-tokenizer.md), how the lexer is built - [event-file-defaults](event-file-defaults.md), the event-file defaults for `intercept` and `semantic` - [#284](https://github.com/rizsotto/Bear/issues/284), the dry-run request; also [#287](https://github.com/rizsotto/Bear/issues/287), [#219](https://github.com/rizsotto/Bear/issues/219), [#644](https://github.com/rizsotto/Bear/issues/644), [#456](https://github.com/rizsotto/Bear/issues/456), [#404](https://github.com/rizsotto/Bear/issues/404) - [compiledb](https://github.com/nickdiego/compiledb), the prior art �������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/rationale/parse-sh-single-tokenizer.md�����������������������������������0000664�0000000�0000000�00000007476�15271777203�0025072�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# A single streaming tokenizer for parse-sh ## Context The first parse-sh (the 4.2.0-rc branch) split its lexical rules across several scanners. The lexer's word reader owned quotes, escapes and comments. The interpreter scanned the raw line again, with its own quote rules, to find here-documents and to read their delimiters. A separate pre-pass joined backslash-newline continuations, which the lexer also did. The scanners drifted apart, and each drift was a bug. The here-doc detector did not know about comments, so a `<<` in a trailing `#` comment swallowed every later line. Its delimiter reader broke words differently from the lexer, so a here-doc inside a subshell swallowed the rest of the input. A redirect-target reader dropped the unterminated-quote signal the word reader had already found, and so made up executions from quoted text. Any two scanners over the same text will disagree in the end. Options considered: - Keep the scanners and fix each drift as it is found. Rejected: it treats symptoms, and each fix has to be found by review instead of being prevented. - Rebuild on a parser-combinator crate (nom or winnow), producing an AST that a visitor turns into executions. Rejected for three reasons. Combinators change the scanner but do not add POSIX knowledge, and the bugs were missing grammar knowledge (the `>|` operator, comment bounds). Shell fights combinators: a here-doc body starts at the next newline, not at `<<`, which needs stateful two-phase lexing (real shells hand-write their lexers for this reason). And a full AST inverts the contract: the requirement is a small subset with loud skips that never guesses, and that is easy to audit in a lexer but hard in a general parser. It would also add a dependency that removes no complexity. - Use a complete POSIX shell parser crate (yash-syntax). Rejected for the same contract inversion, only stronger. It parses all of shell, so every construct it accepts needs either semantics or a mapping to a skip reason, and the small subset stops being small. - One hand-written tokenizer that owns every lexical rule, feeding a parser that owns the interpreter state. Chosen. ## Decision The tokenizer is the only code that reads characters: quotes, escapes, comments, continuations, redirections, here-documents and the recursive-make directory markers. The parser reads its tokens and skip items and owns all interpreter state: the working directory, the directory stack, and the `VAR=value` overlay. After a skip the tokenizer keeps scanning the line, so a later `<<` is still seen, and the parser discards up to the next newline. Quote state resets at every newline, to match the line-oriented contract. ## Consequences - The drift bugs cannot happen, because there is no second scanner. Comment, quote and word-break rules live in one place. - A skip cannot be dropped silently. Tokens come as a `Result`, so every consumer must handle it. This is the loud-skip contract in the type. - Both stages are streaming state machines that hold at most one logical line. A gigabyte build log streams through, and events reach the consumer as each line completes. - The grammar is still written by hand. Missing POSIX knowledge (the `>|` kind of bug) is not prevented, only cheaper to add in one place. The skip path stays the pressure valve, and the subset must not grow toward a full shell (see [parse-sh-producer](parse-sh-producer.md)). ## References - [interception-shell-text-parsing](../requirements/interception-shell-text-parsing.md), the contract whose loud-skip clause drives the design - [parse-sh-producer](parse-sh-producer.md), why the producer exists; this entry covers only its internal shape - [nom](https://crates.io/crates/nom), [winnow](https://crates.io/crates/winnow) and [yash-syntax](https://crates.io/crates/yash-syntax), the rejected crates ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/rationale/preload-chains-to-co-resident-libraries.md���������������������0000664�0000000�0000000�00000002260�15271777203�0027540�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Preload chains to co-resident preload libraries ## Context A build can already carry another preload library, such as Gentoo's `libsandbox.so`. That library may hook the same `exec` family as Bear's and may reset its own preload variable further down the exec chain, dropping Bear's entry. Grandchildren past that point are then not intercepted ([issue #699](https://github.com/rizsotto/Bear/issues/699)). Bear could guard its entry by refusing to delegate to the other library. That would turn off the sandbox and change how the build behaves, which interception must not do. ## Decision Bear puts its library first in the preload variable, keeps the other libraries after it, and forwards every call to the next library in the chain. ## Consequences The sandbox keeps working and the build behaves as without Bear. The cost is that a library which resets the preload variable can hide part of the build from Bear. Bear accepts that gap rather than break the other library. ## References - [interception-preload-mechanism](../requirements/interception-preload-mechanism.md), the requirement this supports - [#699](https://github.com/rizsotto/Bear/issues/699), the sandbox conflict report ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/rationale/preload-linker-selection.md������������������������������������0000664�0000000�0000000�00000012275�15271777203�0024745�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Linker selection for the preload library ## Context The preload library must export exactly the interposer symbols that the C shim defines (the `exec` family, `posix_spawn*`, `popen`, `pclose`, `system`) plus `LIBEXEC_VERSION`, and hide everything else. On ELF platforms the build script does this with a generated linker version script, `exports.map`. rustc adds its own version script to every cdylib link: an anonymous tag that exports the crate's `#[no_mangle]` symbols and ends in `local: *`. Two facts follow, both checked by experiment (2026-07): - Bear's script cannot be dropped. rustc's script hides every symbol from a linked native static archive. Without `exports.map` the interposers leave the dynamic table and interception silently stops. This holds even with rustc's own `-l static:+whole-archive=...`. - The link line always has two version scripts, and GNU ld refuses that: "anonymous version tag cannot be combined with other version tags" (binutils 2.46). A named tag in Bear's script does not help, because rustc's tag is the anonymous one. This is the failure in [#662](https://github.com/rizsotto/Bear/issues/662) (Ubuntu 24.04 aarch64). lld and mold accept the pair and export the union of both scripts' globals. On `x86_64-unknown-linux-gnu`, stable rustc links cdylibs with its bundled rust-lld by default. Other targets, such as aarch64, use the system `cc` default linker, GNU ld. That is why [#662](https://github.com/rizsotto/Bear/issues/662) hit ARM64 only. The build used to hard-code `-fuse-ld=lld` ([#664](https://github.com/rizsotto/Bear/pull/664)), which fails where no lld is installed. An installed linker is not always a capable one. rustup's rust-lld is built without zlib. On a host whose system objects carry zlib-compressed debug sections, such as the FreeBSD base `crt*.o`, it refuses the link: "is compressed with ELFCOMPRESS_ZLIB, but lld is not built with zlib support" ([#722](https://github.com/rizsotto/Bear/pull/722)). Options considered: - Drive the final link with `cc` from a staticlib. One version script links fine under GNU ld (checked). But stable cargo cannot do it: a build script cannot depend on a sibling crate's staticlib (the gap that RFC 3028 "artifact dependencies" fills), a crate's own build script runs before the crate compiles, and linking outside cargo breaks the single `cargo build` and adds a packaging step. - A named version tag in `exports.map`. GNU ld rejects any pair that includes an anonymous tag (checked). - Drop `exports.map` and rely on rustc's export handling. Native-archive symbols are never in rustc's export list (checked). - A fallback chain of linkers that accept two scripts. Chosen. ## Decision The build script picks the ELF linker in this order: the toolchain's bundled rust-lld, a system `ld.lld`, a system `mold`. A candidate is used only if it passes a probe link in `OUT_DIR`: one PIC object linked into a shared library with the same version script given twice, in place of rustc's script and `exports.map`. Both known failures show up in the probe: GNU ld rejects the two anonymous tags, and a zlib-less lld rejects compressed `crt*.o`. If no candidate is found or passes, the build adds no linker flag and prints a build warning that names the GNU ld error and the fix, in case the default linker copes anyway. If the probe cannot run at all (no usable `cc`, for example), the first candidate is used unchecked. ## Consequences - rustup toolchains build the library on every architecture with no extra linker package. On FreeBSD the probe rejects rust-lld and the base system's `ld.lld` takes over. - A skipped first choice shows up as a build warning, so the switch is not silent. On a clean build the probe costs one compile, then one link per candidate until one passes. - A Rust toolchain without bundled rust-lld (some distro packages) needs `lld` or `mold`. Both are widely packaged. Without them the user gets an early warning before the cryptic linker error. - The system probes search `PATH` only. An `ld.lld` that `cc` finds through its own exec prefix falls through to the warning. Revisit, for example with `cc -print-prog-name=ld.lld`, if a user reports that case. - The union means the `rust_<name>` implementation symbols also show up in the dynamic table, despite `local: *` in `exports.map`. This is cosmetic: the names are prefixed and calls resolve inside the library. Removing them needs a function-pointer indirection between the shim and the Rust side, or a rustc option to exclude them. Revisit if a strict export surface becomes a requirement. - Revisit the staticlib option when cargo artifact dependencies are stable. ## References - [interception-preload-mechanism](../requirements/interception-preload-mechanism.md), the requirement this supports - [#662](https://github.com/rizsotto/Bear/issues/662), the GNU ld link failure on aarch64 - [#664](https://github.com/rizsotto/Bear/pull/664), the original hard-coded `-fuse-ld=lld` - [#722](https://github.com/rizsotto/Bear/pull/722), the probe link, after rust-lld failed on FreeBSD - [How to use linker version scripts in Rust 1.54](https://users.rust-lang.org/t/how-to-use-linker-version-scripts-in-rust-1-54/64033), the rustc version-script behavior �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/rationale/preload-symbol-probes.md���������������������������������������0000664�0000000�0000000�00000011000�15271777203�0024254�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# How the build finds the functions the preload library wraps ## Context The preload library wraps a fixed list of libc functions (the `exec` family, `posix_spawn*`, `popen`, `pclose`, `system`), but not every libc has all of them: `execvpe` is missing on macOS, `execvP` is BSD only, and bionic adds `posix_spawn` at API level 28. The build has to find out which ones the target libc exports, and only wrap those. The first version ran its probes in the build script of a build-dependency crate. Cargo builds such a crate for the host, so the probes saw the host libc, and a cross build for Android got glibc's answers. The probes also only compiled a reference to the name, so they checked that a header declares it, not that libc exports it. Other tools solve the same problem. Autoconf `AC_CHECK_FUNC` and Meson `has_function` link a test program that declares the function itself, with no header, and they use the cross compiler when cross compiling. CMake `CheckSymbolExists` includes the header and links. Gentoo's sandbox, another preload library, filters a fixed list against the symbol table of the real libc. No Rust crate probes C symbols; the `libc` crate hardcodes its bindings per target. Options considered: - Probe once in a build-dependency and share the answers. Wrong target when cross compiling, as above. - Probe once in the preload crate and hand the answers to the integration tests through `links` metadata. The tests would need the preload crate as a normal dependency, with the risk that its exported wrappers get linked into the test binary. - Let the tests read the exports of the built library at run time. That turns compile-time test gates into run-time skips, and the tests still need to know which functions the host C compiler can call. - Header and link probe, as in CMake. It also rejects a function libc exports but the header hides, such as `posix_spawn` below API 28 on Android. The shim defines the functions and never calls them, so the declaration does not matter to it. - Pick by platform macro, such as `__APPLE__` for `_NSGetEnviron`. It gives the same answers on today's targets, but the build then holds two ways to decide the same kind of question. - Wrap `posix_spawn*` on Android below API 28 anyway and look the real function up at run time. It keeps the interception on newer devices, but it overrides the probe for one platform. - Wrap musl's `__execvpe`. musl has hidden the symbol since 2018, so no program outside libc can call it, and calls inside libc bind locally. ## Decision Each crate that needs the answers runs the probes from its own build script, against the target it is built for, from one shared probe list. A function counts as present when a test program that declares it itself links against the target libc. Optional headers are probed by including them. The build fails when `execve` is not found, because that means the toolchain is broken. The extension macros that autoconf's `AC_USE_SYSTEM_EXTENSIONS` defines are passed as compiler flags on every target. ## Consequences The answers follow the target, so a cross build wraps what the target libc exports. The probes run twice per build, once for the preload crate and once for the integration tests; with the same list and the same compiler they agree. On Android the answer follows the API level the build targets: a build for API 24 does not wrap `posix_spawn*`, and a build for API 28 or later does. On musl, calls between functions inside libc (for example `execvp` calling `__execvpe`, or `popen` calling `posix_spawn`) cannot be intercepted; only the public entry points can. Revisit when a target needs a function that a link probe cannot see, such as a macro or an inline function. ## References - [interception-preload-mechanism](../requirements/interception-preload-mechanism.md), the requirement this supports - [preload-linker-selection](preload-linker-selection.md), how the wrapped functions are exported - [Autoconf AC_CHECK_FUNC](https://github.com/autotools-mirror/autoconf/blob/master/lib/autoconf/c.m4) - [Meson has_function](https://mesonbuild.com/Reference-manual_returned_compiler.html) - [CMake CheckSymbolExists](https://cmake.org/cmake/help/latest/module/CheckSymbolExists.html) - [Using newer APIs on Android](https://developer.android.com/ndk/guides/using-newer-apis) - [musl commit 0676c3a3](https://git.musl-libc.org/cgit/musl/commit/?id=0676c3a34c7bf12b33f8f5efb92476f4ffc7f20e), hides `__execvpe` - [#705](https://github.com/rizsotto/Bear/issues/705), the s390x report that led to the `__execvpe` wrapper rizsotto-Bear-0c30b27/docs/rationale/process-tree-teardown.md���������������������������������������0000664�0000000�0000000�00000013532�15271777203�0024303�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Supervise the build ## Context When Bear runs a build (`bear -- make`), `bear-driver` starts the build and supervises it as the leader. It waits for the build to end and passes its exit status back. When a termination signal arrives, the whole process tree under the build must stop within the budget that [cli-signal-forwarding](../requirements/cli-signal-forwarding.md) sets. Bear should still write the partial `compile_commands.json` it has collected. The first `supervise()` only sent `SIGKILL` to the direct child. Grandchildren were reparented to init and kept running. And `SIGKILL` cannot be trapped, so neither the build's own trap nor running compilers got a chance to wind down. The leader's job has four steps. **Start.** The leader puts the build in its own process group. On Linux it also puts the build in a fresh cgroup. Three mechanisms can stop a whole tree: | Platform | Mechanism | Can a child escape? | |---|---|---| | any unix | process group (`setpgid`/`setsid`) + `killpg` | yes, by calling `setsid` itself | | Linux | cgroup v2 `cgroup.kill` | no, unprivileged moves are denied | | Windows | Job Object (`KILL_ON_JOB_CLOSE`) | no | Process groups work on every unix and need no new dependency (`libc` is already there). Their one gap is a child that calls `setsid` to daemonize. cgroups close that gap, but they need cgroup v2, a writable (delegated) cgroup directory, and a kernel with `cgroup.kill`. `std::process::Command` cannot start a child in a cgroup, so the child writes its own pid to `cgroup.procs` in a `pre_exec` hook. When any of this is missing, Bear falls back to the process group at runtime. Job Objects need a `windows-sys` dependency, and Bear has too few Windows users to justify that path. The group is made with the safe `Command::process_group(0)`. The compiler version probe uses `setsid` instead, for a reason given in a comment next to the probe code. **Wait.** The leader blocks until the build exits or a signal arrives. `Child::wait()` blocks and cannot watch for a signal at the same time. Options: - Poll with `try_wait()` and a sleep, as the old loop did with 100ms. Rejected: every signal can wait up to one full sleep. - A `poll()` over a `pidfd` and a `signalfd`. Rejected: Linux 5.3+ only and more `libc` code. - A blocking loop on `SIGCHLD` plus the termination signals, using the `signal-hook` crate Bear already has. Chosen: portable and no polling. On unix the termination signals are `SIGTERM`, `SIGINT` and `SIGQUIT`. Windows has no `SIGCHLD`, so there Bear polls every 100ms. **Forward.** The leader sends the real signal it got to the whole process group, not a substitute. Build tools such as `make -j`, `ninja` and `cmake --build` stop their own workers when signaled, and the build's traps can tell `SIGINT` from `SIGTERM`. **Grace and kill.** The leader then gives the tree a grace window of 800ms to wind down. [#713](https://github.com/rizsotto/Bear/issues/713) sized it: a trap on a starved CI runner needed the headroom, and 800ms still keeps a build that ignores the signal inside the budget. During the window the leader checks the direct child with `try_wait()` every 10ms. If the child exits, the window ends early. Then comes the kill. With a cgroup, the leader always writes `cgroup.kill`, even when the child exited in time, to reach a descendant that left the group. Without a cgroup, it sends `SIGKILL` to the group only if the direct child is still there. A `pidfd` for the grace window was drafted and dropped. It was a Linux-only path with a runtime fallback for `ENOSYS` and `EPERM`, about a hundred extra lines and three `unsafe` blocks. The gain was at most 10ms and one wakeup instead of about 80, and only after a signal. Next to an 800ms grace window nobody would notice. The wrapper mechanism runs the same `supervise()` deeper in the tree. That part is in [supervise-a-compiler-call](supervise-a-compiler-call.md). ## Decision The leader starts the build in its own process group and, on Linux, in a fresh cgroup, then waits on `SIGCHLD` and the termination signals. On a signal it forwards the real signal to the group and gives it 800ms. Then it kills the cgroup, or sends `SIGKILL` to the group if the child is still there. ## Consequences - No new dependency. The wait reacts at signal speed, inside the budget. - Forwarding the real signal keeps the build tools' own worker cleanup working, so Bear does not reimplement it per tool. This is why the requirement says Bear must not interfere with the build tool's own signal handling. - The graceful phase is group-based because `cgroup.kill` can only send `SIGKILL`. A descendant that detached with `setsid` gets no grace window, only the final `cgroup.kill`. Accepted: a daemon that detaches on purpose gives up the graceful wind-down. - Without a usable cgroup (not Linux, no cgroup v2, no delegated directory, kernel without `cgroup.kill`) the `setsid` hole stays open and teardown falls back to the process group. - The build leaves Bear's process group, so the terminal no longer sends Ctrl-C to the build directly. Bear is the only path for the signal. This is what makes tree teardown and real-signal forwarding reliable, and it fixes traps in the non-terminal case (CI `SIGTERM`). The cost is that a gap in Bear's forwarding has no terminal backstop. Accepted. - Each build creates and removes one cgroup directory. Killed processes are reaped by init later, so removal after a kill retries briefly. - Outside unix, Bear still kills only the direct child. Revisit with a Job Object if Windows users report builds that outlive Bear. ## References - [cli-signal-forwarding](../requirements/cli-signal-forwarding.md), the requirement this supports - [supervise-a-compiler-call](supervise-a-compiler-call.md), the nested supervisor under the wrapper mechanism - [#713](https://github.com/rizsotto/Bear/issues/713), the starved CI runner that sized the grace window ����������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/rationale/response-file-inlining-best-effort.md��������������������������0000664�0000000�0000000�00000002463�15271777203�0026646�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Response-file inlining is best effort ## Context Bear inlines `@file` response files after the build has run. By then a file may be gone, since response files often live in the build directory and get cleaned. A response file may also reference another one, and not every compiler documents that nesting. Failing the whole database for one missing file was an option. It was not chosen: the build succeeded, so the file was there when it ran. A missing file at analysis time is a stale build artifact, not an error in the build. Refusing nested references that a compiler does not document was also an option. It was not chosen: the build Bear saw already accepted them, or it would not have compiled. ## Decision Follow nested references for every compiler, up to a fixed depth that guards against cycles. Leave any `@file` that cannot be read, or that is past the depth limit, literal in the entry and log a warning. ## Consequences One bad response file costs one entry its inlined flags, never the whole database. The depth limit is not configurable. Revisit it only if a real build nests deeper than the limit. ## References - [output-response-file-inlining](../requirements/output-response-file-inlining.md), the requirement this supports - [#701](https://github.com/rizsotto/Bear/issues/701), the feature request �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/rationale/source-filter-last-match-wins.md�������������������������������0000664�0000000�0000000�00000003232�15271777203�0025637�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Source filter: last matching rule wins ## Context Bear can include or exclude source files from the compilation database by directory rules (`sources.directories`). The original feature request ([#261](https://github.com/rizsotto/Bear/issues/261)) raised the question of precedence: when a file matches several rules with conflicting actions, which one decides? Two policies were on the table: - **Fixed precedence** - e.g. "exclude always wins". Simple to reason about, but rigid: a user cannot carve out an exception inside an excluded subtree (include `src`, exclude `src/test`, then re-include `src/test/integration`). - **Order-based, last match wins** - rules are evaluated top to bottom and the last one that matches sets the action. ## Decision Use order-based evaluation where the last matching rule wins, with the default action being "include" when no rule matches. ## Consequences Users get full control over precedence by ordering rules, including exceptions-to-exceptions, which a fixed policy cannot express. A common pattern is a `.` catch-all first, then more specific rules after it. The cost is that precedence is now positional: reordering rules changes behaviour, so the rule list is order-sensitive and the user owns getting the order right. Matching itself is literal prefix matching (`Path::starts_with`), so rule paths must use the same path format the entries are written in ([output-path-format](../requirements/output-path-format.md)). ## References - [output-source-directory-filter](../requirements/output-source-directory-filter.md), the requirement this supports - [#261](https://github.com/rizsotto/Bear/issues/261), the feature request ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/rationale/structured-data-scope.md���������������������������������������0000664�0000000�0000000�00000006436�15271777203�0024276�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# The site emits only BreadcrumbList and SoftwareApplication JSON-LD ## Context The site build adds JSON-LD structured data to every rendered page, in the same post-processing pass that adds the canonical link. Before picking schema.org types, the obvious ones were checked against Google's [search gallery](https://developers.google.com/search/docs/appearance/structured-data/search-gallery), because a valid schema.org type says nothing about whether Google still rewards it. Three types look like a fit for this site and were rejected. All three are still valid, and Google parses them without penalty, but they no longer produce any rich result: - `HowTo`. The recipe pages are numbered-step task pages, a textbook `HowTo`. Google dropped `HowTo` rich results from mobile search in August 2023 and from desktop in September 2023. - `FAQPage`. An FAQ page is a textbook `FAQPage`. Google limited these rich results to government and health sites in August 2023, then deprecated the feature on 2026-05-07. Search Console reporting for it ends in June 2026 and API support in August 2026. - `WebSite` with a `SearchAction`, the sitelinks search box. Google retired it in November 2024. It is a common default addition, so it is named here before someone adds it out of habit. Someone will look at the recipe pages, see the structural match, and propose this markup again. That is why this entry exists. For `BreadcrumbList` labels, deriving them from file names was tried and rejected. It produced "Cray Hpc" and "Ti Compilers", worse than the trail Google already builds from the URL. Label control is the only thing the type buys. For a directory with no `index.html`, pointing its crumb at the site root was rejected too: a crumb that links to a page its label does not describe is a false statement in machine-readable form. ## Decision The site emits `BreadcrumbList` on every page, with each label taken from that page's own rendered `<title>`, and `SoftwareApplication` on the home page only. A directory with no page of its own gets no crumb, and the `SoftwareApplication` block has no rating or review fields, because Bear collects no such data. ## Consequences - Google already builds a breadcrumb trail from the URL. An explicit `BreadcrumbList` controls the words in that trail. It does not make the page eligible for any new rich result. - Without a rating or a price, `SoftwareApplication` is unlikely to show any rich result. It is there so a crawler that does read JSON-LD gets a clear statement of what Bear is and where it lives. - Both are cheap because the pass already rewrites every page's `<head>`. Neither is a promise of ranking lift. - LLM assistants read rendered prose, not JSON-LD. Whether they cite a page depends on the answer-first writing rule in [site/CLAUDE.md](../../site/CLAUDE.md), not on structured data. - The dates above are what was true when this was decided. Revisit against the current search gallery, not this entry, when Google ships a rich result that fits a recipe or FAQ page. ## References - [docs-site-over-wiki](docs-site-over-wiki.md), the decision to have a site, and the "no lift is assumed" stance this extends - [Google search gallery](https://developers.google.com/search/docs/appearance/structured-data/search-gallery), the rich results Google supports ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/rationale/supervise-a-compiler-call.md�����������������������������������0000664�0000000�0000000�00000004336�15271777203�0025035�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Supervise a compiler call ## Context With the wrapper mechanism the chain is `bear-driver` -> `make` -> `bear-wrapper` -> real `cc`. The wrapper reports the call, then runs the real compiler under the same `supervise()` the driver uses. So supervision nests: the driver is the leader at the top, and each wrapper is a supervisor of one compiler call further down. How the leader starts, waits, forwards and kills is in [process-tree-teardown](process-tree-teardown.md). This entry covers only what the wrapper does differently. The forces are the leader's: one signal to Bear must stop the whole tree within the budget of [cli-signal-forwarding](../requirements/cli-signal-forwarding.md), and the build's own signal handling must keep working. Options: - Each wrapper makes its own process group, like the leader. Rejected: the build splits into many groups. A top-level `killpg` then misses the processes below each wrapper, which reopens the hole grouping is meant to close. - Each wrapper runs its own grace window and `SIGKILL`. Rejected: the leader's group kill (or `cgroup.kill`) is the real teardown, and it already reaches the wrapper's child. A second timer adds nothing. - The wrapper stays in the group and cgroup it inherits, relays a signal to its direct child only, and waits. Chosen. ## Decision The wrapper does not create a process group or a cgroup. On a signal it relays the signal to its direct child, waits for it, and passes the child's exit code back. The leader owns the only escalation timer. ## Consequences - The wrapper and the compiler stay in the leader's process group, so the leader's forward and `SIGKILL` reach them with one `killpg`. - The wrapper inherits the leader's cgroup through its parent, so one `cgroup.kill` from the leader reaches the whole tree. - A compiler that ignores the signal is stopped by the leader when the grace window ends, not by the wrapper. The wrapper just waits until then. ## References - [cli-signal-forwarding](../requirements/cli-signal-forwarding.md), the requirement this supports - [process-tree-teardown](process-tree-teardown.md), how the leader supervises the build - [wrapper-mode-design-decisions](wrapper-mode-design-decisions.md), the wrapper mechanism itself ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/rationale/swift-whole-module-entries.md����������������������������������0000664�0000000�0000000�00000006120�15271777203�0025244�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Swift whole-module entries: one per source, full arguments ## Context `swiftc` supports whole-module compilation. One call can name every `.swift` source of a module (`swiftc -module-name App -wmo a.swift b.swift c.swift`), and the frontend analyzes them together. `valac` has the same many-sources, one-call shape, but the consumer here, [SourceKit-LSP](https://github.com/swiftlang/sourcekit-lsp), needs a different entry shape. Three entry shapes were considered, all of which Bear already makes for some family: - Per source, siblings stripped (the GCC and Clang shape). Rejected: SourceKit-LSP rebuilds a file's semantic context (imports, symbols from other files of the module) from the recorded command. Without the sibling sources the command describes a compilation that cannot produce that file's symbols. - One combined entry, `file` set to the first source (the `valac` shape). Rejected: SourceKit-LSP looks up a command by file path, so every source but the first would have no command. - Per source, full arguments (the CMake shape): one entry per source, each with the whole call. Chosen. CMake's Swift support already writes this, and SourceKit-LSP reads CMake databases today ([forum thread](https://forums.swift.org/t/sourcekit-lsp-and-cmake/67956), [sourcekit-lsp#2087](https://github.com/swiftlang/sourcekit-lsp/issues/2087)). The per-call setting could have grown a second flag next to the existing separable/combined one. Rejected: every call site would have to reason about the two flags together. ## Decision Bear writes one entry per Swift source, each with the complete call: all sources and all flags, minus link-only flags as for every family. The per-call source mode is a three-way choice (stripped per source, full per source, combined). ## Consequences - An N-source whole-module call gives N entries that each list all N sources. Accepted: it is what CMake writes and SourceKit-LSP reads, and the combined shape would leave N-1 files with no command. Revisit only if someone reports database size as a real problem. - A future many-sources, one-call compiler must choose between the combined and full-per-source modes from how its consumer looks up files. Neither is a safe default. - The per-file `swift-frontend` jobs (and the legacy `swiftc -frontend` self-call) are filtered by the same `ignore_when` rule used for GCC's `cc1` and Clang's `-cc1`. ## References - [recognition-compiler-names](../requirements/recognition-compiler-names.md), the requirement that adds `swiftc` - [output-compilation-entries](../requirements/output-compilation-entries.md), the entry-shape contract - [compilation-entry-shapes](compilation-entry-shapes.md), the entry shapes per compiler kind - [vala-transpiler-database](vala-transpiler-database.md), the combined shape - [SourceKit-LSP](https://github.com/swiftlang/sourcekit-lsp), the consumer - [SourceKit-LSP and CMake](https://forums.swift.org/t/sourcekit-lsp-and-cmake/67956), the forum thread on CMake's Swift databases - [sourcekit-lsp#2087](https://github.com/swiftlang/sourcekit-lsp/issues/2087), a related SourceKit-LSP issue ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/rationale/vala-transpiler-database.md������������������������������������0000664�0000000�0000000�00000007210�15271777203�0024711�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Recording Vala (valac) builds in the compilation database ## Context `valac` is a transpiler-driver, not a plain compiler. It parses Vala (`.vala`) and Genie (`.gs`) sources and emits C. Unless given `-C`/`--ccode`, it then runs a real C compiler on that C (by default `cc`, or `--cc=` or `$CC`). Under interception Bear sees two processes: `valac` and the internal `cc`. The consumer behind the request ([#709](https://github.com/rizsotto/Bear/discussions/709)) is [vala-language-server](https://github.com/vala-lang/vala-language-server) (VLS). It takes an entry as Vala only when `command[0]` contains `valac`. It reads only the `command` string form and skips an entry that has just an `arguments` array. Bear writes the array form by default, so the two clash. `valac` builds all of a target's sources as one unit with one output (a `--library`/`--vapi`). The usual one-entry-per-source shape gave N entries for one call, each still carrying the producing flags. VLS keys each command on the `.vapi` it produces, so it saw N producers of the same binding and refused to build. Meson writes one entry per `valac` call, with `file` set to the first source. Options considered: - Record the internal `cc` call instead of `valac`. Rejected: VLS needs `valac` in `command[0]`, so the `cc` entry is useless to it. - Switch to the string form for `valac` entries only. Rejected: every entry in a database should have the same shape. - Filter out the internal `cc` entries. Rejected: interception gives no reliable way to tie a `cc` call to its parent `valac`, and entry checks do not test that a file exists, so there is no clean hook. The entries are also valid in their own right: a C language server can use them. - Add `.vapi` and `.gir` as source extensions next to `.vala` and `.gs`. Rejected: they are bindings `valac` reads, not translation units, so they would give entries for inputs nothing compiles. - Classify `-X`/`--Xcc` (one token passed to the C compiler) as a link flag. Rejected: the token can be a compile flag (`-X -fPIC`) or a link flag (`-X -lm`), and link flags are stripped from the entry. Dropping a compile flag breaks the entry; keeping a stray link flag is harmless. ## Decision Bear writes one entry per `valac` call, as Meson does, and keeps the internal `cc` entries. `.vala` and `.gs` are source extensions, and `-X`/`--Xcc` count as compile flags. ## Consequences - A default Bear database (array form) gives VLS no usable entries. The man page says so and points to `format.entries.use_array_format: false`. This is a documentation cost, not a silent failure. - The internal `cc` entries point at C files that, without `-C`, live in a temporary directory and may be gone after the build. clangd may try to index them. That is better fixed on the clangd side (a `.clangd` `PathMatch` rule) than by dropping valid entries, and VLS ignores them. Revisit only if a real consumer needs them suppressed. - A new Vala-family extension must be added to the source extension list by hand, as for every other language. - `valac` is recognized with versioned names (`valac-0.56`) and with a target prefix, because Debian and Ubuntu install it as `x86_64-linux-gnu-valac`. ## References - [output-compilation-entries](../requirements/output-compilation-entries.md), the requirement this supports - [compilation-entry-shapes](compilation-entry-shapes.md), the entry shapes per compiler kind - [#709](https://github.com/rizsotto/Bear/discussions/709), the feature request discussion - [vala-language-server](https://github.com/vala-lang/vala-language-server), its `src/projects/ccproject.vala` holds the `valac` filter and the command-string rule ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/rationale/workspace-crate-layout.md��������������������������������������0000664�0000000�0000000�00000010305�15271777203�0024447�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# The analysis core is its own crate, not a module ## Context Bear does two separate things. It intercepts a build, and it decides what an intercepted execution means: is it a compiler call, and what are its arguments. The second half, compiler recognition and flag parsing, used to be a module in the same library crate as the command-line arguments, the configuration schema, the modes, and the output writers. The module had the right shape but no boundary anyone could rely on. Inside one crate every `pub` item is visible everywhere, and `use crate::config` from the analysis code compiles without complaint. It did: the analysis code imported four configuration types, including the whole top-level config struct. A module boundary is only a convention, and only review attention enforces it. The hard part was the configuration edge. An `as:` entry in the configuration file names a compiler family, so a user-written string must become a compiler identity somewhere. The set of valid identities is generated from the compiler definition YAML. Before the split, the schema's serde deserializer did that, so the schema layer had to include generated code to know the family set. Options for that edge, each of which kept the coupling in another shape: - Keep the compiler-family type in the schema, with its serde derive. The smallest diff, but the schema then includes codegen output and is rebuilt whenever a compiler YAML file changes. That is the dependency the split removes. - Have the schema's validator call a parse function in the analysis core. This keeps validation at load time, but it is the same edge pointing the other way. A schema describes a file format and should not need the analysis core linked in to be understood. - Mirror the type in the schema with `serde(remote)`. This needs a hand-written shadow copy that tracks the original. The family set is generated, so there is no fixed variant list to mirror, and the copy still needs the generated ids to validate. The split also raises a test problem. A dependency's `cfg(test)` code is not built when a dependent's tests build, and the application's output tests use the analysis crate's command factories. An always-public module would ship test factories in release builds. A second copy in the dependent would drift from the original. ## Decision Compiler recognition and flag parsing live in their own library crate. It owns the compiler YAML and its codegen, and depends on the interception primitives only: no configuration types, no `clap`, no command-line arguments, no serde. The schema stores `as:` as the string the user wrote; the analysis crate resolves it, and the application layer, the only code that sees both, is the single conversion point. Test factories sit behind an off-by-default `testing` feature that dependents enable only as a dev-dependency. The module is gated on `any(test, feature = "testing")`, not `cfg(test)`, so a dependent's tests can see it. ## Consequences - The compiler enforces the dependency direction. A back-edge from the analysis core to the application is a dependency cycle and a build error, not a review finding. - The public API is deliberate. `lib.rs` states what dependents may touch, and widening it is a visible diff. - The manifest lists what analysis needs, and that turned out to exclude serde. A new dependency is a manifest line a reviewer sees. - Editing application code no longer reruns compiler codegen or rebuilds the analysis core, and the analysis tests build no application code. - A change that crosses the boundary touches two crates. In one workspace with no published versions that is a wider diff, not a compatibility problem. - `as:` is no longer checked during deserialization. An unknown spelling is reported after the configuration echo, not before it, and a launcher spelling such as `ccache` echoes back as written instead of as `wrapper`. Both were judged neutral or better. - The split changed no user-visible contract beyond those two, so it supports no requirement. ## References - [compiler-family-definition](compiler-family-definition.md), why the family set is generated data and when `as:` is resolved - [CLAUDE.md](../../CLAUDE.md), the crate roles and per-area routing ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/rationale/wrapper-mode-design-decisions.md�������������������������������0000664�0000000�0000000�00000006325�15271777203�0025700�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Wrapper mechanism: hard links, deterministic directory, resolve-at-startup ## Context The wrapper mechanism intercepts compilers by placing wrapper executables, named after the compilers, ahead of the real ones on PATH. Each wrapper reports the call and then runs the real compiler. Three setup questions follow, and each has a force that is not obvious from the code. How a wrapper file is made. The wrapper takes the compiler name from the file name of its own executable, and reads `wrappers.cfg` from the directory that holds it. It gets that path from the OS, and on Linux the OS resolves symlinks. A symlink to `bear-wrapper` was rejected for that reason: the wrapper would see itself as `bear-wrapper` in the install directory, find neither its compiler name nor its config, and fail. On Windows the wrapper file is a copy. Where the wrapper directory lives. Autotools-style builds record the compiler path during `./configure` and use it again during `make`. A random temporary directory was rejected: [#654](https://github.com/rizsotto/Bear/issues/654) showed that it breaks such multi-step builds, because the recorded path can point at a directory that a later step does not have. When a bare compiler name becomes a path. The wrapper must know which real compiler to run. Looking the name up on PATH in every wrapper call was rejected: it repeats the lookup for each compiler call, and the wrapper would depend on the PATH it runs with instead of on a self-contained configuration. ## Decision - Wrappers are **hard links** to `bear-wrapper`, not symlinks. On Windows they are copies, and elsewhere a copy is the fallback when the hard link fails. - The wrapper directory is a **deterministic** `.bear/` in the current working directory, not a random temp dir. - Bare compiler names are **resolved to absolute paths once at startup**, not on every wrapper invocation. ## Consequences - **Hard links, not symlinks.** Each wrapper sees its own name and its own directory, so it finds its compiler and its config. A copy works the same way, and covers Windows and file systems where hard links fail, such as overlay file systems in containers. The cost of a copy is disk space. The link type does not fix the ccache recursion problem: ccache does not recognize Bear's wrapper as itself. That is handled in [interception-wrapper-recursion](../requirements/interception-wrapper-recursion.md). - **Deterministic directory.** Paths recorded during `./configure` stay valid during `make`, provided both run under the same Bear invocation. - **Resolve at startup.** No repeated PATH lookups, and the wrapper configuration is self-contained. The trade-off is that a PATH change mid-build is not picked up; the wrapper keeps using the path resolved at startup. ## References - [interception-wrapper-mechanism](../requirements/interception-wrapper-mechanism.md), the requirement this supports - [interception-wrapper-recursion](../requirements/interception-wrapper-recursion.md), the ccache recursion the wrapper mechanism must avoid - [wrapper-recursion-ccache-alternatives](wrapper-recursion-ccache-alternatives.md), how the recursion is fixed - [#654](https://github.com/rizsotto/Bear/issues/654), temp directories break multi-step builds �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/rationale/wrapper-recursion-ccache-alternatives.md�����������������������0000664�0000000�0000000�00000007736�15271777203�0027452�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Wrapper recursion: why filter Bear's lookup PATH, not the alternatives ## Context With the wrapper mechanism, a compiler env var or PATH discovery can resolve to a masquerade wrapper (ccache, distcc, icecc, ...) rather than the real compiler. If Bear records that path, the build loops the wrapper into itself. Several fixes were considered before settling on "resolve past the masquerade dir and record the real compiler". How the loop arises: masquerade wrappers (ccache, distcc, icecream, colorgcc, buildcache) install a directory of symlinks named after real compilers and prepend it to PATH; a bare `gcc` then resolves to the wrapper, which looks up the real compiler further along PATH (skipping its own symlinks) and forwards the call. Bear's wrapper mechanism prepends its own directory of compiler-named links to PATH as well. A wrapper that recognises itself only by symlink comparison cannot tell Bear's link from a real compiler, selects it as the "real" `gcc`, and the two forward to each other without end. distcc avoids that specific loop by stripping every PATH entry up to and including its own directory, but that also drops Bear's directory, silently breaking nested interception. Options considered: - **Set `CCACHE_COMPILER` in the wrapper's child environment** (the original proposal). Rejected: the path the wrapper knows IS the ccache symlink (that is what `which gcc` returned at setup), and `CCACHE_COMPILER` pointing at a symlink-to-ccache makes ccache recurse into itself. Empirically verified on Fedora: `CCACHE_COMPILER=/usr/lib64/ccache/gcc ccache gcc -c foo.c` hangs and must be killed; `CCACHE_COMPILER=/usr/bin/gcc` works. Fixing it would require resolving past ccache to get the real path anyway, which is what the chosen fix does, making `CCACHE_COMPILER` redundant. It is also ccache-specific and would not help icecc or distcc. - **`CCACHE_PATH` set to PATH minus `.bear/`.** Rejected: ccache-specific (no equivalent for other wrappers), still requires enumerating a safe PATH, and does not address Bear's config pointing at the wrong executable. - **Removing masquerade directories from the child's PATH.** Rejected as heavy-handed: a masquerade dir might also hold binaries that do not loop (some installs put `distcc` itself there); stripping it globally would break those. - **Filter Bear's own lookup PATH.** Chosen: the narrowest intervention. ## Decision Filter Bear's own lookup PATH so resolution lands on the real compiler past any masquerade directory, and record that path. Do not touch the child's PATH or set ccache-specific variables. ## Consequences Detecting a masquerade wrapper by resolving the symlink of the discovered path, rather than matching a list of known directory locations, covers distribution-local masquerade setups automatically: any installer that symlinks compiler names to a wrapper binary is caught, with no per-distro path table to maintain. The cost is missing a masquerade installed as a shell script or hard copy, which no major distro ships. The set of wrapper names is fixed, and no subprocess is spawned to identify a wrapper. Detection stays a cheap filesystem check that runs nothing untrusted. The cost is missing uncommon or locally built wrappers. While Bear observes a build, the wrapped compilers bypass the masquerade wrapper, so ccache and its kin do not run for them. That is intended: Bear observes the build, it does not speed it up. ## References - [interception-wrapper-recursion](../requirements/interception-wrapper-recursion.md), the requirement this supports - [interception-wrapper-mechanism](../requirements/interception-wrapper-mechanism.md), a related requirement - [#445](https://github.com/rizsotto/Bear/issues/445), the original PATH-ordering report - [#686](https://github.com/rizsotto/Bear/issues/686), bare-name CC resolution - [ccache 4.x manual](https://ccache.dev/manual/4.10.2.html), the `CCACHE_COMPILER` and `CCACHE_PATH` settings - [icecream](https://github.com/icecc/icecream), its masquerade setup ����������������������������������rizsotto-Bear-0c30b27/docs/requirements/������������������������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0020266�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/requirements/CLAUDE.md���������������������������������������������������0000664�0000000�0000000�00000005325�15271777203�0021552�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������## Requirements directory Each file here is one contract: the *what* the user can expect from Bear. Tests verify the implemented ones. Does not belong here: - Bear's own CLI flags, config keys, config values, or schema fragments, in any section, acceptance criteria included. The ban is on the spelling the user types (`mode: wrapper`), not on the behavior it selects ("the preload mechanism is selected on Linux" is fine). Describe the behavior ("when appending", "a configuration option toggles inlining"); the man page (`man/bear.1.md`) names the flag or key. Flags of the compilers Bear classifies (`-c`, `--version`) are subject matter and do appear. - Copies of lists that a compiler YAML in `crates/semantic/compilers/` owns, such as flag or compiler names. State the rule ("module-interface flags are recognized") and let the YAML own the set, as `recognition-compiler-names` does. - Implementation guides, algorithm walkthroughs, error-handling tables. - Design reasons, trade-offs, rejected alternatives. - Bug reports and to-dos. Those go in the task tracker. ## Terms These apply to prose. Existing IDs and titles that use an old term are renamed in their area sweep, not ad hoc. - Run modes are named as in `man/bear.1.md`: combined mode, intercept mode, semantic mode, parse-sh mode. - Interception uses the preload mechanism or the wrapper mechanism, as in `interception-preload-mechanism` and `interception-wrapper-mechanism`, even where the man page and the config key say "mode". Never "preload mode", "wrapper mode", or "interception mode". - Append is a behavior ("when appending"), not a mode. ## Files and IDs A file is named `<area>-<short-name>.md`, for example `output-append.md`. The name without `.md` is the requirement ID. Write it that way, in backticks, in cross-references between requirement files (`output-append`) and in test tags. Fill [`TEMPLATE.md`](TEMPLATE.md) for the format. Tests cite IDs with the `Requirements:` tag described in [`../../tests/integration/CLAUDE.md`](../../tests/integration/CLAUDE.md). ## Status | Status | Meaning | |---|---| | `proposed` | Drafted, waiting for the user's review | | `accepted` | Approved by the user for implementation | | `implemented` | Code complete, tagged tests passing | | `rejected` | Declined by the user, or withdrawn | Only the user sets `accepted` or `rejected`. When the user approves or declines a requirement in the current session, the agent may set the status for them. The agent sets `implemented` once the code is done and the tagged tests pass. A changed contract goes back to `proposed`. Whether a declined idea keeps a `rejected` file or becomes a rationale entry is settled in [`../rationale/CLAUDE.md`](../rationale/CLAUDE.md). �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/requirements/TEMPLATE.md�������������������������������������������������0000664�0000000�0000000�00000002643�15271777203�0022030�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: <Short title of the contract> status: proposed --- <!-- Copy to <area>-<short-name>.md. Status values are in CLAUDE.md. --> ## Intent What the user expects to happen, from the user's point of view. ## Acceptance criteria Given-When-Then scenarios. Each one is a single observable outcome that a test can verify. Use plain rules or a table only where there is no natural Given, such as a format rule or a list of deliberately excluded names. - **Given** a build that compiles one source file, **when** Bear finishes, **then** the output holds one entry for that file. - **Given** a build that runs no compiler, **when** Bear finishes, **then** the output is an empty JSON array. A deliberate exclusion that is likely to be proposed again (a name not recognized, a mode not supported) is a criterion too. Its reasoning goes in a rationale entry linked below. ## Non-functional constraints Performance, platform support, backwards compatibility. Omit the section when there are none. ## Known limitations Gaps in the contract that are accepted rather than planned for fixing. Link the tracking issue when one exists. Omit the section when there are none. ## Rationale Links to the rationale entries under [`../rationale/`](../rationale/) that motivated this requirement, one short label per link. A `rejected` requirement also gives its reason for rejection here, in one line. Omit the section when it would be empty. ���������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/requirements/cli-diagnostic-format.md������������������������������������0000664�0000000�0000000�00000010636�15271777203�0024775�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: Diagnostic message format status: implemented --- ## Intent Bear prints diagnostics for two readers. A user running a build wants short, familiar messages that blend into a build log, the way `cc`, `make`, and `git` print theirs (`cc: error: ...`). A developer debugging Bear wants every detail. Bear runs as several processes (the driver, the wrapper, and the preloaded library inside each intercepted compiler), and their lines interleave on one terminal, so each line must say which process spoke, when, at what level, and from where in the code. The user view is the default. Opting into diagnostic logging switches every line to the developer view. The man page names the setting that does it. ## Acceptance criteria - Diagnostics go to standard error. Standard output carries only the tool's data, so the two never mix. When the database may go there is owned by [`output-json-compilation-database`](output-json-compilation-database.md). - Every line names the process that emitted it. The names are stable: the driver is `bear`, the wrapper is `wrapper`, and the preloaded library is `preload`. - In the user view a line reads `bear: <message>`. A warning adds `warning:` after the prefix and an error adds `error:` (`bear: warning: <message>`, `bear: error: <message>`). - In the user view only warnings and errors are shown. Skipped lines of shell text and rejected lines of an event stream are warnings. The run summary of the output stage is at info level, so it shows only when the user opts in. - Every diagnostic uses this format, errors that stop Bear before the build starts included. Errors in the command line itself are the exception: they come with the usage text, and their exit code is owned by [`cli-exit-codes`](cli-exit-codes.md). - An error or warning about a file names that file once, with what Bear was doing with it and the cause (`bear: error: cannot write <file>: <cause>`). It does not name Bear's internal components or stages, and it does not repeat the same failure at several levels. - **Given** parse-sh mode reading shell text with an unsupported line and no opt-in to logging, **when** the user runs it, **then** the skip is reported on standard error as `bear: warning: <message>`, and standard output carries only the database. - **Given** a build that gives no warnings or errors and no opt-in to logging, **when** the user runs it under Bear, **then** Bear writes nothing to standard error. - **Given** the wrapper mechanism and a wrapper that fails to report an execution, **when** the failure is logged without opt-in, **then** the line starts with `wrapper: error:`. - **Given** an output path in a directory that does not exist, **when** the user runs a build, **then** before the build runs Bear prints one line, `bear: error: cannot create <output>: <cause>`. - **Given** an output that fails while it is written (a full device), **when** Bear writes it, **then** Bear prints one line, `bear: error: cannot write <output>: <cause>`. - **Given** a named input file that does not exist, **when** semantic mode or parse-sh mode runs, **then** Bear prints one line, `bear: error: cannot read <input>: <cause>`. - **Given** a build command that cannot be started, **when** the user runs it under Bear, **then** Bear prints one line, `bear: error: cannot run '<command>': <cause>`. - The messages about a previous database when appending are owned by [`output-append`](output-append.md). - **Given** an opt-in to diagnostic logging, **when** Bear runs, **then** every line carries a timestamp, the level, the process name with its process id, and the source location in Bear, then the message. The requested level sets which lines appear. An empty setting counts as no opt-in. - **Given** an opt-in at debug level and a build whose compiler starts subprocesses, **when** the user runs it under the preload mechanism, **then** standard error holds lines from `bear` and lines from `preload`, each with its own process id, so interleaved lines stay apart. ## Non-functional constraints - All diagnostic text is ASCII. - The cause part of a message is the operating system's own text where the failure comes from it, so its wording varies by platform. - The format is a contract on standard error only. Changing it does not change exit codes or the compilation database. - The man page ([`../../man/bear.1.md`](../../man/bear.1.md)) documents both views for users. ��������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/requirements/cli-exit-codes.md�������������������������������������������0000664�0000000�0000000�00000010546�15271777203�0023427�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: Exit codes by mode status: implemented --- ## Intent Shells, CI runners, and parent `make` rules read Bear's exit code to decide whether a run worked. When Bear runs a build, the exit code reports the build's result. When Bear runs without a build, it reports whether Bear could do its own job. This file owns the whole exit-code contract, so a caller never has to piece it together from the mode requirements. ## Acceptance criteria | Mode | Outcome | Exit code | |---|---|---| | any | help is requested | `0` | | any | the command line is malformed | non-zero, before any work runs | | any | an error of Bear's own (an input cannot be read, the output cannot be created or written, the build cannot start) | non-zero | | combined, intercept | the build exits on its own | the build's code | | combined, intercept | the build is killed by a signal | non-zero | | combined, intercept | entries are dropped by output validation | the build's code | | semantic, parse-sh | the input is empty, or is understood even when the output is empty | `0` | | semantic, parse-sh | non-empty input in which nothing is understood | non-zero | Which command lines are malformed is owned by the mode requirements, for example [`interception-events-format`](interception-events-format.md) for the event-file usage errors. - **Given** a build that exits `0`, **when** it runs under Bear in combined mode, or in intercept mode with an output file, **then** Bear exits `0`. - **Given** a build that exits `42`, **when** it runs under Bear in either mode, **then** Bear exits `42`. Codes in the portable range (0-255 on Unix) pass through unchanged. - **Given** a build killed by a signal, so it has no exit code, **when** Bear finishes, **then** Bear exits non-zero, never `0`. How the signal reaches the build is owned by [`cli-signal-forwarding`](cli-signal-forwarding.md). - **Given** a build that handles a forwarded signal and exits on its own, **when** Bear finishes, **then** Bear exits with the build's code, `0` included. - **Given** a build that exits `0` and an output destination that becomes unwritable during the build, so the final rename fails, **when** Bear finishes, **then** Bear exits non-zero although the build succeeded. The write itself is owned by [`output-atomic-write`](output-atomic-write.md). - **Given** a build that exits `0` but where every candidate entry fails output validation, **when** Bear finishes, **then** the database is an empty array and Bear exits `0`. The validation rules are owned by [`output-json-compilation-database`](output-json-compilation-database.md). - **Given** semantic mode reading a conforming events file, **when** it runs, **then** Bear exits `0`. This holds when every event is understood but yields no valid entry (an event whose working directory is empty), and the database is an empty array. - **Given** semantic mode reading non-empty input in which every line is non-conforming, **when** it runs, **then** Bear exits non-zero. - **Given** parse-sh mode reading shell text with at least one line in the supported subset, **when** it runs, **then** Bear exits `0`. This holds when the text names no compiler (a lone `mv` line), and the database is empty. - **Given** parse-sh mode reading non-empty input in which every line is an unsupported construct, **when** it runs, **then** Bear exits non-zero. - **Given** semantic mode or parse-sh mode with a named input file that does not exist, **when** it runs, **then** Bear exits non-zero. - **Given** a command line with no build command or with an unknown argument, **when** the user runs Bear, **then** it exits non-zero and prints the usage or the error before running anything. - **Given** a request for help, for Bear or for one of its modes, **when** the user runs it, **then** Bear prints the usage and exits `0`. The skip-and-continue rule for input lines is owned by [`interception-events-format`](interception-events-format.md). ## Known limitations - Bear does not encode the signal that killed a build in its exit code (no `128 + signal number`), so a caller that reads only the exit code cannot tell a killed build from a failed one. The build still gets the real signal. - Nested Bear runs (Bear running under another Bear) are not supported, and their exit code is undefined. The exclusion is owned by [`cli-signal-forwarding`](cli-signal-forwarding.md). ����������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/requirements/cli-signal-forwarding.md������������������������������������0000664�0000000�0000000�00000010344�15271777203�0024774�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: Signal forwarding to the build status: implemented --- ## Intent When the user runs a build under Bear in a terminal and presses Ctrl-C (`SIGINT`), the interrupt must reach the build. Bear must not swallow it and leave `make` and the whole process tree under it running in the background. The same holds when a CI runner sends `SIGTERM` to Bear to abort a build: the build must stop too. This file covers how the signal reaches the build and how the process tree is torn down. Bear's exit code, after a signal included, is owned by [`cli-exit-codes`](cli-exit-codes.md). The contract holds the same with the preload mechanism and with the wrapper mechanism; which one runs is owned by [`interception-mechanism-selection`](interception-mechanism-selection.md). ## Acceptance criteria - **Given** a build running under Bear (`sleep 10`), **when** the user presses Ctrl-C, or `SIGTERM` (or `SIGQUIT` on Unix) is sent to Bear, after the sleep starts, **then** the build stops, Bear and the sleep both end in less than one second on a system not under heavy load (the time budget), and Bear exits with a non-success status. - **Given** a compiler blocked mid-compile (its source is a named pipe with no writer), **when** a termination signal is sent to Bear, standing in for Ctrl-C, **then** Bear and the compiler both end within the time budget. - **Given** a build script with distinct traps for `SIGINT` and `SIGTERM`, each writing its own marker file and exiting with its own status, **when** `SIGINT` is sent to Bear, **then** the build gets `SIGINT`, not a substitute: only the `INT` marker appears and the `INT` trap's exit status passes through (as `cli-exit-codes` states). **When** `SIGTERM` is sent instead, **then** only the `TERM` marker appears and the `TERM` trap's exit status passes through. - **Given** a build script that traps `SIGTERM`, writes a marker from the trap, and exits with its own status, **when** `SIGTERM` is sent to Bear while the script sleeps, **then** the build is first asked to stop with `SIGTERM` and gets a short grace period: the marker appears, so the trap ran rather than the script being killed, and the trap's exit status passes through (as `cli-exit-codes` states). - **Given** a build script that starts a background grandchild (`sleep 60 &`) and then sleeps itself, **when** `SIGTERM` is sent to Bear, **then** Bear exits with a non-success status and the grandchild is gone. The whole tree is stopped, not just the direct child. On Linux with a usable cgroup this holds even when the grandchild starts its own session with `setsid`. - **Given** a build script that ignores `SIGTERM` and loops forever, **when** `SIGTERM` is sent to Bear, **then** the grace period runs out, the build is forced to stop, both end within the time budget, and Bear exits with a non-success status. - **Given** the wrapper mechanism and a build that runs `$CC -c` on a named pipe with no writer, so the compiler blocks under the wrapper, **when** `SIGTERM` is sent to Bear, **then** Bear and the blocked compiler both end within the time budget, and Bear exits with a non-success status. ## Non-functional constraints - Platform support: Linux, macOS, BSD, and Windows. - Bear does not interfere with the build tool's own signal handling. - Running a build under Bear adds no noticeable delay compared with running the same command directly. ## Known limitations - A child that starts a new session of its own (a daemon that calls `setsid`) leaves the build's process group and may outlive Bear. On Linux with a usable cgroup, Bear stops the build's cgroup instead, which a child cannot leave. Without one, the limitation applies. - On Windows only `SIGTERM` and `SIGINT` are observed. Other ways to end a process (`CTRL_BREAK_EVENT`, a parent calling `TerminateProcess` on Bear) are not handled. - Nested Bear runs (Bear running under another Bear) are not supported, and their signal and exit-code behavior is undefined. To record several builds into one database, run them one after the other while appending ([`output-append`](output-append.md)). ## Rationale - [Supervise the build](../rationale/process-tree-teardown.md) - [Supervise a compiler call](../rationale/supervise-a-compiler-call.md) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/requirements/interception-cc-env-var-flags.md����������������������������0000664�0000000�0000000�00000005565�15271777203�0026357�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: Handle compiler env vars that contain flags status: implemented --- ## Intent Compiler environment variables often carry the compiler and a simple trailing flag or two, as in `CC="gcc -std=c11"` or `CXX="clang++ -stdlib=libc++"`. This is a GNU Make convention seen in configure scripts, Makefiles, and Dockerfiles. When the user runs a build under Bear with the wrapper mechanism and such a value, Bear wraps the real program and rewrites the variable so the build still gets the flags. ## Acceptance criteria - **Given** `CC="fake-cc -std=c11"` and `fake-cc` on PATH, **when** Bear sets up the wrapper mechanism, **then** the wrapper for `fake-cc` runs the real compiler, and the build sees `CC` as `<wrapper path> -std=c11`. - **Given** `CC="/abs/path/my-gcc -m32"`, **when** Bear sets up the wrapper mechanism, **then** the wrapped program is `/abs/path/my-gcc`, and the rewritten value keeps `-m32`. - **Given** a C source file, a build script that runs `$CC -c test.c`, and `CC="<compiler> -DBEAR_TEST_FLAG=1"`, **when** Bear finishes, **then** the build succeeds, and the database holds exactly one entry whose arguments contain `-DBEAR_TEST_FLAG=1`. - **Given** a value with flags, **when** Bear rewrites it, **then** the new value is the wrapper path and the original flag tokens, joined by single spaces, with no shell quoting added. With no flags, the new value is the wrapper path alone, with no quoting added. - **Given** a value with quotes, shell metacharacters, or a command substitution, **when** Bear reads it, **then** none of these has a special meaning: the value is split on whitespace only. - **Given** a ccache masquerade directory on PATH and a real compiler past it, **when** `CC="gcc -std=c11"`, **then** the wrapper runs the real compiler past the masquerade directory, and the rewritten `CC` still holds `-std=c11`. Masquerade handling is owned by `interception-wrapper-recursion`. - **Given** a Unix-like shell on Windows with forward-slash paths, **when** `CC="C:/tools/fake-cc.exe -DBEAR_TEST=1"`, **then** a wrapper is registered for `fake-cc.exe`, and the rewritten `CC` still holds `-DBEAR_TEST=1`. - **Given** `CC=""` or `CC=" "`, **when** Bear sets up the wrapper mechanism, **then** the variable is not rewritten, and a warning is logged. A value without flags, or an unset variable, follows the ordinary resolution owned by `interception-wrapper-mechanism`. ## Non-functional constraints - Platform support: Unix and Unix-like environments: Linux, macOS, BSD, and the Unix-like shells hosted on Windows (MSYS2, Git Bash, WSL). ## Known limitations - No effect on a native Windows build: native Windows build tooling (MSBuild, `nmake`, `cmd`, PowerShell) does not read `CC`/`CXX` from the environment. ## Rationale - [Compiler env var parsing: whitespace split, no shell parser](../rationale/compiler-env-flag-parsing.md) �������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/requirements/interception-events-format.md�������������������������������0000664�0000000�0000000�00000013265�15271777203�0026112�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: Events file as external interchange format status: implemented --- ## Intent Intercept mode writes the executions it captures to an events file the user names. Semantic mode reads an events file and produces a compilation database. The user expects the events file to be a documented interchange format: its schema, encoding, and stability are written down, so outside tools can produce or consume it without reading Bear's sources. One use is to turn an existing build log into a compilation database without running the build again. ## Acceptance criteria Format rules: - One line of the events file is one JSON object that describes one execution. Lines end with `\n`. The file is UTF-8, with no comments and no trailing commas. - Each event object has exactly these four keys, all required. A line that misses one, or holds one with the wrong JSON type, does not conform. | JSON key | Type | Meaning | |---------------|--------------------------|----------------------------------------------------------------------------| | `executable` | string (filesystem path) | Path to the program run. Absolute, or a bare name as the caller spelled it. | | `arguments` | array of strings | The argument vector. Element zero is the program name (`argv[0]`). | | `working_dir` | string (filesystem path) | Absolute working directory the program ran in. | | `environment` | object (string->string) | Environment variables in effect for the program. | - The stable part is the four key names and their JSON types. Changing any of them needs a major-version bump of the format. Across major versions, compatibility may break. - The contents of `environment` are advisory, not stable. Every Bear producer keeps only the variables that can change which build tool runs or how it compiles. Which variables those are may change between releases, with one fixed rule: `PATH`, when captured, is kept, so a consumer can resolve bare executable names. A consumer must not assume any other variable is present. A third-party producer may include or leave out variables freely. The database records the compiler as spelled, never resolved (owned by [`output-json-compilation-database`](output-json-compilation-database.md)). Scenarios: - **Given** an events file from a third-party tool that conforms to the schema, **when** semantic mode reads it, **then** the database holds one entry per recognized compiler call in the file. - **Given** an events file from a successful intercept mode run, by the same Bear or by an earlier release in the same major version of the format, **when** semantic mode reads it, **then** the run succeeds and the database equals the one combined mode gives for the same build. - **Given** the same three compiler events written to one file in order and to a second file reversed, **when** semantic mode reads each file, **then** both databases hold the same three entries. Only their order may differ. Order rules when appending are owned by [`output-append`](output-append.md). - **Given** an events file with one malformed line (invalid JSON, a missing key, a wrong type) in the middle, **when** semantic mode reads it, **then** Bear reports the line number and the reason, keeps reading the lines after it, and writes a database from the valid lines. Whether the run fails, for example when every line is rejected, is owned by [`cli-exit-codes`](cli-exit-codes.md). Parse-sh mode uses the same skip-and-continue rule for its own input ([`interception-shell-text-parsing`](interception-shell-text-parsing.md)). - **Given** a producer piped into semantic mode with no input file named, **when** semantic mode runs, **then** it reads the events from standard input, and the database holds one entry per recognized compiler call. Diagnostics go to standard error. - **Given** semantic mode reading an empty input, from a named file or from standard input, **when** it runs, **then** it writes an empty database and prints a warning on standard error that the input held no events. The exit code is owned by [`cli-exit-codes`](cli-exit-codes.md). - **Given** semantic mode reading standard input that is a terminal, **when** it starts, **then** it prints a warning on standard error. Both warnings are shown by default; levels are owned by [`cli-diagnostic-format`](cli-diagnostic-format.md). - **Given** intercept mode with no events file named, **when** the user runs it, **then** Bear rejects the command line before the build runs. There is no default events file. The exit code is owned by [`cli-exit-codes`](cli-exit-codes.md). - **Given** intercept mode with standard output as the events file, **when** the user runs it, **then** Bear exits with an error before the build runs, the error says the events cannot go to standard output, and no file named `-` is created. Standard output as the database destination is owned by [`output-json-compilation-database`](output-json-compilation-database.md). - The events file is written all or nothing, as owned by [`output-atomic-write`](output-atomic-write.md). ## Non-functional constraints - The format round-trips: events written by intercept mode are always accepted by semantic mode. A break in either direction is a bug. - The format stays JSON Lines, not a single JSON array, so a producer can stream it and a truncated file still yields its complete lines. - The schema in this file is normative for the events file. ## Rationale - [No implicit event-file name: intercept names it, semantic reads stdin](../rationale/event-file-defaults.md) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/requirements/interception-mechanism-selection.md�������������������������0000664�0000000�0000000�00000004733�15271777203�0027247�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: Interception mechanism selection status: implemented --- ## Intent The user runs a build under Bear without choosing how the interception happens. Bear has two interception mechanisms: the preload mechanism (`interception-preload-mechanism`) and the wrapper mechanism (`interception-wrapper-mechanism`). It selects exactly one of them per invocation, before the build starts. The default fits the platform; a configuration option forces either mechanism for users who need the other one. Whichever runs, the build behaves as it would without Bear. ## Acceptance criteria - **Given** any build, **when** Bear runs it, **then** exactly one mechanism is active, selected before the build starts, and it does not change mid-build or per intercepted process. - **Given** a Linux or BSD host and no interception configuration, **when** the user runs a shell script in intercept mode, **then** the shell and the processes it spawns are all reported. This shows the preload mechanism is active, since the wrapper mechanism never sees the shell. - **Given** a macOS or Windows host and no interception configuration, **when** the user runs a build under Bear, **then** the wrapper directory is present in the working directory while the build runs. - **Given** a Linux host and a configuration that forces the wrapper mechanism, **when** the user runs a build under Bear, **then** the wrapper directory is present in the working directory while the build runs. - **Given** a configuration that forces the preload mechanism on a host where library injection is unavailable (Windows, or macOS while System Integrity Protection is enabled, checked at startup), **when** the user runs a build under Bear, **then** Bear reports an error that names the wrapper mechanism as the alternative, and the build is not started. Bear never silently switches to the other mechanism. - **Given** either mechanism, **when** the build runs, **then** its output is the same as without Bear. Bear's exit code is owned by `cli-exit-codes`. - **Given** either mechanism and a report that cannot reach Bear, **when** the build runs `cc -c test.c`, **then** the compiler still runs, the build's output and exit code are unchanged, and the only effect is that the entry for `test.c` is missing from the database. - **Given** either mechanism and a parallel build of four source files (`make -j4`), **when** Bear finishes, **then** all four compilations appear in the database. No report is lost. �������������������������������������rizsotto-Bear-0c30b27/docs/requirements/interception-preload-mechanism.md���������������������������0000664�0000000�0000000�00000012131�15271777203�0026677�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: LD_PRELOAD-based command interception status: implemented --- ## Intent With the preload mechanism, Bear injects a shared library into every process the build starts, and so sees every program execution during the build. The user does not change the build system or install compiler wrappers; any build tool that spawns compilers works. Linux and the BSDs use `LD_PRELOAD`; macOS uses `DYLD_INSERT_LIBRARIES`. Which mechanism runs is owned by `interception-mechanism-selection`, and so is the promise that interception leaves the build's output alone. ## Acceptance criteria - **Given** a build that launches `cc -c a.c` through an `exec` family call, `cc -c b.c` through `posix_spawn`, `cc -c c.c` through `popen`, and `cc -c d.c` through `system`, **when** Bear finishes, **then** all four compilations appear in the database (Android is an exception, see Known limitations). - **Given** a build step that clears its environment (`env -i cc -c test.c`) and launches the compiler through a call that takes an explicit environment, **when** the build runs, **then** the preload library restores its variables in the child, and the compilation appears in the database. - **Given** a build that runs `cp`, `mkdir`, and `cc -c test.c`, **when** the build runs, **then** all three executions are reported. Which reported executions become entries is owned by `recognition-compiler-names`. - **Given** `LD_PRELOAD=/usr/lib/libsandbox.so` set before Bear starts, **when** the build runs, **then** the preload variable holds Bear's library first, followed by `/usr/lib/libsandbox.so`, and both stay in the child processes. - **Given** a build whose last compiler reports just before the build exits, **when** that report races the end of interception, **then** it is still delivered and the last compilation appears in the database (issue #704). - **Given** the preload mechanism on a macOS host with System Integrity Protection disabled, **when** the build spawns a compiler, **then** `DYLD_INSERT_LIBRARIES` is set in the child processes and compiler invocations are intercepted as on Linux. ## Non-functional constraints - Platform: Linux (including Android) and the BSDs use `LD_PRELOAD`; macOS uses `DYLD_INSERT_LIBRARIES`. Not available on Windows, nor on macOS while System Integrity Protection is enabled. - Bear finds the preload library at run time, relative to where it is installed, not at a path fixed when Bear was built (issues #648, #649, #597, #582). ## Known limitations **Statically linked executables**: a statically linked executable does not load the dynamic linker, so the preload library cannot intercept it. **Environment stripped before `execvp`** (and other `exec` variants without an explicit environment argument): the preload library restores the preload variable only when the call passes an explicit environment. If the build removes `LD_PRELOAD` from its environment and then spawns children via `execvp`, grandchild processes may not be intercepted. **Wrong ELF class during cross-compilation** (issue #236): the preload library is built for the host architecture. When the build runs a cross-compiler built for a different architecture, the dynamic linker rejects the library with "wrong ELF class". The build still completes, but those commands are not intercepted. **glibc symbol-version mismatch in cross-compilation** (discussion #707): the preload library is linked against the host's glibc. Loaded into a compiler that runs against an older-glibc sysroot, it may need a newer glibc symbol version. That invocation fails with a "version not found" error and the command is not recorded. **macOS SIP** (issue #558): System Integrity Protection strips `DYLD_INSERT_LIBRARIES` for protected executables, so the preload library cannot intercept them. What Bear does when the preload mechanism is selected there is owned by `interception-mechanism-selection`. **Preload conflicts with sandboxes** (issue #699): a co-resident `LD_PRELOAD` sandbox library that hooks the same `exec` family can reset its own preload variable further down the exec chain and drop Bear's entry. A grandchild past that point is not intercepted. **`popen` and `system` on Android**: on Android these two calls are not intercepted, so a compiler launched through them is missing from the database. The `exec` family is intercepted as on other Linux systems. **`posix_spawn` on older Android API levels**: Android has `posix_spawn` and `posix_spawnp` from API level 28. A Bear built for an older API level does not intercept them, even on a newer device, so a compiler launched through them is missing from the database. **Affects all child processes** (issue #444): the preload variable applies to every process the build spawns, not only compilers. This can disturb tools that are sensitive to preloaded libraries. ## Rationale - [Linker selection for the preload library](../rationale/preload-linker-selection.md) - [Preload chains to co-resident preload libraries](../rationale/preload-chains-to-co-resident-libraries.md) - [How the build finds the functions the preload library wraps](../rationale/preload-symbol-probes.md) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/requirements/interception-shell-text-parsing.md��������������������������0000664�0000000�0000000�00000026706�15271777203�0027056�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: Compilation database from shell command text status: implemented --- ## Intent A user has the text a build system prints when asked to show commands without running them, such as `make -n` output or a saved build log. The user wants a compilation database from it in one step, without paying for a full build: ```sh make -n | bear parse-sh ``` Parse-sh mode rebuilds the executions the text describes. They go through the same semantic analysis and output stage as in every other mode that writes a database. The database is the mode's only product. This is a best-effort front-end, not a replacement for interception. Interception sees real `exec()` calls and stays the recommended path. Parse-sh mode rebuilds approximate executions from text the build system chose to print, so its fidelity is bounded by that text. The man page and the known limitations below must say so. ## Acceptance criteria ### Mode shape - The mode reads shell text from standard input by default, or from a named file. - The database follows the same output contracts as the other modes that write one ([`output-json-compilation-database`](output-json-compilation-database.md), [`output-append`](output-append.md), [`output-atomic-write`](output-atomic-write.md), and the rest of the `output-` set). The default destination is `compile_commands.json`. - Streaming the database to standard output is owned by [`output-json-compilation-database`](output-json-compilation-database.md). - **Given** an existing database and new shell text, **when** the user runs the mode against the same destination while appending, **then** the result is the merged set owned by [`output-append`](output-append.md). - **Given** the zlib fixture and a configuration that widens duplicate detection to compare full argument lists, **when** the user supplies it, **then** the database keeps the entries the default duplicate detection folds away, and the skip report on standard error is the same with and without it. Configuration shapes recognition, duplicate detection, filtering, and formatting as in the other modes. It never changes how the text is parsed. - **Given** the line `gcc -c foo.c` from a log captured elsewhere and a caller-set initial working directory of `/custom/build`, **when** the mode parses it, **then** the entry's directory is `/custom/build`, not the mode's own working directory. - The mode offers no view of the rebuilt executions. Parsed commands that are not compiler calls (`ar`, `ln`, a linker line) appear nowhere. The skip warnings on standard error are the only window into the parse. ### Candidates - **Given** the line `gcc -c foo.c` in a file named `build.sh`, **when** the user runs the mode with `build.sh` as input and `db.json` as output, **then** `db.json` holds exactly one entry: file `foo.c`, compiler spelled `gcc`, directory the mode's working directory. Standard output carries nothing. - Each line that lexes as a simple command yields one candidate execution, except `cd` (see the table). The parser does not decide what a compiler is. Compiler recognition and output validation decide which candidates become entries, as for intercepted executions. A candidate that is not a compiler gives no entry and no diagnostic. - **Given** input whose only line is `mv objs/a.o a.lo`, **when** the mode parses it, **then** no line is skipped, no diagnostic names the line, and the database is written empty. The exit code is owned by [`cli-exit-codes`](cli-exit-codes.md). - A candidate's executable is the first word that is not a leading assignment, taken as written. Bare names stay bare. - A candidate's environment is the mode's own environment at parse time, with that command's leading `VAR=value` words laid over it. It reaches semantic analysis as an intercepted execution's environment does, seen through [`output-env-derived-flags`](output-env-derived-flags.md). - **Given** the line `CPATH=/opt/include gcc -c foo.c -o foo.o >/dev/null 2>&1` and environment-derived flags enabled (the default), **when** the mode parses it, **then** the entry names the compiler `gcc`, its arguments carry no redirection words, and they include an include flag for `/opt/include`. - The working directory of each candidate is tracked and becomes the entry's directory. It starts at the mode's own working directory, or at the caller-set one. ### Shell subset The mode handles a small, fixed subset of POSIX `sh`. A construct marked skipped makes the whole line skipped: nothing on that line becomes a candidate, and the line is reported (see skip reporting). The mode never guesses at the meaning of a skipped construct. | Construct | Handling | |---|---| | Word splitting on unquoted whitespace | Handled | | Single quotes, double quotes, backslash escapes | Handled | | Line continuation (backslash-newline) | Handled. The lines join into one | | Separators: newline, `;`, `&&`, `\|\|`, `&`, `\|` | Handled. Each command is a candidate, both sides of a pipe included | | Comments (`#` to end of line), blank lines | Handled. They produce nothing | | Leading `VAR=value` words | Handled. Environment for that command, never the executable | | Redirections (`>`, `>>`, `<`, `2>`, `2>&1`, `>/dev/null`, and the like) | Handled. Removed from the words. The target word is discarded unread, so an expansion or substitution inside it does not skip the line | | Unterminated quote or substitution in a redirection target | Skipped | | Brace group `{ ...; }` in command position | Handled. The braces give no candidate. The commands inside share the shell state, so a `cd` inside persists after `}`, as in `sh`. Groups may nest and span lines | | `}` with no open group | Skipped, as unbalanced braces | | Input ending with a `{` still open | Reported as unbalanced braces against the line the `{` opened on. The commands inside the group are kept | | `cd <dir>` | Handled. Updates the working directory for later commands and gives no candidate, since `cd` is a builtin that interception never sees | | `cd -` | Handled when a previous directory is known: restores it. Skipped otherwise, with the working directory unchanged. The `-` is never a directory name | | make's `Entering directory '...'` and `Leaving directory '...'` markers | Handled, as an explicit extension to `sh`. They push and pop the working directory, so recursive `make -n -w` output tracks directories | | Subshell `( ... )` | Skipped. It runs in a child shell, unlike a brace group | | Command substitution (backticks, `$(...)`) | Skipped | | Parameter expansion (`$VAR`, `${...}`) | Skipped | | Glob in the executable word | Skipped | | Here-document | Skipped | | Shell keyword (`case`, `for`, `while`, `if`, and the like) in command position | Skipped | | Unterminated quote | Skipped | - **Given** this input, ```sh gcc -DNAME='a b' -c \ foo\ bar.c && ar rc libz.a foo.o | tee log # archive it gcc -c baz.c >/dev/null 2>&1 || true ``` **when** the mode parses it, **then** the database has exactly two entries: `foo bar.c` with the single argument word `-DNAME=a b`, and `baz.c` with no redirection words. `ar`, `tee`, and `true` give no entries, the comment and the blank line give nothing, and no line is skipped. - **Given** the lines `gcc -c foo.c > $(logdir)/x.log` and `cmd > 'oops`, **when** the mode parses them, **then** the first gives an entry for `foo.c` and no skip, and the second gives nothing and is skipped as an unterminated quote. - **Given** this input, starting in `/build`, ```sh { cd sub; gcc -c a.c; } gcc -c b.c ``` **when** the mode parses it, **then** the braces give no entry and both compilations have directory `/build/sub`. A `}` with no open group (`echo a; }`) skips its line, and an input that ends with a `{` still open reports unbalanced braces against the line it opened on and still keeps the commands inside the group. - **Given** this input, starting in `/build`, ```sh cd sub gcc -c a.c cd - gcc -c b.c ``` **when** the mode parses it, **then** neither `cd` line gives an entry, `a.c` has directory `/build/sub`, and `b.c` has `/build`. An input whose first command is `cd -` has that line skipped and the working directory left unchanged. - **Given** a recursive-make log with `Entering directory` and `Leaving directory` markers around compiler lines without `cd`, **when** the mode parses it, **then** each entry's directory is the one in effect at that line. ### Skip reporting - Each skipped line is reported on standard error as a warning, with its line number and reason. When any line is skipped, a summary warning counts the commands parsed and the lines skipped. Warnings are shown by default; levels are owned by [`cli-diagnostic-format`](cli-diagnostic-format.md). - **Given** `gcc -c foo.c` followed by `for f in *.c; do gcc -c $f; done`, **when** the mode runs, **then** standard error carries the skip warning and a summary of one command parsed and one line skipped. The `for` line counts once, not once per command inside it. - **Given** input with nothing skipped, **when** the mode runs, **then** it prints no warning. Its summary is at info level, shown only when the user opts in. - **Given** empty input, **when** the mode runs, **then** it prints a warning that no commands were found. - **Given** real `make -n` output from the zlib fixture, **when** the user pipes it to the mode, **then** `compile_commands.json` is written at the default destination and covers every compiler call in the text, the non-compiler lines (`ar`, `mv`, `mkdir`, `ln`) give no entries and no diagnostics, and the lines outside the subset (subshells, substitutions) are reported as skips but are not fatal. - **Given** input in which every line is skipped, **when** the mode runs, **then** it reports each line and the run fails. The exit code is owned by [`cli-exit-codes`](cli-exit-codes.md). ## Known limitations These hold however good the parser is, because they come from the dry-run text itself. The man page must state them too. - Dry-run output can leave out commands. Recursive make may not pass the dry-run flag on. Commands behind sources that are not generated yet never appear, because the generator did not run. `$(shell ...)` evaluated at parse time can differ from a real build. - The build system must support a dry run and print real commands in it. Silent rules, custom launchers, and response files lower fidelity. - The rebuilt execution context is approximate. The environment and the `PATH` used to resolve bare names are the mode's own at parse time and may differ from the real build's. A log from another machine implies a foreign environment and `PATH` that the mode cannot rebuild, even when the caller sets the working directory. - Input is POSIX `sh` text only. Other shells and Windows `cmd` are out of scope. A saved full-build log works only as far as its lines are `sh` commands. Interleaved compiler output is skipped like any other unsupported line. - Whether a source file exists on the parsing machine does not change the output: entries come from the text alone, so a log whose sources are absent still gives a full database. The exception is the path format that resolves paths against the real filesystem. It needs the sources present. The man page names the alternative for that case. ## Rationale - [parse-sh: a quarantined, best-effort parser whose only product is the database](../rationale/parse-sh-producer.md) - [A single streaming tokenizer for parse-sh](../rationale/parse-sh-single-tokenizer.md) ����������������������������������������������������������rizsotto-Bear-0c30b27/docs/requirements/interception-wrapper-mechanism.md���������������������������0000664�0000000�0000000�00000013050�15271777203�0026732�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: Wrapper-based command interception status: implemented --- ## Intent With the wrapper mechanism, Bear puts wrapper executables, named after the compilers, in a wrapper directory first on PATH, and points the compiler program variables at them. The build runs the wrapper instead of the real compiler. The wrapper reports the execution to Bear and runs the real compiler. The user gets a compilation database without changing the build system. Which mechanism runs is owned by `interception-mechanism-selection`, and so is the promise that interception leaves the build's output alone. The compiler program variables are the variables that name a program. From make's implicit variables: `CC`, `CXX`, `CPP`, `FC`, `AR`, `AS`, `M2C`, `PC`, `LEX`, `YACC`, `LINT`. From CMake's language variables: `CUDACXX`, `CUDAHOSTCXX`, `HIPCXX`, `HIPHOSTCXX`, `ISPC`, `OBJC`, `OBJCXX`, `RC`, `SWIFTC`, `SYCLCXX`, and the assembler variables `ASM`, `ASM_NASM`, `ASM_MASM`, `ASM_MARMASM`, `ASM_POASM`, `ASM-ATT`. ## Acceptance criteria - **Given** a build that runs `cc -c test.c`, **when** Bear finishes, **then** the database holds one entry for `test.c`, and the entry names the real compiler by its absolute path, never the wrapper path. - **Given** a wrapped compiler that runs another wrapped compiler by its bare name, **when** the build runs, **then** the wrapper directory is still first on PATH for that process, and the inner call is intercepted too. - **Given** `CC=gcc` (a bare name) or `CC=/usr/bin/gcc` (an absolute path), **when** the build runs, **then** Bear resolves the bare name via PATH to `/usr/bin/gcc`, wraps `gcc` in both cases, and the entry names `/usr/bin/gcc` as the compiler. Values that carry flags are owned by `interception-cc-env-var-flags`. - **Given** `CFLAGS` set in the build environment, **when** the build runs, **then** the build sees it unchanged. - **Given** `RUSTC`, `RUSTC_WRAPPER`, or `CARGO` set in the build environment, **when** the build runs, **then** the build sees the value unchanged and Bear wraps no program for it. - **Given** a configuration that lists no compiler for interception (entries excluded from the database do not count), **when** Bear starts, **then** it scans PATH and wraps every compiler it recognizes there. Which names it recognizes, cross-compilation prefixes included, is owned by `recognition-compiler-names`. - **Given** a configuration that lists at least one compiler for interception, **when** Bear starts, **then** it wraps exactly the listed paths, whatever their names (an unusual cross-compiler included), and skips the PATH scan. - **Given** a compiler named in a compiler program variable, **when** Bear starts, **then** it is wrapped, whether or not the configuration lists compilers, and the build sees the wrapper in that variable. - **Given** a build that runs `/opt/custom/bin/mycc -c test.c` directly, with `mycc` not named in a compiler program variable and not listed in the configuration, **when** Bear finishes, **then** that invocation is not intercepted. - **Given** a build under Bear, **when** the build runs, **then** the wrapper directory `.bear/` exists in the working directory, and **when** Bear exits, **then** it is removed and no wrapper artifacts remain. - **Given** two Bear runs in the same working directory, **when** each runs its build, **then** each uses the same wrapper directory path. - **Given** one Bear invocation that runs `./configure` and then `make` (`sh -c './configure && make'`), **when** `./configure` records a path into the wrapper directory, **then** that path stays valid for `make`. - **Given** a Windows build with `CC=cl` (no `.exe` extension), **when** the build compiles `test.c`, **then** the wrapper directory holds exactly one wrapper for `cl`, which `cl`, `cl.exe`, and `CL.EXE` all match, and the entry names the real `cl.exe` by absolute path. ## Non-functional constraints - Platform: works on all supported platforms (Linux, macOS, FreeBSD, Windows). - Bear finds the wrapper binary at run time, not at a path fixed when Bear was built (issue #668). ## Known limitations **Only wrapped compilers are intercepted**: the wrapper mechanism sees only the compilers it wrapped at startup, and only when the build finds them through PATH or a compiler program variable. A compiler that is not on PATH, not in a compiler program variable, and not listed in the configuration is not intercepted. **Unrecognized compiler names** (issue #561): a compiler whose name Bear does not recognize is skipped by the PATH scan. It is wrapped only when a compiler program variable names it, or when the configuration lists it. Listing it turns the PATH scan off, so the other compilers must be listed too. **PATH ordering conflicts** (issue #445): the wrapper directory must be first in PATH. A masquerade directory such as ccache's can conflict with it. Resolving past masquerade wrappers is owned by `interception-wrapper-recursion`. **Wrapper directory lifetime** (issue #654): Bear removes the wrapper directory when it exits. If the configure step runs under Bear and a later build step runs without Bear, a path recorded into the directory (for example as the compiler path) no longer exists, and the build fails with "No such file or directory". Running every step that uses the recorded path under Bear avoids this. ## Rationale - [Wrapper mechanism rewrites program variables only](../rationale/env-vars-rewritten-by-wrapper.md) - [Wrapper mechanism: hard links, deterministic directory, resolve-at-startup](../rationale/wrapper-mode-design-decisions.md) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/requirements/interception-wrapper-recursion.md���������������������������0000664�0000000�0000000�00000005707�15271777203�0027011�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: Resolve past masquerade wrappers in the wrapper mechanism status: implemented --- ## Intent Some distributions ship compiler masquerade wrappers: a directory of symlinks named after compilers that point at ccache, icecream, or a similar tool, placed on PATH. When the user runs a build under Bear with the wrapper mechanism on such a host, Bear must not loop with the masquerade wrapper. The database records the real compiler, and the build completes. The user does not strip any directory from PATH to make this work. ## Acceptance criteria - **Given** a host where `/usr/lib64/ccache/gcc -> /usr/bin/ccache` is first in PATH, **when** the user runs a build under Bear with the wrapper mechanism, **then** the build completes without hanging, the database holds one entry per source file, and each recorded compiler is an absolute path to the real compiler, never the masquerade wrapper and never a wrapper in `.bear/`. - **Given** a masquerade directory on PATH, **when** the user runs a build under Bear, **then** the user does not have to strip any directory from PATH, unset any environment variable, or set `CCACHE_*` variables. Bear filters only its own lookup. The build's PATH keeps the masquerade directory, behind the wrapper directory. - **Given** a compiler variable whose value resolves, past every masquerade directory, to no real compiler, **when** Bear starts, **then** it logs a warning naming the compiler and the masquerade directories it excluded, registers no wrapper for that name, and does not fall back to the masquerade wrapper. The build then runs that compiler without interception. - **Given** one compiler that cannot be resolved past the masquerade directories, **when** Bear starts, **then** Bear still runs, and the other compilers are still wrapped. ## Non-functional constraints - A masquerade wrapper is detected by inspecting the filesystem only. No subprocess is spawned to identify it. - Detection follows the symlink of the found path, rather than matching known directory locations. Any directory of compiler-named symlinks to a recognized wrapper binary is detected, wherever it is installed. ## Known limitations **Fixed set of recognized wrapper names**: a masquerade wrapper whose name is not in Bear's fixed set is not detected. The user can drop it from PATH or use the preload mechanism. **Symlink-only detection**: a masquerade wrapper installed as a shell script or a hard copy, rather than a symlink, is not detected. **Masquerade tool not run**: while Bear observes the build, the wrapped compilers bypass the masquerade wrapper, so ccache and similar tools do not run for them. **Silent skip in the PATH scan**: when Bear scans PATH for compilers, it skips masquerade links without a message. Only a compiler named in a compiler variable gets the warning. ## Rationale - [Wrapper recursion: why filter Bear's lookup PATH, not the alternatives](../rationale/wrapper-recursion-ccache-alternatives.md) ���������������������������������������������������������rizsotto-Bear-0c30b27/docs/requirements/output-append.md��������������������������������������������0000664�0000000�0000000�00000005357�15271777203�0023427�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: Appending to an existing compilation database status: implemented --- ## Intent A user who builds in steps, or builds parts of a project at different times, wants one compilation database that collects the entries of every Bear run. When appending, Bear merges the new entries with the existing `compile_commands.json` instead of replacing it. ## Acceptance criteria - **Given** an existing database with an entry for `file1.c`, **when** a run that is appending compiles `file2.c`, **then** the database holds both entries, and the new entry for `file2.c` comes before the existing entry for `file1.c`. - **Given** no existing database, **when** a run that is appending compiles `file1.c`, **then** Bear writes a database with only that entry, and does not fail. - **Given** an existing database with an entry for `file1.c`, **when** a run that is not appending compiles `file2.c`, **then** the database holds only the entry for `file2.c`. Overwriting is the default. - **Given** the same setup, **when** the run asks for overwriting explicitly, **then** the result is the same. A caller can name the behavior it depends on instead of relying on the default. - **Given** any run that writes a database, **when** it asks for both appending and overwriting, **then** Bear reports a usage error and runs no build. - **Given** an existing database and a build that runs no compiler, **when** a run that is appending finishes, **then** the existing entries are kept unchanged. - **Given** an existing database that is not valid JSON, **when** a run that is appending finishes, **then** Bear logs a warning for the break, and one more for the entry the break is inside of, if any. The database holds the new entries plus the existing entries read before the break. Entries after the break are dropped. - **Given** an existing database with some invalid entries (complete JSON objects that are not valid entries), **when** a run that is appending finishes, **then** Bear logs one warning per invalid entry and skips it, and keeps the valid entries and the new ones. - **Given** an existing database that cannot be opened (for example, read permission is denied), **when** a run that is appending starts, **then** Bear reports an error before the build runs and writes no output. - Which entry survives when a file is in both the new and the existing entries is owned by `output-duplicate-detection`. ## Non-functional constraints - An interrupted run does not corrupt the database. That is owned by `output-atomic-write`. ## Rationale - [Latest compilation wins for a rebuilt file](../rationale/duplicate-latest-flags-win.md) - [Naming the overwrite behaviour before it stops being the default](../rationale/append-default-migration.md) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/requirements/output-atomic-write.md��������������������������������������0000664�0000000�0000000�00000007016�15271777203�0024556�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: All-or-nothing output files status: implemented --- ## Intent An output file Bear writes is never left corrupt or half written, even when Bear is interrupted or fails. A tool that reads `compile_commands.json`, or a later semantic mode run that reads the events file, finds either the previous complete file or the new complete file. It never finds a truncated file, or a temporary file left over from a failed run. This covers every file a run produces: the compilation database and the events file. ## Acceptance criteria - **Given** a successful build, with or without an existing output file, **when** Bear writes the output, **then** it writes a temporary file and renames it over the output, so a reader never sees a truncated or partly written file, and no temporary file remains after the run. - **Given** a build that runs and fails, **when** Bear finishes, **then** the output is replaced with what was captured. A failed build is not an error of Bear's own. - **Given** a run that ends in an error of Bear's own (the build command cannot start or be supervised, interception fails, the input cannot be read, or writing fails), with or without an existing output file, **when** Bear finishes, **then** it reports the error, the previous output (if any) is unchanged, and no temporary file is left behind. Regression guard for issue #718. - **Given** an existing database and an events input that semantic mode rejects entirely, **when** Bear fails to analyze it, **then** the existing database is unchanged and no temporary file is left behind. - **Given** an output destination that cannot be written, **when** Bear starts, **then** it reports the error before the build runs. - **Given** an output destination that becomes unwritable during the build (for example, it is replaced with a non-empty directory), **when** Bear renames the temporary file at the end, **then** it reports an IO error that names the output path, and leaves the temporary file in place for debugging. - **Given** an existing output file with a name that looks like a temporary file (for example `out.tmp`), **when** the run ends in an error of Bear's own (for example the build command cannot start), **then** that file is unchanged. The temporary file is hidden, sits next to the output, and has a unique name, so it never collides with the output or with the temporary file of another Bear run. - **Given** a run that publishes its output, with or without an existing output file, **when** Bear publishes it, **then** it has the same permissions as a newly created file, not those of the file it replaces. - **Given** an output path that names an existing file which is not a regular file (a device such as `/dev/null`, a named pipe), **when** Bear writes the output, **then** it writes to that file in place and does not replace it with a regular file. Such an output is not all or nothing. ## Known limitations - Concurrent Bear runs that target the same output file do not corrupt it, but the run that finishes last wins. - The output directory must already exist. Bear does not create missing parent directories. - If Bear is killed by a signal it cannot catch, or cannot stop the interception after a failure, the temporary file may be left behind (empty or partial). - When interception fails partway through a build that did start, the entries captured before the failure are lost. The previous output stays. ## Rationale - [A failed build still publishes its output](../rationale/failed-build-still-publishes.md) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/requirements/output-compilation-entries.md�������������������������������0000664�0000000�0000000�00000012307�15271777203�0026136�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: Compilation entries from an intercepted invocation status: implemented --- ## Intent Bear turns each recognized compiler invocation into zero, one, or many entries. Each entry tells a tool such as clangd or clang-tidy how to compile one source, whatever way the build phrased the invocation. The JSON shape of an entry is owned by `output-json-compilation-database`. Which executables are compilers, and which family each belongs to, is owned by `recognition-compiler-names`. Flags folded in from the environment are owned by `output-env-derived-flags`. Later stages that filter the entries are owned by `output-append`, `output-duplicate-detection`, and `output-source-directory-filter`. ## Acceptance criteria The compiler family definition sets which entry shape applies: | Shape | Example | Entries for N sources | `file` | Sources kept in each entry's arguments | |---|---|---|---|---| | Separable sources (GCC, Clang, Fortran, CUDA, MSVC, and the like) | `cc -c a.c b.c` | N | each source in turn | only that entry's own source | | Single translation unit (Vala) | `valac a.vala b.vala` | 1 | the first source in argument order | all N | | Whole module (Swift) | `swiftc -module-name App a.swift b.swift` | N | each source in turn | all N | Sources are identified by extension, from one built-in list shared by every compiler family. - **Given** a build that runs `cc -c src1.c src2.c src3.c` in one invocation, **when** Bear finishes, **then** the database holds three entries with `file` set to `src1.c`, `src2.c`, and `src3.c`, and each entry's arguments hold `-c` and only its own source. - **Given** a build that runs `cc -c src.c`, **when** Bear finishes, **then** the database holds one entry, with `file` set to `src.c` and the compiler at `arguments[0]`. - **Given** a build that runs `valac src1.vala src2.vala`, **when** Bear finishes, **then** the database holds one entry for that invocation, with `file` set to `src1.vala`, and both sources in its arguments. - **Given** a build that runs `swiftc -module-name App a.swift b.swift`, **when** Bear finishes, **then** the database holds one entry for `a.swift` and one for `b.swift`, and the arguments of both hold both sources. - **Given** a build that runs `cc -o a.out src1.c src2.c src3.c`, **when** Bear finishes, **then** the database holds one entry per source, each a pure compile step, and no entry has `a.out` as `file`. - **Given** a build that runs `cc -o a.out obj1.o obj2.o obj3.o`, where every file argument is an object file, archive, or shared library, **when** Bear finishes, **then** the database holds no entry for that link step. - **Given** a build that runs `cc --version` or `cc -###`, which only request information, **when** Bear finishes, **then** the database holds no entry for either. - **Given** a build that runs `cc -E src.c` (preprocess only) and `cc -M src.c` (dependency generation only), **when** Bear finishes, **then** the database holds no entry for either. The same holds for any invocation where argument parsing finds no recognized source: Bear never makes a best-effort guess. - **Given** a build that runs `cc -MD -c src.c`, **when** Bear finishes, **then** the database holds one entry for `src.c`. `-MD`, `-MMD`, and `-fsyntax-only` do compile the source, so they produce an entry. - **Given** a build that runs `cc -o a.out -lsomething -O2 src.c`, **when** Bear finishes, **then** the entry holds `-O2` and not `-lsomething`. Flags that only affect the link stage are removed, in every entry shape; each compiler family defines that set. Flags that affect preprocessing, compiling, or assembling are kept. - **Given** a build that runs `cc -I first -I second -DFOO -DBAR -c src.c`, **when** Bear finishes, **then** the entry lists `-I first` before `-I second`, and `-DFOO` before `-DBAR`. The kept arguments keep their relative order from the original command, and the compiler stays at `arguments[0]`. - **Given** the `output` field is turned on in the configuration and a build that runs `cc -o a.out src1.c src2.c`, **when** Bear finishes, **then** every entry's `output` is `a.out`. The `output` field is off by default. Its path spelling is owned by `output-path-format`. - **Given** the `output` field is turned on and a build that runs `cc -c src.c` with no output flag, **when** Bear finishes, **then** the entry has no `output` key. ## Non-functional constraints - The same rules apply to every compiler family Bear recognizes. The flags and extensions named here are examples, not the full sets. - Extensions match exactly, case included, on every platform. `foo.C` is a source because `.C` is in the list, not because `.c` is. - Response-file (`@file`) arguments are owned by `output-response-file-inlining`. ## Known limitations - A multi-source invocation with one output flag gives every entry the same `output` value. Per-source object names (`src1.o`, `src2.o`) are not inferred. ## Rationale - [Entry shapes for clang tooling](../rationale/compilation-entry-shapes.md) - [Recording Vala (valac) builds in the compilation database](../rationale/vala-transpiler-database.md) - [Swift whole-module entries: one per source, full arguments](../rationale/swift-whole-module-entries.md) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/requirements/output-duplicate-detection.md�������������������������������0000664�0000000�0000000�00000006202�15271777203�0026074�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: Duplicate entry detection and filtering status: implemented --- ## Intent A build may run the compiler for the same source file more than once: parallel make retries, ccache wrappers, or repeated builds that append to an existing database after a file's flags change. The [compilation database specification](https://clang.llvm.org/docs/JSONCompilationDatabase.html) allows several entries for one file, but stale duplicates confuse tools and keep old flags after a rebuild. By default Bear keeps one entry per source file, identified by its `directory` and `file` fields. A user who needs several configurations recorded for one file can widen the set of fields that tell entries apart. ## Acceptance criteria - **Given** a build that compiles `file.c` twice with the same flags, **when** Bear writes the database, **then** it holds one entry for `file.c`. - **Given** a build that compiles `a.c`, `b.c`, and `c.c` in that order, **when** Bear writes the database, **then** the entries appear in that order. Kept entries stay in the order they were received. - **Given** a build that compiles `file.c` with `-O2` and then with `-O3`, **when** Bear writes the database with the default match, **then** it holds one entry for `file.c`, the first one. The default match is the `directory` and `file` fields. - **Given** a match on `directory`, `file`, and `arguments`, and the same build, **when** Bear writes the database, **then** it holds both entries. - **Given** `src/util.c` and `lib/util.c` (same name, different directories), **when** Bear writes the database, **then** it holds both entries. Regression guard for issue #667. - **Given** a match on `file` alone and a build that compiles `file.c` twice with different flags, **when** Bear writes the database, **then** only the first entry is kept. - **Given** a match on `file` and `output`, and `file.c` compiled to both `debug/file.o` and `release/file.o`, **when** Bear writes the database, **then** it holds both entries. - **Given** appending, an existing database with an entry for `file.c`, and a new build that compiles `file.c` with different flags, **when** Bear writes the database, **then** it holds one entry for `file.c`, with the new flags. The new entry wins because new entries come first (owned by `output-append`) and the first one is kept. - **Given** a compiler driver or wrapper and the child compiler it spawns, both intercepted by the preload mechanism as two events for one source, **when** Bear writes the database with the default match, **then** exactly one entry survives, and it records the driver or wrapper invocation, which is emitted first. Regression guard for issue #638. - **Given** a match configuration that is empty, lists a field twice, or lists both `command` and `arguments` (two forms of the same data), **when** Bear loads the configuration, **then** it rejects it with an error that names the problem. ## Non-functional constraints - A duplicate is detected no matter how far apart the two occurrences are in the input. ## Rationale - [Latest compilation wins for a rebuilt file](../rationale/duplicate-latest-flags-win.md) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/requirements/output-env-derived-flags.md���������������������������������0000664�0000000�0000000�00000004507�15271777203�0025456�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: Include compiler environment flags in compilation entries status: implemented --- ## Intent Compilers read part of their configuration from the environment, not from argv. GCC and Clang take extra header search paths from `CPATH` and its siblings. MSVC reads `CL` and `_CL_` as extra leading and trailing options. A build that relies on these compiles fine, but a tool that reads the database sees only argv and misses what the compiler really used. Bear turns these variables into the explicit flags they stand for and adds them to each entry's `arguments`, so the entry stands on its own. This is on by default. A configuration option turns it off for users who want argv alone. Flags carried inside a compiler variable such as `CC="gcc -std=c11"`, seen by the wrapper mechanism, are a separate contract, owned by `interception-cc-env-var-flags`. ## Acceptance criteria - **Given** a build that runs `cc -c src.c -o src.o` with `CPATH` set to `/opt/first:/opt/second`, **when** Bear runs with the default configuration, **then** the entry's `arguments` hold an include flag for `/opt/first` followed by one for `/opt/second`, in the order `CPATH` lists them. - **Given** the same build and environment, **when** Bear runs with environment-derived flags turned off, **then** the entry's `arguments` hold exactly the command-line arguments, after any response-file inlining (owned by `output-response-file-inlining`), and neither path appears. - **Given** an MSVC or clang-cl compilation with `CL` and `_CL_` set, **when** Bear runs with the default configuration, **then** the compiler stays first in `arguments`, the words of `CL` go after the compiler and before the command-line flags, and the words of `_CL_` go after the command-line flags. - A variable's value is split into words with the same rules as the family's response files (owned by `output-response-file-inlining`). - The compiler YAML of each family owns which variables are recognized, the flag each one becomes, and whether its words go before or after the command-line flags. This file owns the toggle, its default, and the rule that a variable's values keep their order. ## Non-functional constraints - The toggle changes only the database. The interception layer and the recorded event environment (`interception-events-format`) are not affected. �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/requirements/output-generated-file-filter.md�����������������������������0000664�0000000�0000000�00000003631�15271777203�0026307�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: Generated-file exclusion by filename pattern status: implemented --- ## Intent Machine-generated sources, such as Qt `moc` output or protobuf stubs, often should not appear in the database, so that linters and editors act only on hand-written code. A user can list filename patterns that exclude, or re-include, matching sources. This is off by default. ## Acceptance criteria - **Given** events that compile `main.cpp` and `moc_window.cpp`, and a rule that excludes the pattern `moc_*.cpp`, **when** Bear runs, **then** the database holds `main.cpp` only. - **Given** the same events and no pattern rules, **when** Bear runs, **then** the database holds both files. - **Given** events that compile `src/main.cpp` and `generated/main.cpp`, and a rule that excludes `generated/*.cpp`, **when** Bear runs, **then** the database holds `src/main.cpp` only. A pattern with a path separator matches the full source path as written in the entry. A pattern without one matches the basename. - **Given** events that compile `main.cpp` and `util.cpp`, and a rule that excludes `*.cpp` followed by one that includes `main.cpp`, **when** Bear runs, **then** the database holds `main.cpp` only. The last matching rule wins, as for directory rules (`output-source-directory-filter`). A file that no pattern matches is kept. - **Given** events that compile `src/main.cpp` and `src/moc_window.cpp`, a directory rule that includes `src`, and a pattern rule that excludes `moc_*.cpp`, **when** Bear runs, **then** the database holds `src/main.cpp` only. An entry is written only when both the directory rules and the pattern rules accept it. - **Given** a configuration with a malformed pattern, **when** the configuration is validated, **then** validation fails with an error that names the rule. ## Rationale - [Generated-file filtering uses user-supplied globs](../rationale/generated-file-filter-globs.md) �������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/requirements/output-header-entries.md������������������������������������0000664�0000000�0000000�00000005047�15271777203�0025053�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: Header file entries in the output status: implemented --- ## Intent Editors and linters often look up compile flags for a header file, not only for translation units, so that features like "jump to definition" work when a header is open. The user can opt in to a synthesized entry for each header, with flags taken from a compiled translation unit. It is off by default. ## Acceptance criteria - **Given** a build that compiles `src/main.c`, next to an uncompiled `src/util.h`, **when** Bear finishes with header entries off (the default), **then** the database holds an entry for `src/main.c` only. - **Given** the same build, **when** Bear finishes with header entries on and the default strategy, which considers headers in the same directory as a compiled source, **then** the database also holds an entry for `src/util.h`. It copies the arguments of `src/main.c` with the source path replaced by the header path and the output flag removed, and it has no `output` field. For example `["cc", "-c", "src/main.c", "-o", "src/main.o"]` gives `["cc", "-c", "src/util.h"]`. - **Given** a build whose compilation emitted a dependency file that lists headers inside the compilation's working directory and system headers outside it, **when** Bear finishes with the dependency-file strategy, **then** the database holds a synthesized entry for exactly the listed headers inside the working directory, and no others. - **Given** a build that runs `swiftc -c thing.swift`, next to an uncompiled `util.h`, **when** Bear finishes with header entries on, **then** the database holds no entry for `util.h`. Only C, C++, and Objective-C translation units give their arguments to a header entry. - **Given** a build that compiles `src/util.h` directly (`cc -c src/util.h`) and `src/main.c` next to it, **when** Bear finishes with header entries on and the default duplicate detection, **then** the database holds exactly one entry for `src/util.h`: the real one from the build. - A synthesized entry never displaces a real one. It is dropped when the key of duplicate detection (owned by `output-duplicate-detection`) matches a real entry; with a key or path format under which the two differ, both appear. Synthesized entries are validated like any other entry (owned by `output-json-compilation-database`). - The set of file extensions that count as headers is built in. No configuration changes it. ## Rationale - [Header entries: clone a sibling by default, read dependency files on request](../rationale/header-synthesis-sibling-clone.md) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/requirements/output-json-compilation-database.md�������������������������0000664�0000000�0000000�00000011736�15271777203�0027205�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: JSON compilation database output status: implemented --- ## Intent When the user runs Bear on a build, Bear writes a JSON compilation database (`compile_commands.json` by default) that lists the compiler calls of the build. Each entry holds the working directory, the source file, and the compile command. Which calls produce entries, and in what shape, is owned by `output-compilation-entries`. One malformed entry never costs the user the rest of the database. Related contracts have their own owners: how many entries one file may keep is owned by `output-duplicate-detection`, and how the `file`, `directory`, and `output` paths are spelled is owned by `output-path-format`. ## Acceptance criteria - **Given** a build that compiles one C source file, **when** Bear finishes, **then** the output is valid JSON with exactly one entry, whose `directory` is the working directory, whose `file` is the source, and whose `arguments` start with the compiler. - **Given** a build script that compiles several C and C++ sources, **when** Bear finishes, **then** the output holds one entry per source, each with the right compiler (C or C++) in `arguments[0]`. - **Given** a build that runs no compiler, **when** Bear finishes, **then** the output is an empty JSON array `[]`. - **Given** a build where some sources compile and some fail, **when** Bear finishes, **then** the output holds an entry for every attempted compilation. The exit code is owned by `cli-exit-codes`. - **Given** the user sets the output path to `build/commands.json`, **when** Bear finishes, **then** that file holds the entries, and no `compile_commands.json` is written in the working directory. - **Given** the default configuration, **when** Bear writes an entry, **then** the entry carries the `arguments` array and no `command` field. - **Given** the command-string format is selected and a compiler call with the argument `-DNAME="hello"`, **when** Bear writes the entry, **then** the entry carries `command` and no `arguments`. The value is the argument list joined with POSIX shell quoting (single- or double-quoted, both are valid) and then JSON-encoded, so in the file that argument reads `'-DNAME=\"hello\"'` or `\"-DNAME=\\\"hello\\\"\"`. JSON-decoding and then shell-word splitting gives back the original argument list. - **Given** an execution whose compiler is spelled `gcc` (a bare name) or `/usr/bin/gcc` (a path), **when** Bear writes the entry, **then** `arguments[0]` is that spelling, unchanged. The output stage never resolves, absolutizes, or rewrites the compiler path. What spelling each source observes is owned elsewhere. For the preload mechanism, `interception-events-format` says the recorded path is absolute or a bare name as the caller spelled it. The others are owned by `interception-wrapper-mechanism` and `interception-shell-text-parsing`. - **Given** an entry that fails validation, **when** Bear writes the output, **then** the entry is dropped, a `WARN` log line names its `file`, its `directory`, and the reason, the run summary counts it, and the entries after it are still written. An entry fails validation when: - its `file` or `directory` is empty; - it has both `command` and `arguments`, or neither; - its `command` cannot be split as POSIX shell words (for example the unterminated quote in `cc -c "src.c`). - **Given** a build whose entries are all valid but one with an empty `directory`, **when** Bear writes the output, **then** the valid entries appear unchanged. The exit code is owned by `cli-exit-codes`. - **Given** a build where every candidate entry fails validation, **when** Bear writes the output, **then** the database is written as an empty JSON array `[]`, each drop has its `WARN` line, and a single `ERROR` line reports that every entry was dropped. - **Given** combined mode with standard output as the database destination, **when** the user runs it, **then** Bear exits with an error before the build runs, the error says the database cannot go to standard output, and no file named `-` is created. - **Given** semantic mode or parse-sh mode with standard output as the database destination, **when** the user runs it, **then** the JSON database appears on standard output, diagnostics stay on standard error, and no file is created, neither one named `-` nor the default destination. The database equals the one the same input gives when written to a file. - The streamed write is not atomic. The all-or-nothing rule of `output-atomic-write` covers files only. - **Given** standard output as the database destination and appending requested, **when** the user runs semantic mode or parse-sh mode, **then** Bear exits with an error before any input is read, and the error says it cannot append to standard output. ## Non-functional constraints - The output conforms to the [Clang JSON Compilation Database specification](https://clang.llvm.org/docs/JSONCompilationDatabase.html). - Works on Linux, macOS, and Windows. ����������������������������������rizsotto-Bear-0c30b27/docs/requirements/output-path-format.md���������������������������������������0000664�0000000�0000000�00000010047�15271777203�0024372�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: Path format for file and directory fields status: implemented --- ## Intent The [JSON compilation database specification](https://clang.llvm.org/docs/JSONCompilationDatabase.html) allows paths that are absolute or relative to the entry's directory. Tools that read the database want different forms. Some need absolute paths, some work better with relative ones, and some need symlinks resolved. Bear lets the user choose the path format of the `directory` field and of the `file` field. ## Acceptance criteria Four formats are supported for each of the two fields: | Format | Result | |---|---| | as is (the default) | the path exactly as interception observed it | | absolute | the path made absolute; it need not exist on disk | | relative | the path relative to the entry's directory | | symlink-resolving | symlinks followed and `.` and `..` removed; the path must exist when the output is written | - **Given** a build run from `/home/user/project` that compiles `src/main.c`, **when** both fields use the default format, **then** `directory` and `file` are written as interception observed them, with `file` as `src/main.c`. - **Given** an event that records the working directory as the relative path `build`, while Bear runs from `/home/user/project`, **when** `directory` uses the absolute format, **then** `directory` is `/home/user/project/build`. The `directory` field is the working directory of the compiler call, formatted with itself as the base. - **Given** a source given as `/home/user/project/src/main.c` and a directory of `/home/user/project`, **when** `file` uses the relative format, **then** `file` is `src/main.c`. The `file` field is resolved against the already formatted `directory`. - **Given** `/a/b/one.c` compiled from `/a/b` and `/a/c/two.c` compiled from `/a/c`, **when** `directory` is absolute and `file` is relative, **then** the entries are `/a/b` with `one.c` and `/a/c` with `two.c`. - **Given** `directory` uses the relative format, **when** Bear writes the entry, **then** `directory` is `.`. - **Given** a source reached through a symlink, **when** `file` uses the symlink-resolving format, **then** `file` is the real path. - **Given** a build on Windows, **when** a field uses the symlink-resolving format, **then** the path has no `\\?\` extended-length prefix (issue #683). - **Given** a source that no longer exists when the output is written, **when** `file` uses the symlink-resolving format, **then** Bear logs a warning and writes the path as interception observed it. - **Given** a working directory that no longer exists when the output is written, **when** `directory` uses the symlink-resolving format, **then** the entry is dropped. The drop follows the same contract as a validation drop: it is named in a warning, counted in the run summary, and part of the error when every entry is dropped (owned by `output-json-compilation-database`). - **Given** a build run from `/home/user/project` that compiles `src/main.c` to `obj/main.o`, with the `output` field on, **when** `file` uses the relative format, **then** `output` is `obj/main.o`. The `output` field always uses the format of `file`. - **Given** the same build where `obj/main.o` was deleted before the output is written, **when** `file` uses the symlink-resolving format, **then** `file` is the real path, and `output` falls back to `obj/main.o` with a warning. - **Given** a build run from `/home/user/project` that compiles `src/main.c` to `obj/main.o` with `-I../include`, **when** `file` uses the absolute format, **then** `arguments` carry the source as `/home/user/project/src/main.c` and the output as `/home/user/project/obj/main.o`, and `-I../include` is unchanged. The source and output paths in `arguments` always use the format of `file`. ## Non-functional constraints - The relative format handles paths that leave the directory, such as `../../other/dir/file.c`. ## Known limitations - Paths inside other flags, such as include search paths, are not transformed. They are written as the build spelled them. �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/requirements/output-response-file-inlining.md����������������������������0000664�0000000�0000000�00000010252�15271777203�0026526�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: Inline response files in compilation entries status: implemented --- ## Intent Some build systems, Xcode among them, put part of a compiler's flags in a response file and pass it as `@file`. Tools that read the database usually do not follow this. They see `@/path/to/args.resp` as a literal argument and miss every include path, define, and warning flag inside. The file often lives in the build directory and may be gone by the time a tool reads the entry. When the user turns inlining on, Bear replaces each `@file` with the file's contents, so the entry stands on its own. It is off by default: Bear records argv exactly as the build wrote it. ## Acceptance criteria - **Given** a build that runs `cc -DBASE=1 @flags.resp -c src.c -o src.o`, where `flags.resp` holds `-I/opt/include -DEXTRA=2`, **when** Bear runs with the default configuration, **then** the entry's `arguments` hold `@flags.resp` literally and none of the file's contents. - **Given** the same build, **when** Bear runs with inlining on, **then** the entry's `arguments` hold `-DBASE=1`, `-I/opt/include`, `-DEXTRA=2`, `-c`, `src.c`, `-o`, `src.o` in that order, and no element starts with `@`. The file's tokens take the place of the `@file` token, in file order, and the compiler stays at index 0. - **Given** a GCC/Clang-family compilation whose response file holds `'-std=gnu++20' -fmodules`, **when** Bear runs with inlining on, **then** the entry's `arguments` hold `-std=gnu++20` and `-fmodules` as two tokens, quotes removed. For this family, whitespace separates tokens, single or double quotes group them, and a backslash quotes the next character. - **Given** an MSVC or clang-cl compilation `cl.exe @args.rsp src.cpp`, where `args.rsp` holds `/I "C:\Program Files\inc" /DFOO=1`, **when** Bear runs with inlining on, **then** the entry's `arguments` hold `/I`, `C:\Program Files\inc`, and `/DFOO=1` as separate tokens. For this family, Windows command-line rules apply: only double quotes group, and a backslash matters only next to a quote. - **Given** `@outer.resp` that references `@inner.resp`, **when** Bear runs with inlining on, **then** the tokens of `inner.resp` appear in place, and no element of `arguments` starts with `@`. Nested references are followed whether or not the compiler documents them. - **Given** a response file that references itself, directly or through a cycle, **when** Bear runs with inlining on, **then** expansion stops at a fixed, built-in depth limit, the `@file` token at that depth stays literal, and a warning is logged. - **Given** an invocation `cc @rel.resp -c src.c` run in `/proj`, where `/proj/rel.resp` holds `-DFROM_RESP=1`, **when** Bear runs with inlining on from another directory, **then** `rel.resp` is read from `/proj` and the entry holds `-DFROM_RESP=1`. Nested references resolve against the same invocation directory. An absolute reference such as `@/opt/flags.resp` is read from that path as is. - **Given** a build that runs `cc -c src.c @flags.resp`, where `flags.resp` holds `extra.c`, **when** Bear runs with inlining on, **then** the database holds an entry for `extra.c` as well as one for `src.c`. Inlined tokens take part in flag classification, link-flag removal, and per-source entries (owned by `output-compilation-entries`) like any other argument. - **Given** a response file that was removed after the build, **when** Bear runs with inlining on, **then** the entry keeps `@flags.resp` literally, a warning names the missing file, and the other entries are written as usual. An unreadable file is handled the same way. ## Non-functional constraints - Inlining changes only the database. The recorded event (`interception-events-format`) keeps argv as the build wrote it, `@file` tokens included. - Bear reads response files on the host where it runs. The path the build recorded must be reachable from there. ## Known limitations - Response-file flags with a syntax other than `@file`, such as nvcc's `--options-file` and `-optf` or IBM XL's `-qoptfile`, are not inlined. They pass through literally. ## Rationale - [Response-file inlining is best effort](../rationale/response-file-inlining-best-effort.md) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/requirements/output-source-directory-filter.md���������������������������0000664�0000000�0000000�00000006064�15271777203�0026741�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: Source directory filtering status: implemented --- ## Intent Users often want to keep some sources out of the database. Files under `/usr/include`, generated files in `build/`, or test-only sources in `src/test/` clutter it and may confuse tools like clangd or clang-tidy. Directory rules let the user include or exclude entries by the path of their source file. ## Acceptance criteria - **Given** no directory rules, **when** Bear writes the database, **then** every entry is kept, whatever its path. - **Given** a rule that excludes `/usr/include`, **when** a build compiles `src/main.c` and a file under `/usr/include`, **then** only `src/main.c` is written. - **Given** the rules include `src`, exclude `src/test`, include `src/test/integration`, in that order, **when** a build compiles files in all three, **then** `src/main.c` is kept, `src/test/unit.c` is dropped, and `src/test/integration/api.c` is kept. Rules are evaluated in order, and the last matching rule wins. - **Given** rules that cover only `src`, **when** a build compiles `lib/external.c`, **then** it is kept. A file no rule matches is kept. - **Given** a rule for `src`, **when** a build compiles `src/main.c` and `not_src/main.c`, **then** only `src/main.c` matches. A rule matches whole path components, never part of a name. It matches files in that directory and in every subdirectory. - **Given** a rule that excludes the file `src/main.c`, **when** a build compiles `src/main.c` and `src/main.cpp`, **then** `src/main.c` is dropped and `src/main.cpp` is kept. - **Given** a rule that excludes the absolute path `/usr/include`, **when** a build compiles `src/main.c`, recorded with that relative path, **then** `src/main.c` is kept. Paths are compared as written in the entry, with no normalization, so rule paths must match the path format in use (`output-path-format`). - **Given** a rule that excludes `src`, **when** a build compiles `src/main.c`, `Src/main.c`, and `SRC/main.c`, **then** only `src/main.c` is dropped. Matching is case-sensitive on every platform, Windows and macOS included. - **Given** an entry that stands for a whole invocation as one translation unit (`output-compilation-entries`), **when** the rules are applied, **then** the entry is kept or dropped as a unit, by the one source in its `file` field. - **Given** a rule with an empty path, **when** the configuration is validated, **then** validation fails with an error. ## Non-functional constraints - Matching does not touch the file system: no stat calls, no symlink resolution. ## Known limitations - Symlinks are not resolved. A rule for `/real/path` does not match a file reached through `/symlink/path`. The symlink-resolving path format (`output-path-format`) resolves paths before matching. - For a one-unit invocation whose sources span an included and an excluded directory, only the source in `file` counts. Real builds do not produce such input, so the gap has no test. ## Rationale - [Source filter: last matching rule wins](../rationale/source-filter-last-match-wins.md) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/requirements/recognition-ambiguous-name-probe.md�������������������������0000664�0000000�0000000�00000011433�15271777203�0027146�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: Disambiguate ambiguous compiler names by version probe status: implemented --- ## Intent Some compiler names do not tell which toolchain is behind them. `cc` and `c++` are GCC on most Linux hosts and Clang on the BSDs and macOS. On HPE Cray systems, the PrgEnv wrapper `CC` drives whichever compiler the loaded programming environment selects. When a build invokes such a name, Bear must parse the call with the right compiler's flag semantics. A wrong guess corrupts the entry: Clang's `-Xclang <arg>` takes the next argument, GCC's flag table does not, so the source and output are misread. The user expects `bear -- cc -c hello.c` to give a correct entry on every host, with no per-platform configuration, and with a way to override Bear's guess. ## Acceptance criteria - **Given** a host where `/usr/bin/cc` is Clang (or GCC), **when** Bear recognizes `cc -c hello.c`, **then** it runs `cc --version` once and parses the call with the family the version output names. On the Clang host, `-Xclang -ast-dump` is parsed with the right arity. `c++` behaves the same way. - **Given** an HPE Cray host where `CC --version` prints the loaded programming environment's banner, **when** Bear recognizes `CC -c hello.cpp`, **then** it parses the call with the family that banner names: Clang under PrgEnv-cray (the CCE Clang banner), GCC under PrgEnv-gnu (the FSF GCC banner). - **Given** a name outside the ambiguous set (`gcc -c hello.c`), **when** Bear recognizes it, **then** no probe runs. Static name recognition, owned by `recognition-compiler-names`, classifies it. - **Given** a cross-prefixed `cc` or `c++` (`aarch64-linux-gnu-cc`), **when** Bear recognizes it, **then** it is neither probed nor recognized, and yields no entry. The user sets its family in the configuration. - **Given** one `cc` path invoked 1000 times in one run, **when** Bear recognizes them, **then** the executable is run at most once for probing. Paths that resolve to the same file share one probe. - **Given** an executable that hangs on `--version`, **when** Bear probes it, **then** the probe returns within a fixed, built-in timeout (2 seconds), and the execution yields no entry. - **Given** an executable that reads stdin on `--version` (a stray `bash` named `cc` on PATH), **when** Bear probes it, **then** the probe does not block, because stdin is closed. - **Given** a `cc` whose `--version` output has no known signature (a custom wrapper with a vendor banner), **when** Bear recognizes it, **then** the probe declines, the execution is not classified, and it yields no entry. Bear never falls back to a guess by name. The same holds when the probe exits non-zero, cannot be started, or is not available on the platform. - **Given** Bear running with its preload library in the environment, **when** it probes a compiler, **then** the probe runs without the preload library and is not recorded as a build event. - **Given** a configuration that maps `/usr/bin/cc` to GCC, **when** Bear recognizes `/usr/bin/cc`, or a bare `cc` that the entry matches, **then** the result is GCC, no probe runs, and the recorded compiler stays as observed. A configured family is the only way to switch the probe off, per path. How an entry matches an execution is owned by `recognition-compiler-names`. - **Given** two configured entries whose paths share the filename `cc` but map it to different families, **when** Bear recognizes a bare `cc`, **then** the first entry classifies it and a warning names the conflict. Each full path keeps its own entry's family. - **Given** a bare ambiguous name, **when** Bear probes it, **then** it runs the name as spelled, found through Bear's own PATH. The recorded compiler is not rewritten (owned by `output-json-compilation-database`). - **Given** `/usr/lib/ccache/cc`, a masquerade link that resolves to the ccache binary, **when** Bear recognizes it, **then** it probes the path as invoked, the masquerade passes `--version` through to the real compiler, the call is parsed with the family that output names, and the recorded compiler stays `/usr/lib/ccache/cc`. Each masquerade name is probed on its own, not shared with others that resolve to the same launcher. - **Given** the same masquerade link whose passthrough output has no known signature, **when** Bear recognizes it, **then** the probe declines and the execution yields no entry. - A compiler launcher invoked by its own name is never probed. That contract is owned by `recognition-compiler-launchers`. ## Known limitations The probe knows only the Clang and GCC version banners. A compiler with another banner, for example `nvc++` under PrgEnv-nvidia, stays unrecognized. ## Rationale - [Disambiguating cc/c++: a cached version probe as sole classifier](../rationale/ambiguous-cc-version-probe.md) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/requirements/recognition-compiler-launchers.md���������������������������0000664�0000000�0000000�00000006152�15271777203�0026726�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: Recognize compiler-launcher invocations status: implemented --- ## Intent Some builds compile through a compiler launcher, such as ccache or distcc, that carries the real compiler in its own arguments. Bear records such a compilation as the real compiler's invocation, parsed with that compiler's flag semantics, not the launcher's. ## Acceptance criteria - **Given** `ccache gcc -c main.c`, **when** Bear recognizes it, **then** the recorded compiler is `gcc`, and the recorded arguments are `gcc -c main.c`. The launcher token is dropped; the real compiler's arguments survive. - **Given** `sccache clang -c main.c` or `icecc gcc -c main.c`, **when** Bear recognizes it, **then** the recorded compiler is the real compiler (`clang`, `gcc`), the same as for ccache. - **Given** `distcc -j 4 gcc -c main.c`, **when** Bear recognizes it, **then** the recorded compiler is `gcc`, and the `-j 4` tokens do not appear in the recorded arguments. The distcc options that Bear's launcher definition knows are skipped this way before the compiler, and are never taken as the compiler name. - **Given** `ccache make all`, where the argument is not a recognized compiler, **when** Bear recognizes it, **then** no entry is produced. - **Given** a bare `ccache` with no arguments, **when** Bear recognizes it, **then** no entry is produced. - **Given** `ccache distcc gcc -c main.c`, a launcher wrapping another launcher, **when** Bear recognizes it, **then** no entry is produced. Bear does not chase a chain of launchers. - **Given** a launcher invoked by its own basename, **when** Bear recognizes it, **then** no version probe is spawned for that name. A launcher's own basename is not ambiguous. An ambiguous compiler name that resolves to a launcher binary (a masquerade link) is probed as invoked; that is owned by `recognition-ambiguous-name-probe`. - **Given** a ccache masquerade symlink that shadows `gcc` on PATH (`/usr/lib/ccache/gcc` resolving to the ccache binary), invoked as `gcc -c main.c`, **when** Bear recognizes it, **then** the recorded compiler is `gcc` as observed, and the recorded arguments are the invocation's own (`gcc -c main.c`). Collapsing this entry with the real compiler it runs under the preload mechanism is owned by `output-duplicate-detection`. - **Given** `icerun gcc -c main.c` (icecream's `icerun`), **when** Bear recognizes it, **then** that execution yields no entry. `icerun` is not a launcher. - This file owns launchers that appear in the build's own commands. Loop prevention for Bear's own wrappers under the wrapper mechanism is owned by `interception-wrapper-recursion`. ## Known limitations **Fixed set of launcher names**: Bear recognizes a launcher by its basename against a fixed set. A locally built or uncommon launcher with another name is not recognized. Configuring a launcher at another basename does not make it recognized either. ## Rationale - [Some compiler-adjacent names are deliberately not recognized](../rationale/names-deliberately-not-recognized.md) - [A compiler family is defined by its YAML file](../rationale/compiler-family-definition.md) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/requirements/recognition-compiler-names.md�������������������������������0000664�0000000�0000000�00000013554�15271777203�0026051�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: Recognize compiler names across toolchains status: implemented --- ## Intent Builds that invoke a recognized compiler record their compilations in the compilation database without any configuration. Recognized compilers include the GNU and LLVM drivers and the vendor toolchains, standalone assemblers, MPI wrappers, and Swift and Fortran compilers that Bear's compiler definitions describe. The recorded compiler is the name the build invoked, exactly as executed. ## Acceptance criteria - The tool lists the recognized compilers in its own output; the man page names the command. Each row gives a human-readable description, and each compiler row gives the family name a configuration uses to select it. The criteria below apply to whatever that list contains. - **Given** an event file with a `<name> -c hello.c` execution, where `<name>` is a classic GNU or LLVM driver in any recognized spelling (`gcc`, `clang++`, `arm-linux-gnueabihf-gcc`, `gcc-12`, `clang-15`), **when** Bear runs in semantic mode, **then** the database holds one entry for `hello.c`, its arguments start with the name as invoked, and it is parsed with the base name's flag semantics. - The classic GNU and LLVM driver names are also recognized with a cross-compilation target prefix (`aarch64-linux-gnu-clang`), a version suffix (`gcc-12`, `gcc12`, `gcc-11.2`), or both (`arm-linux-gnueabi-gcc-12`). Some other families accept one of these shapes; which ones is a property of each compiler, not a blanket rule. - **Given** an event file with a `<name> -c hello.c` execution for any other recognized C-family name (for example a vendor driver, `emcc`, `qcc`, or an MPI wrapper), **when** Bear runs in semantic mode, **then** the database holds one entry for `hello.c` whose arguments start with `<name>`, parsed with that family's flag semantics. - **Given** an event file with a `gfortran -c hello.f90` execution, **when** Bear runs in semantic mode, **then** the database holds one entry for `hello.f90` whose arguments start with `gfortran`, parsed with GCC flag semantics. The assembler, Fortran, and Swift families follow the same shape with their own sources and flags. - Each entry records the compiler and arguments as invoked. A wrapper or driver is never expanded to the underlying compiler command. Compiler launchers are the exception; they are owned by `recognition-compiler-launchers`. - **Given** an event file with an info-only execution, **when** Bear runs in semantic mode, **then** it yields no entry. The general info-only contract (`--version`, `--help`, and the like) is owned by `output-compilation-entries`. The MPI wrappers' own wrapper-info options (for example `mpicc -showme`) are this requirement's addition to it. - A value-carrying wrapper or driver option never swallows a following source file, and its token is kept verbatim in the arguments. - **Given** an event file with an `mpicc -cc=gcc -c hello.c` execution, **when** Bear runs in semantic mode, **then** the database holds one entry for `hello.c`, and `-cc=gcc` is kept in the arguments and does not swallow the source file. - **Given** an event file with a `qcc -Vgcc_ntoaarch64le -c hello.c` execution, **when** Bear runs in semantic mode, **then** the database holds one entry for `hello.c`, and `-Vgcc_ntoaarch64le` is kept in the arguments. - **Given** a compiler configured at `/opt/toolchain/f95` with the Flang family, and an event file with an `f95 -c hello.f90` execution (or the same with the full path, or with a path that resolves to the same file), **when** Bear runs in semantic mode, **then** the database holds one entry for `hello.f90`, parsed with Flang flag semantics, not GCC's. A configured family wins over the family the name would otherwise get. - **Given** an event file with a `swiftc -module-name App -emit-object a.swift b.swift` execution, **when** Bear runs in semantic mode, **then** the database holds two entries, one for `a.swift` and one for `b.swift`, and both entries' arguments contain `a.swift`, `b.swift`, and `-module-name App`. The whole-module shape is owned by `output-compilation-entries`. - **Given** an event file with a `gcc -c foo.s` execution, **when** Bear runs in semantic mode, **then** the database holds one entry for `foo.s`, recorded as a GCC compilation (regression guard for issue #146). - **Given** an event file with a `fasm hello.asm output.bin` execution, **when** Bear runs in semantic mode, **then** the database holds one entry for `hello.asm`, and `output.bin` is a plain argument, not the entry's `output` field. - **Given** an event file with an execution of a name below, **when** Bear runs in semantic mode, **then** it yields no entry: - `as` (the GNU assembler) - `mpirun`, `mpiexec` - `swift`, `zig` - `swift-frontend`, and `swiftc -frontend` - `amdgpu-arch` - `ml`, `ml64` (MASM) - The ambiguous names `cc`, `c++`, and `CC` are owned by `recognition-ambiguous-name-probe`. - When the preload mechanism reports both a driver or wrapper and the child compiler it runs for one source, the pair collapses to one entry that keeps the driver invocation. That is owned by `output-duplicate-detection`. ## Known limitations **Xcode's `swiftc` on macOS**: it is Apple-signed, so System Integrity Protection keeps the preload mechanism out of it. The wrapper mechanism records it. What Bear does when the preload mechanism is selected there is owned by `interception-mechanism-selection`. ## Rationale - [MPI wrappers are recorded as compilers, not expanded](../rationale/mpi-wrappers-as-compilers.md) - [Swift whole-module entries: one per source, full arguments](../rationale/swift-whole-module-entries.md) - [A compiler family is defined by its YAML file](../rationale/compiler-family-definition.md) - [Some compiler-adjacent names are deliberately not recognized](../rationale/names-deliberately-not-recognized.md) ����������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/docs/requirements/recognition-cpp20-modules.md��������������������������������0000664�0000000�0000000�00000004270�15271777203�0025523�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: Recognize C++20 module compilations status: implemented --- ## Intent When a project uses C++20 modules, the build compiles module-interface source files (typically `.cppm`, `.ixx`, `.ccm`) and passes module-related flags (`clang++ --precompile`, `-fmodule-file=`). The user expects `compile_commands.json` to hold these compilations the same way it holds ordinary `.cpp` translation units. Clang tooling (clangd, clang-tidy) needs the module inputs and module flags to follow imports and give accurate diagnostics. ## Acceptance criteria - A source file with a module-interface extension is a translation unit like any other: `.cppm`, `.ixx`, `.mxx`, `.ccm`, `.cxxm`, `.c++m`. - Module-related compiler flags are recognized. With or without a value, they never take a following source file as their own and never suppress or corrupt the entry. The recognized set is the one Bear's compiler definitions carry. - A precompiled module (`.pcm`, a BMI file) on the command line is never the entry's `file`. - **Given** a module-interface compile, **when** the user runs `bear -- clang++ -std=c++20 --precompile foo.cppm -o foo.pcm`, **then** the output holds one entry with `file` `foo.cppm`, and its `arguments` keep `--precompile` and the input and output paths. - **Given** a build that uses a precompiled module, **when** the user runs `bear -- clang++ -std=c++20 -fmodule-file=foo=foo.pcm -c main.cpp`, **then** the output holds one entry with `file` `main.cpp` that keeps `-fmodule-file=foo=foo.pcm`, and no entry for `foo.pcm`. - **Given** a build that runs both commands above, **when** Bear finishes, **then** the output holds exactly two entries, one for `foo.cppm` and one for `main.cpp`. - **Given** a GCC module build, **when** the user runs `bear -- g++ -std=c++20 -fmodules-ts -c mod.cppm`, **then** the output holds one entry with `file` `mod.cppm`. ## Non-functional constraints - A module-interface source is detected by its file extension only. Detecting it by probing the compiler is out of scope. ## Known limitations Bear captures the module compile commands and nothing more. It does not track the module dependency graph, find BMI caches, or write module maps. ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/man/��������������������������������������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0015366�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/man/CLAUDE.md�����������������������������������������������������������������0000664�0000000�0000000�00000001023�15271777203�0016641�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������## Man page `bear.1.md` is the source. `bear.1` is generated from it. ## Generating the man page After editing `bear.1.md`, regenerate from the `man/` directory: ```sh pandoc -s -t man bear.1.md -o bear.1 ``` ## Rules - Keep `bear.1.md` and `bear.1` in sync -- always regenerate after edits - Format follows pandoc man page conventions (% title, % author, % date header) - CLI flags and options must match what `crates/bear/src/args.rs` defines via `clap` - If CLI behavior changes, update `bear.1.md` first, then regenerate �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/man/bear.1��������������������������������������������������������������������0000664�0000000�0000000�00000070253�15271777203�0016370�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������.\" Automatically generated by Pandoc 3.7.0.2 .\" .TH "BEAR" "1" "November 2, 2026" "Bear User Manuals" .SH NAME bear \- generate a compilation database for Clang tooling .SH SYNOPSIS \f[B]bear\f[R] [\f[I]OPTIONS\f[R]] \-\- \f[I]BUILD_COMMAND\f[R]\&... .PP \f[B]bear intercept\f[R] [\f[I]OPTIONS\f[R]] \-\-output \f[I]FILE\f[R] \-\- \f[I]BUILD_COMMAND\f[R]\&... .PP \f[B]bear semantic\f[R] [\f[I]OPTIONS\f[R]] .PP \f[B]bear parse\-sh\f[R] [\f[I]OPTIONS\f[R]] .SH DESCRIPTION Bear produces a JSON compilation database (\f[CR]compile_commands.json\f[R]). Clang\-based tools such as clangd and clang\-tidy read this file to learn how each source file of a project is compiled: with which compiler, which flags, from which directory. Build systems know this, but most do not export it; Bear recovers it from the build itself. .PP Bear works in two stages. First it captures the commands a build executes, and writes them as a stream of execution events. The capture either observes a real build run, or parses the text of a dry run. Then semantic analysis recognizes the compiler invocations among those events, parses their command lines, and writes the compilation database. .SS Modes .TP \f[B]Combined mode\f[R] (default) \f[CR]bear \-\- <build command>\f[R] runs the build, captures and analyzes in one step, and writes the database. This is the recommended way to use Bear: it observes the commands the build actually executes, so the result is exact. Use it whenever you can run the build. .TP \f[B]Intercept mode\f[R] \f[CR]bear intercept\f[R] runs the build but only captures, writing the raw event stream to a file. Useful when the build is expensive and you want to run it once and process the events several times, for example to try different output configurations. .TP \f[B]Semantic mode\f[R] \f[CR]bear semantic\f[R] runs no build: it is a filter that reads an event stream (from \f[CR]bear intercept\f[R], or any producer of the documented event format) and writes the compilation database. Combine it with intercept mode to re\-generate the database without rebuilding. .TP \f[B]Parse\-sh mode\f[R] \f[CR]bear parse\-sh\f[R] produces the compilation database from shell command text, without executing anything. Typical input is a build system\(cqs dry run (\f[CR]make \-n\f[R]) or a saved build log. It is a text parser and inherently lower fidelity than interception; reach for it when the build cannot be run, for example to recover a database from a CI log. See its limitations under COMMANDS. .SS Interception methods Interception itself has two methods, selectable in the configuration file. \f[B]Preload\f[R] (the default on Linux and the BSDs) injects a small library into every process the build starts and observes its \f[CR]exec()\f[R] calls. \f[B]Wrapper\f[R] (the default on macOS and Windows) substitutes the known compilers with a reporting wrapper executable. Preload is transparent to the build but cannot observe statically linked executables, and it is unavailable on Windows and on macOS while System Integrity Protection is enabled; forcing it there is a startup error that names wrapper mode, not a silent fallback. Wrapper mode works on every platform, but the build has to pick the wrapper up: a \(lqconfigure\(rq step that discovers compilers must itself run under Bear (see TROUBLESHOOTING). .SS OPTIONS .TP \f[B]\-c, \-\-config\f[R] \f[I]FILE\f[R] Path of the configuration file. It controls the interception method, compiler recognition, source filtering, duplicate handling, and output formatting (see CONFIGURATION). Given before a subcommand it applies to that subcommand too. .TP \f[B]\-o, \-\-output\f[R] \f[I]FILE\f[R] Path of the compilation database to write (default: \f[CR]compile_commands.json\f[R]). \f[CR]\-\f[R] is not accepted here, because in combined mode standard output belongs to the build. To stream the database, use \f[CR]bear semantic \-\-output \-\f[R] (see COMMANDS). .TP \f[B]\-a, \-\-append\f[R] Append to an existing output file instead of overwriting it. New entries are placed before the existing ones, so a rebuilt file\(cqs newest invocation survives duplicate filtering and replaces the stale entry (see \f[CR]duplicates\f[R] under CONFIGURATION). .TP \f[B]\-\-overwrite\f[R] Replace the output file with only this run\(cqs entries. This is what Bear already does when neither flag is given; the flag exists so a script can name the behaviour it depends on rather than rely on the default. Giving it together with \f[CR]\-\-append\f[R] is an error. A future release makes appending the default, and a script that passes \f[CR]\-\-overwrite\f[R] today keeps its current behaviour across that change. .TP \f[B]\-h, \-\-help\f[R] Print help. .TP \f[B]\-V, \-\-version\f[R] Print version. .SH COMMANDS .SS bear intercept Runs the build and captures execution events to a file, without analyzing them. .PP \f[B]bear intercept\f[R] [\f[I]OPTIONS\f[R]] \-\-output \f[I]FILE\f[R] \-\- \f[I]BUILD_COMMAND\f[R]\&... .TP \f[B]\-o, \-\-output\f[R] \f[I]FILE\f[R] Path of the event file to write. Required, and \f[CR]\-\f[R] is not accepted: the build owns standard output, and there is no default event\-file name. .SS bear semantic Reads an event stream and writes the compilation database. It runs no build, so it behaves as a plain filter: diagnostics go to standard error, and both ends can be standard streams. .PP \f[B]bear semantic\f[R] [\f[I]OPTIONS\f[R]] .TP \f[B]\-i, \-\-input\f[R] \f[I]FILE\f[R] Path of the event file to read (default: \f[CR]\-\f[R], standard input). Any producer of a conforming event stream can pipe into it. When standard input is a terminal, or the stream turns out to be empty, a warning is printed on standard error. .TP \f[B]\-o, \-\-output\f[R] \f[I]FILE\f[R] Path of the compilation database to write (default: \f[CR]compile_commands.json\f[R]). Pass \f[CR]\-\f[R] to write to standard output; that write is neither atomic nor appendable, so \f[CR]\-\-output \-\f[R] together with \f[CR]\-\-append\f[R] is rejected. .TP \f[B]\-a, \-\-append\f[R] Same as in combined mode: place new entries before the existing ones in the output file. .TP \f[B]\-\-overwrite\f[R] Same as in combined mode: name the current default explicitly. Not accepted together with \f[CR]\-\-append\f[R]. .SS bear parse\-sh Produces the compilation database from shell command text, without running anything. Typical input is a build system\(cqs dry\-run output, such as \f[CR]make \-n\f[R], or a saved build log: .IP .EX make \-n | bear parse\-sh .EE .PP \f[B]bear parse\-sh\f[R] [\f[I]OPTIONS\f[R]] .TP \f[B]\-i, \-\-input\f[R] \f[I]FILE\f[R] Path of the shell text to parse (default: \f[CR]\-\f[R], standard input). .TP \f[B]\-o, \-\-output\f[R] \f[I]FILE\f[R] Path of the compilation database to write (default: \f[CR]compile_commands.json\f[R]). Pass \f[CR]\-\f[R] to write to standard output; that write is neither atomic nor appendable, so \f[CR]\-\-output \-\f[R] together with \f[CR]\-\-append\f[R] is rejected. .TP \f[B]\-a, \-\-append\f[R] Same as in combined mode: place new entries before the existing ones in the output file. .TP \f[B]\-\-overwrite\f[R] Same as in combined mode: name the current default explicitly. Not accepted together with \f[CR]\-\-append\f[R]. .TP \f[B]\-C, \-\-directory\f[R] \f[I]DIR\f[R] Initial working directory for the parsed commands. Use it for input captured elsewhere (a CI log, a dry run from another checkout). Give an absolute path; the directory need not exist on this machine. .PP The commands recognized in the text go through the same semantic analysis and output stage as an intercepted build, so the configuration applies exactly as in combined mode. Parsed commands that are not compiler invocations (\f[CR]ar\f[R], \f[CR]mv\f[R], \f[CR]mkdir\f[R]) simply produce no entries. .PP It understands a documented subset of POSIX shell syntax: word splitting and quoting; the \f[CR];\f[R], \f[CR]&&\f[R], \f[CR]||\f[R], \f[CR]&\f[R], and \f[CR]|\f[R] separators; comments; redirections; \f[CR]cd\f[R]; brace groups (\f[CR]{ ...; }\f[R], whose \f[CR]cd\f[R] persists past the closing brace); and make\(cqs \f[CR]Entering directory\f[R] / \f[CR]Leaving directory\f[R] markers, which track the working directory across a recursive build. Sub\-makes print these markers by default; run \f[CR]make \-nw\f[R] to have the top\-level make print them too, so the log itself names the build root. A line using anything outside that subset is skipped and reported on standard error with its line number and reason. This covers subshells, command substitution, parameter expansion, globs in the executable position, here\-documents, unterminated quotes, and shell keywords such as \f[CR]if\f[R] or \f[CR]for\f[R]. The run succeeds as long as at least one line parsed as a command. .PP Interception remains the higher\-fidelity default: it observes the \f[CR]exec()\f[R] calls a build really makes, while \f[CR]bear parse\-sh\f[R] reconstructs approximate events from text the build system chose to print. A dry run can omit commands (recursive make does not always propagate \f[CR]\-n\f[R], and commands behind not\-yet\-generated sources never print); the environment and \f[CR]PATH\f[R] used to resolve bare executable names are the parse\-time ones, not the real build\(cqs; and only POSIX \f[CR]sh\f[R] text is supported. Prefer \f[CR]bear \-\- <build command>\f[R] whenever the build can actually be run. .PP The source files named in the parsed commands need not exist: entries are reconstructed from the text alone. The one exception is the \f[CR]canonical\f[R] path format, which resolves symlinks on disk; when the sources are absent, configure \f[CR]absolute\f[R] instead (see \f[CR]format.paths\f[R] under CONFIGURATION). .SH OUTPUT Bear writes a JSON compilation database conforming to the Clang JSON Compilation Database specification (see SEE ALSO): a JSON array of entry objects with the following fields. .TP \f[B]directory\f[R] The working directory of the compilation. .TP \f[B]file\f[R] The main translation unit source file. .TP \f[B]arguments\f[R] The compilation command as an array of strings. This is the default; Bear prefers it over \f[CR]command\f[R] because it avoids shell\-escaping issues. .TP \f[B]command\f[R] The compilation command as a single shell\-escaped string (written instead of \f[CR]arguments\f[R] when \f[CR]format.entries.use_array_format\f[R] is \f[CR]false\f[R]). .TP \f[B]output\f[R] The file produced by the compilation (optional). .PP By default Bear does not transform paths: each entry records them as they appeared in the intercepted invocation, so \f[CR]file\f[R] and \f[CR]output\f[R] are often relative to \f[CR]directory\f[R]. Use \f[CR]format.paths\f[R] in the configuration to normalize them. .PP Two compilers produce an entry shape worth knowing about: .IP \(bu 2 \f[B]swiftc\f[R]: a whole\-module invocation naming several \f[CR].swift\f[R] sources produces one entry per source, each carrying the complete invocation\(cqs arguments (every source of the module, not just its own). This matches the shape CMake emits and SourceKit\-LSP consumes: in whole\-module compilation each file\(cqs semantics depend on every other source. The repeated argument data is expected, not a bug. The internal \f[CR]swift\-frontend\f[R] jobs that \f[CR]swiftc\f[R] spawns are filtered out automatically. .IP \(bu 2 \f[B]valac\f[R]: one entry per \f[CR]valac\f[R] invocation (valac compiles all of a target\(cqs \f[CR].vala\f[R]/\f[CR].gs\f[R] sources together as one unit); the entry\(cqs \f[CR]file\f[R] is the first source and every source is kept in the command. Since valac transpiles to C and compiles the result, the database also contains entries for the generated C files (see TROUBLESHOOTING for the clangd consequences). .SH CONFIGURATION Bear reads a YAML configuration file, found by search (see FILES) or named with \f[CR]\-\-config\f[R]. Without one, built\-in defaults apply. A file that is written at all must name \f[CR]schema\f[R]; every section below it is optional. The example below shows every section: .IP .EX schema\f[B]:\f[R] \(dq4.2\(dq intercept\f[B]:\f[R] mode\f[B]:\f[R] wrapper\f[I] # default: preload on Linux and the BSDs\f[R] compilers\f[B]:\f[R] \f[B]\-\f[R] path\f[B]:\f[R] /usr/bin/cc as\f[B]:\f[R] gcc \f[B]\-\f[R] path\f[B]:\f[R] /usr/local/bin/gcc ignore\f[B]:\f[R] true sources\f[B]:\f[R] directories\f[B]:\f[R] \f[B]\-\f[R] path\f[B]:\f[R] /project/tests action\f[B]:\f[R] exclude files\f[B]:\f[R] \f[B]\-\f[R] pattern\f[B]:\f[R] \(dqmoc_*.cpp\(dq action\f[B]:\f[R] exclude \f[B]\-\f[R] pattern\f[B]:\f[R] \(dq*.pb.cc\(dq action\f[B]:\f[R] exclude duplicates\f[B]:\f[R] match_on\f[B]:\f[R]\f[I] # default: directory, file\f[R] \f[B]\-\f[R] file \f[B]\-\f[R] arguments format\f[B]:\f[R] paths\f[B]:\f[R] directory\f[B]:\f[R] canonical\f[I] # default: as\-is\f[R] file\f[B]:\f[R] canonical\f[I] # default: as\-is\f[R] entries\f[B]:\f[R] use_array_format\f[B]:\f[R] true include_output_field\f[B]:\f[R] true arguments\f[B]:\f[R] from_response_files\f[B]:\f[R] false from_environment\f[B]:\f[R] true headers\f[B]:\f[R] enabled\f[B]:\f[R] true\f[I] # default: false\f[R] strategy\f[B]:\f[R] siblings .EE .PP Every value above is illustrative; the non\-default ones are annotated inline. Each section below states its actual defaults. .SS intercept Controls the interception method (see DESCRIPTION for the trade\-offs): .IP \(bu 2 \f[B]mode\f[R]: \f[CR]preload\f[R] (default on Linux and the BSDs) or \f[CR]wrapper\f[R] (default on macOS and Windows; works everywhere). .SS compilers Hints for compiler recognition. Bear recognizes GCC\- and Clang\-compatible drivers (including cross\-compiler prefixed names), the common vendor compiler drivers and standalone assemblers, \f[CR]swiftc\f[R] and \f[CR]valac\f[R], compiler launchers, and MPI compiler wrappers. Run \f[CR]bear semantic \-\-print\-compilers\f[R] for the full list of recognized compiler names, each with the \f[CR]as\f[R] value it maps to. .PP A few recognition behaviors are worth knowing. Compiler launchers (\f[CR]ccache\f[R], \f[CR]distcc\f[R], \f[CR]sccache\f[R], \f[CR]icecc\f[R]) are dropped from the recorded command: \f[CR]ccache gcc \-c main.c\f[R] produces an entry for \f[CR]gcc \-c main.c\f[R]. MPI compiler wrappers are recorded as invoked, not expanded to the compiler they wrap; Clang tooling that needs the wrapper\(cqs baked\-in include paths can point at it directly, for example with clangd\(cqs \f[CR]\-\-query\-driver\f[R]. C++20 module\-interface units (\f[CR].cppm\f[R], \f[CR].ixx\f[R], and similar) are recognized as sources, while precompiled \f[CR].pcm\f[R] artifacts never appear as an entry\(cqs \f[CR]file\f[R]. .PP In wrapper mode this section also decides what gets wrapped. With no entry other than \f[CR]ignore\f[R] ones, Bear scans PATH at startup and wraps every compiler it recognizes. As soon as one entry is not ignored, Bear wraps exactly the listed paths and skips the scan, so listing one unusual compiler stops \f[CR]gcc\f[R] and \f[CR]clang\f[R] from being intercepted unless they are listed too. Compilers named in the compiler environment variables (\f[CR]CC\f[R], \f[CR]CXX\f[R], and similar) are wrapped either way. Preload mode is unaffected: it intercepts every executed program regardless of this section. .PP Use this section when a compiler at a given path is not recognized, or is recognized wrongly: .IP \(bu 2 \f[B]path\f[R]: Path of the compiler executable. Required. .IP \(bu 2 \f[B]as\f[R]: Compiler type hint for semantic analysis. Optional; when omitted, Bear guesses the family from the executable\(cqs filename and falls back to \f[CR]gcc\f[R] when nothing matches. The accepted values are the \f[CR]as\f[R] names shown by \f[CR]bear semantic \-\-print\-compilers\f[R], plus \f[CR]wrapper\f[R] for a compiler launcher. A launcher is recognized only by its own basename, such as \f[CR]ccache\f[R] or \f[CR]distcc\f[R], so \f[CR]wrapper\f[R] on a path with another basename does not make it recognized. .IP \(bu 2 \f[B]ignore\f[R]: Exclude this executable\(cqs invocations from the database. Optional, default \f[CR]false\f[R]. .PP The generic names \f[CR]cc\f[R], \f[CR]c++\f[R], and the HPE Cray PrgEnv wrapper \f[CR]CC\f[R] are classified by probing the executable\(cqs \f[CR]\-\-version\f[R] output, since the same basename can be a different compiler depending on the platform or the loaded environment module. When the probe cannot classify the executable, override it: .IP .EX compilers\f[B]:\f[R] \f[B]\-\f[R] path\f[B]:\f[R] /usr/bin/cc as\f[B]:\f[R] clang .EE .SS sources Filters entries by source file location and filename. .IP \(bu 2 \f[B]directories\f[R]: List of directory\-based rules, each with a \f[B]path\f[R] and an \f[B]action\f[R] (\f[CR]include\f[R] or \f[CR]exclude\f[R]). .IP \(bu 2 \f[B]files\f[R]: List of filename\-glob rules, each with a \f[B]pattern\f[R] and an \f[B]action\f[R] (\f[CR]include\f[R] or \f[CR]exclude\f[R]). .PP Rules of each list are evaluated in order, the last matching rule wins, and an entry matched by no rule is included; an empty list includes everything. The two lists compose: an entry is emitted only when both accept it. A file pattern without a path separator matches the source file\(cqs basename; a pattern containing a separator matches the full source path as it appears in the entry, so use the same path format as configured in \f[CR]format.paths.file\f[R]. An invalid pattern is rejected during configuration validation with an error naming the offending rule. Typical use for file patterns is dropping machine\-generated sources (Qt \f[CR]moc\f[R] output, protobuf stubs) so that linters and editors act only on hand\-written code; see EXAMPLES. .SS duplicates Controls duplicate detection. .IP \(bu 2 \f[B]match_on\f[R]: List of entry fields to compare (\f[CR]file\f[R], \f[CR]arguments\f[R], \f[CR]directory\f[R], \f[CR]command\f[R], \f[CR]output\f[R]). Two entries are duplicates when all of the configured fields match; the first occurrence is kept. The default is \f[CR]directory\f[R] and \f[CR]file\f[R], so one entry is kept per source file per directory, regardless of arguments; when a build compiles the same file with different flags, add \f[CR]arguments\f[R] to keep an entry per configuration. Combined with \f[CR]\-\-append\f[R] (new entries first), the default means a rebuilt file\(cqs newest invocation replaces its stale entry. .SS format Controls output formatting. .IP \(bu 2 \f[B]paths.directory\f[R], \f[B]paths.file\f[R]: How to format these fields\(cq paths: \f[CR]as\-is\f[R] (default, no transformation), \f[CR]canonical\f[R] (resolve symlinks; requires the path to exist), \f[CR]relative\f[R] (to the \f[CR]directory\f[R] field), or \f[CR]absolute\f[R]. .IP \(bu 2 \f[B]entries.use_array_format\f[R]: Use the \f[CR]arguments\f[R] array (default) instead of the \f[CR]command\f[R] string. Set to \f[CR]false\f[R] for consumers that read only the \f[CR]command\f[R] field. .IP \(bu 2 \f[B]entries.include_output_field\f[R]: Include the \f[CR]output\f[R] field in entries. .IP \(bu 2 \f[B]arguments.from_response_files\f[R]: Replace \f[CR]\(atfile\f[R] response\-file references with the file\(cqs tokenized contents (resolved relative to the compilation\(cqs working directory, expanded recursively, using the compiler\(cqs quoting convention). Disabled by default, in which case an \f[CR]\(atfile\f[R] argument is recorded verbatim; a missing or unreadable file is left literal with a warning. .IP \(bu 2 \f[B]arguments.from_environment\f[R]: Fold compiler environment variables that act as implicit flags into the arguments. GCC/Clang header\-search paths (\f[CR]CPATH\f[R], \f[CR]C_INCLUDE_PATH\f[R], \f[CR]CPLUS_INCLUDE_PATH\f[R], \f[CR]OBJC_INCLUDE_PATH\f[R]) become include flags, and MSVC\(cqs \f[CR]CL\f[R] / \f[CR]_CL_\f[R] become leading / trailing options. Enabled by default; set to \f[CR]false\f[R] to record only the command\-line flags. (Unrelated to the \f[CR]CC=\(dqgcc \-std=c11\(dq\f[R] convention handled during interception; see TROUBLESHOOTING.) .SS headers Editors and linters often need compile flags for header files, not only for the translation units that were compiled. When enabled, Bear synthesizes an entry for a header by cloning the arguments of a compiled C/C++/Objective\-C translation unit, with the source path replaced by the header path and the output flag removed. Off by default. .IP \(bu 2 \f[B]enabled\f[R]: Turn header\-entry synthesis on or off. Default \f[CR]false\f[R]. .IP \(bu 2 \f[B]strategy\f[R]: How headers are discovered and which translation unit donates the flags: .RS 2 .IP \(bu 2 \f[CR]siblings\f[R] (default): headers receive an entry cloned from a compiled source in the same directory. Zero prerequisites, but the flags are approximate, and headers in directories without compiled sources (the split \f[CR]include/\f[R] + \f[CR]src/\f[R] layout) get nothing. .IP \(bu 2 \f[CR]dependency\-files\f[R]: reads the make\-style \f[CR].d\f[R] dependency files the build already emitted (for example via \f[CR]\-MMD\f[R]) and synthesizes an entry for each header prerequisite that resolves inside the compilation\(cqs working directory. Accurate, and reaches headers in other directories, but requires the build to have left dependency files on disk. .RE .PP The header extension set is fixed (the built\-in C\-family headers). Synthesized entries pass through duplicate detection like any other entry, so a header with a real compilation entry is not duplicated. .SH EXIT STATUS In combined and intercept mode, Bear exits with the build command\(cqs exit code: 0 when the build succeeds, the same non\-zero code when it fails. When the build is killed by a signal, Bear exits non\-zero. When the build handles the signal and exits on its own, Bear exits with the build\(cqs code, 0 included. .PP In semantic mode, Bear exits 0 on success and non\-zero when the analysis fails. .PP In parse\-sh mode, Bear exits 0 when the input could be parsed \- even when the database comes out empty because the text named no compiler invocations \- and also on empty input (with a warning on standard error). It exits non\-zero when every non\-empty input line was skipped, so a run that understood nothing cannot pass for a successful one. .PP If Bear itself encounters an internal error, it exits non\-zero regardless of the build command\(cqs status. .SH ENVIRONMENT .TP \f[B]RUST_LOG\f[R] Selects the diagnostic format and verbosity on standard error. When unset, Bear uses the UNIX style, \f[CR]bear: message\f[R] (and \f[CR]wrapper:\f[R] or \f[CR]preload:\f[R] from its helper processes), showing warnings and errors only. When set to a level (\f[CR]error\f[R], \f[CR]warn\f[R], \f[CR]info\f[R], or \f[CR]debug\f[R], in increasing verbosity), Bear switches to a developer format that tags every line with a timestamp, the level, the emitting process and its pid, and the source location. Debug output is essential for troubleshooting; see TROUBLESHOOTING. .SH FILES The configuration file \f[CR]bear.yml\f[R] is searched in the following locations, in order; the first file found is loaded: .TP \f[B]\f[CB]./bear.yml\f[B]\f[R] The current working directory. .TP \f[B]\f[CB]$XDG_CONFIG_HOME/bear.yml\f[B]\f[R], \f[B]\f[CB]$XDG_CONFIG_HOME/Bear/bear.yml\f[B]\f[R] (Unix) When \f[CR]$XDG_CONFIG_HOME\f[R] is set. .TP \f[B]\f[CB]$HOME/.config/bear.yml\f[B]\f[R], \f[B]\f[CB]$HOME/.config/Bear/bear.yml\f[B]\f[R] (Unix) When \f[CR]$XDG_CONFIG_HOME\f[R] is unset. .TP \f[B]\f[CB]%LOCALAPPDATA%\(rsbear.yml\f[B]\f[R], \f[B]\f[CB]%LOCALAPPDATA%\(rsBear\(rsbear.yml\f[B]\f[R] (Windows) When \f[CR]%LOCALAPPDATA%\f[R] is set. .TP \f[B]\f[CB]%APPDATA%\(rsbear.yml\f[B]\f[R], \f[B]\f[CB]%APPDATA%\(rsBear\(rsbear.yml\f[B]\f[R] (Windows) When \f[CR]%APPDATA%\f[R] is set. .SH EXAMPLES Generate a database for a Make project: .IP .EX bear \-\- make .EE .PP For a CMake project in preload mode, only the build needs Bear: .IP .EX cmake \-B build bear \-\- cmake \-\-build build .EE .PP In wrapper mode the configure step must also run under Bear, so it discovers the wrapper as the compiler; discard that run\(cqs output: .IP .EX bear \-\- cmake \-B build bear \-\- cmake \-\-build build .EE .PP Capture once, analyze many times. This allows trying output configurations without rebuilding: .IP .EX bear intercept \-\-output events.json \-\- make bear semantic \-\-input events.json bear \-\-config strict.yml semantic \-\-input events.json \-\-output strict.json .EE .PP Recover a database from a dry run, without building: .IP .EX make \-n | bear parse\-sh .EE .PP For a recursive Make build, add \f[CR]\-w\f[R]. The top\-level make then prints \f[CR]Entering directory\f[R] markers too, and parse\-sh resolves every command against the right directory: .IP .EX make \-nw | bear parse\-sh .EE .PP Update the database after rebuilding one part of the project: .IP .EX bear \-\-append \-\- make \-C src/module .EE .PP Drop generated sources (Qt moc output, protobuf stubs) with a \f[CR]bear.yml\f[R]: .IP .EX sources\f[B]:\f[R] files\f[B]:\f[R] \f[B]\-\f[R] pattern\f[B]:\f[R] \(dqmoc_*.cpp\(dq action\f[B]:\f[R] exclude \f[B]\-\f[R] pattern\f[B]:\f[R] \(dq*.pb.cc\(dq action\f[B]:\f[R] exclude \f[B]\-\f[R] pattern\f[B]:\f[R] \(dq*.pb.h\(dq action\f[B]:\f[R] exclude .EE .PP Synthesize entries for headers in a split \f[CR]include/\f[R] + \f[CR]src/\f[R] layout, when the build emits \f[CR].d\f[R] dependency files: .IP .EX headers\f[B]:\f[R] enabled\f[B]:\f[R] true strategy\f[B]:\f[R] dependency\-files .EE .SH TROUBLESHOOTING .SS Debug logging Before reporting any issue, run Bear with debug logging enabled and include the output in the report: .IP .EX RUST_LOG=debug bear \-\- <build command> .EE .SS Empty output The most common cause is that the build executed no compiler: an incremental build with everything up to date runs nothing. Bear intercepts executed commands only; it does not read the build files. Run a clean build. .PP In wrapper mode, a \(lqconfigure\(rq step that detects compilers captures the real compiler path before Bear can substitute the wrapper. Run the configure step under Bear too (discarding that output), then run the build under Bear. .SS Compiler variables with flags In wrapper mode Bear accepts the GNU Make convention of a compiler variable carrying a trailing flag or two (\f[CR]CC=\(dqgcc \-std=c11\(dq make\f[R]): it splits the value on whitespace, resolves the first token as the compiler, and rewrites the variable so the build still sees the flags. This is a Unix / GNU Make convention; native Windows build systems (MSBuild, \f[CR]nmake\f[R]) do not consume \f[CR]CC\f[R]/\f[CR]CXX\f[R] from the environment. For anything beyond simple whitespace\-separated tokens (quoting, metacharacters, substitutions), use \f[CR]CFLAGS\f[R] / \f[CR]CXXFLAGS\f[R] / \f[CR]LDFLAGS\f[R] instead; Bear passes those through untouched. .SS Preload errors in cross\-compilation An error like \f[CR]version \(aqGLIBC_2.33\(aq not found (required by .../libexec.so)\f[R] means Bear\(cqs preload library was built against a newer glibc than the one the SDK\(cqs compilers load: the library must be ABI\-compatible with the libc of the intercepted process, not only with the host\(cqs. Use a Bear build linked against a glibc no newer than the SDK\(cqs. The project wiki\(cqs Troubleshooting page lists diagnostic commands. .SS Language servers on Vala projects vala\-language\-server reads the \f[CR]command\f[R] field and ignores the \f[CR]arguments\f[R] array; build the database with \f[CR]format.entries.use_array_format: false\f[R]. In a mixed C/Vala project, clangd also indexes the \f[CR]valac\f[R] entries and emits unknown\-argument noise on them; suppress it with a \f[CR].clangd\f[R] file: .IP .EX If\f[B]:\f[R] PathMatch\f[B]:\f[R] .*\(rs.vala Diagnostics\f[B]:\f[R] Suppress\f[B]:\f[R] \(aq*\(aq .EE .SS Getting help Consult the documentation site for known problems and search existing issues before opening a new report. Follow the bug report template, and always include the \f[CR]RUST_LOG=debug\f[R] output. .SH SEE ALSO \f[B]clangd\f[R](1), \f[B]clang\-tidy\f[R](1), \f[B]make\f[R](1) .PP Documentation site, with task pages for build systems, platforms, and toolchains: \c .UR https://rizsotto.github.io/Bear/ .UE \c .PP The Clang JSON Compilation Database specification: \c .UR https://clang.llvm.org/docs/JSONCompilationDatabase.html .UE \c .PP Project homepage and issue tracker: \c .UR https://github.com/rizsotto/Bear .UE \c .SH COPYRIGHT Copyright (C) 2012\-2026 by László Nagy \c .UR https://github.com/rizsotto/Bear .UE \c .SH AUTHORS László Nagy. �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/man/bear.1.md�����������������������������������������������������������������0000664�0000000�0000000�00000061155�15271777203�0016770�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������% BEAR(1) Bear User Manuals % László Nagy % November 2, 2026 <!-- to generate the final `bear.1` file, run `pandoc -s -t man bear.1.md -o bear.1` --> # NAME bear - generate a compilation database for Clang tooling # SYNOPSIS **bear** [*OPTIONS*] \-\- *BUILD_COMMAND*... **bear intercept** [*OPTIONS*] \-\-output *FILE* \-\- *BUILD_COMMAND*... **bear semantic** [*OPTIONS*] **bear parse-sh** [*OPTIONS*] # DESCRIPTION Bear produces a JSON compilation database (`compile_commands.json`). Clang-based tools such as clangd and clang-tidy read this file to learn how each source file of a project is compiled: with which compiler, which flags, from which directory. Build systems know this, but most do not export it; Bear recovers it from the build itself. Bear works in two stages. First it captures the commands a build executes, and writes them as a stream of execution events. The capture either observes a real build run, or parses the text of a dry run. Then semantic analysis recognizes the compiler invocations among those events, parses their command lines, and writes the compilation database. ## Modes **Combined mode** (default) : `bear -- <build command>` runs the build, captures and analyzes in one step, and writes the database. This is the recommended way to use Bear: it observes the commands the build actually executes, so the result is exact. Use it whenever you can run the build. **Intercept mode** : `bear intercept` runs the build but only captures, writing the raw event stream to a file. Useful when the build is expensive and you want to run it once and process the events several times, for example to try different output configurations. **Semantic mode** : `bear semantic` runs no build: it is a filter that reads an event stream (from `bear intercept`, or any producer of the documented event format) and writes the compilation database. Combine it with intercept mode to re-generate the database without rebuilding. **Parse-sh mode** : `bear parse-sh` produces the compilation database from shell command text, without executing anything. Typical input is a build system's dry run (`make -n`) or a saved build log. It is a text parser and inherently lower fidelity than interception; reach for it when the build cannot be run, for example to recover a database from a CI log. See its limitations under COMMANDS. ## Interception methods Interception itself has two methods, selectable in the configuration file. **Preload** (the default on Linux and the BSDs) injects a small library into every process the build starts and observes its `exec()` calls. **Wrapper** (the default on macOS and Windows) substitutes the known compilers with a reporting wrapper executable. Preload is transparent to the build but cannot observe statically linked executables, and it is unavailable on Windows and on macOS while System Integrity Protection is enabled; forcing it there is a startup error that names wrapper mode, not a silent fallback. Wrapper mode works on every platform, but the build has to pick the wrapper up: a "configure" step that discovers compilers must itself run under Bear (see TROUBLESHOOTING). ## OPTIONS **-c, \-\-config** *FILE* : Path of the configuration file. It controls the interception method, compiler recognition, source filtering, duplicate handling, and output formatting (see CONFIGURATION). Given before a subcommand it applies to that subcommand too. **-o, \-\-output** *FILE* : Path of the compilation database to write (default: `compile_commands.json`). `-` is not accepted here, because in combined mode standard output belongs to the build. To stream the database, use `bear semantic --output -` (see COMMANDS). **-a, \-\-append** : Append to an existing output file instead of overwriting it. New entries are placed before the existing ones, so a rebuilt file's newest invocation survives duplicate filtering and replaces the stale entry (see `duplicates` under CONFIGURATION). **\-\-overwrite** : Replace the output file with only this run's entries. This is what Bear already does when neither flag is given; the flag exists so a script can name the behaviour it depends on rather than rely on the default. Giving it together with `--append` is an error. A future release makes appending the default, and a script that passes `--overwrite` today keeps its current behaviour across that change. **-h, \-\-help** : Print help. **-V, \-\-version** : Print version. # COMMANDS ## bear intercept Runs the build and captures execution events to a file, without analyzing them. **bear intercept** [*OPTIONS*] \-\-output *FILE* \-\- *BUILD_COMMAND*... **-o, \-\-output** *FILE* : Path of the event file to write. Required, and `-` is not accepted: the build owns standard output, and there is no default event-file name. ## bear semantic Reads an event stream and writes the compilation database. It runs no build, so it behaves as a plain filter: diagnostics go to standard error, and both ends can be standard streams. **bear semantic** [*OPTIONS*] **-i, \-\-input** *FILE* : Path of the event file to read (default: `-`, standard input). Any producer of a conforming event stream can pipe into it. When standard input is a terminal, or the stream turns out to be empty, a warning is printed on standard error. **-o, \-\-output** *FILE* : Path of the compilation database to write (default: `compile_commands.json`). Pass `-` to write to standard output; that write is neither atomic nor appendable, so `--output -` together with `--append` is rejected. **-a, \-\-append** : Same as in combined mode: place new entries before the existing ones in the output file. **\-\-overwrite** : Same as in combined mode: name the current default explicitly. Not accepted together with `--append`. ## bear parse-sh Produces the compilation database from shell command text, without running anything. Typical input is a build system's dry-run output, such as `make -n`, or a saved build log: make -n | bear parse-sh **bear parse-sh** [*OPTIONS*] **-i, \-\-input** *FILE* : Path of the shell text to parse (default: `-`, standard input). **-o, \-\-output** *FILE* : Path of the compilation database to write (default: `compile_commands.json`). Pass `-` to write to standard output; that write is neither atomic nor appendable, so `--output -` together with `--append` is rejected. **-a, \-\-append** : Same as in combined mode: place new entries before the existing ones in the output file. **\-\-overwrite** : Same as in combined mode: name the current default explicitly. Not accepted together with `--append`. **-C, \-\-directory** *DIR* : Initial working directory for the parsed commands. Use it for input captured elsewhere (a CI log, a dry run from another checkout). Give an absolute path; the directory need not exist on this machine. The commands recognized in the text go through the same semantic analysis and output stage as an intercepted build, so the configuration applies exactly as in combined mode. Parsed commands that are not compiler invocations (`ar`, `mv`, `mkdir`) simply produce no entries. It understands a documented subset of POSIX shell syntax: word splitting and quoting; the `;`, `&&`, `||`, `&`, and `|` separators; comments; redirections; `cd`; brace groups (`{ ...; }`, whose `cd` persists past the closing brace); and make's `Entering directory` / `Leaving directory` markers, which track the working directory across a recursive build. Sub-makes print these markers by default; run `make -nw` to have the top-level make print them too, so the log itself names the build root. A line using anything outside that subset is skipped and reported on standard error with its line number and reason. This covers subshells, command substitution, parameter expansion, globs in the executable position, here-documents, unterminated quotes, and shell keywords such as `if` or `for`. The run succeeds as long as at least one line parsed as a command. Interception remains the higher-fidelity default: it observes the `exec()` calls a build really makes, while `bear parse-sh` reconstructs approximate events from text the build system chose to print. A dry run can omit commands (recursive make does not always propagate `-n`, and commands behind not-yet-generated sources never print); the environment and `PATH` used to resolve bare executable names are the parse-time ones, not the real build's; and only POSIX `sh` text is supported. Prefer `bear -- <build command>` whenever the build can actually be run. The source files named in the parsed commands need not exist: entries are reconstructed from the text alone. The one exception is the `canonical` path format, which resolves symlinks on disk; when the sources are absent, configure `absolute` instead (see `format.paths` under CONFIGURATION). # OUTPUT Bear writes a JSON compilation database conforming to the Clang JSON Compilation Database specification (see SEE ALSO): a JSON array of entry objects with the following fields. **directory** : The working directory of the compilation. **file** : The main translation unit source file. **arguments** : The compilation command as an array of strings. This is the default; Bear prefers it over `command` because it avoids shell-escaping issues. **command** : The compilation command as a single shell-escaped string (written instead of `arguments` when `format.entries.use_array_format` is `false`). **output** : The file produced by the compilation (optional). By default Bear does not transform paths: each entry records them as they appeared in the intercepted invocation, so `file` and `output` are often relative to `directory`. Use `format.paths` in the configuration to normalize them. Two compilers produce an entry shape worth knowing about: - **swiftc**: a whole-module invocation naming several `.swift` sources produces one entry per source, each carrying the complete invocation's arguments (every source of the module, not just its own). This matches the shape CMake emits and SourceKit-LSP consumes: in whole-module compilation each file's semantics depend on every other source. The repeated argument data is expected, not a bug. The internal `swift-frontend` jobs that `swiftc` spawns are filtered out automatically. - **valac**: one entry per `valac` invocation (valac compiles all of a target's `.vala`/`.gs` sources together as one unit); the entry's `file` is the first source and every source is kept in the command. Since valac transpiles to C and compiles the result, the database also contains entries for the generated C files (see TROUBLESHOOTING for the clangd consequences). # CONFIGURATION Bear reads a YAML configuration file, found by search (see FILES) or named with `--config`. Without one, built-in defaults apply. A file that is written at all must name `schema`; every section below it is optional. The example below shows every section: ```yaml schema: "4.2" intercept: mode: wrapper # default: preload on Linux and the BSDs compilers: - path: /usr/bin/cc as: gcc - path: /usr/local/bin/gcc ignore: true sources: directories: - path: /project/tests action: exclude files: - pattern: "moc_*.cpp" action: exclude - pattern: "*.pb.cc" action: exclude duplicates: match_on: # default: directory, file - file - arguments format: paths: directory: canonical # default: as-is file: canonical # default: as-is entries: use_array_format: true include_output_field: true arguments: from_response_files: false from_environment: true headers: enabled: true # default: false strategy: siblings ``` Every value above is illustrative; the non-default ones are annotated inline. Each section below states its actual defaults. ## intercept Controls the interception method (see DESCRIPTION for the trade-offs): - **mode**: `preload` (default on Linux and the BSDs) or `wrapper` (default on macOS and Windows; works everywhere). ## compilers Hints for compiler recognition. Bear recognizes GCC- and Clang-compatible drivers (including cross-compiler prefixed names), the common vendor compiler drivers and standalone assemblers, `swiftc` and `valac`, compiler launchers, and MPI compiler wrappers. Run `bear semantic --print-compilers` for the full list of recognized compiler names, each with the `as` value it maps to. A few recognition behaviors are worth knowing. Compiler launchers (`ccache`, `distcc`, `sccache`, `icecc`) are dropped from the recorded command: `ccache gcc -c main.c` produces an entry for `gcc -c main.c`. MPI compiler wrappers are recorded as invoked, not expanded to the compiler they wrap; Clang tooling that needs the wrapper's baked-in include paths can point at it directly, for example with clangd's `--query-driver`. C++20 module-interface units (`.cppm`, `.ixx`, and similar) are recognized as sources, while precompiled `.pcm` artifacts never appear as an entry's `file`. In wrapper mode this section also decides what gets wrapped. With no entry other than `ignore` ones, Bear scans PATH at startup and wraps every compiler it recognizes. As soon as one entry is not ignored, Bear wraps exactly the listed paths and skips the scan, so listing one unusual compiler stops `gcc` and `clang` from being intercepted unless they are listed too. Compilers named in the compiler environment variables (`CC`, `CXX`, and similar) are wrapped either way. Preload mode is unaffected: it intercepts every executed program regardless of this section. Use this section when a compiler at a given path is not recognized, or is recognized wrongly: - **path**: Path of the compiler executable. Required. - **as**: Compiler type hint for semantic analysis. Optional; when omitted, Bear guesses the family from the executable's filename and falls back to `gcc` when nothing matches. The accepted values are the `as` names shown by `bear semantic --print-compilers`, plus `wrapper` for a compiler launcher. A launcher is recognized only by its own basename, such as `ccache` or `distcc`, so `wrapper` on a path with another basename does not make it recognized. - **ignore**: Exclude this executable's invocations from the database. Optional, default `false`. The generic names `cc`, `c++`, and the HPE Cray PrgEnv wrapper `CC` are classified by probing the executable's `--version` output, since the same basename can be a different compiler depending on the platform or the loaded environment module. When the probe cannot classify the executable, override it: ```yaml compilers: - path: /usr/bin/cc as: clang ``` ## sources Filters entries by source file location and filename. - **directories**: List of directory-based rules, each with a **path** and an **action** (`include` or `exclude`). - **files**: List of filename-glob rules, each with a **pattern** and an **action** (`include` or `exclude`). Rules of each list are evaluated in order, the last matching rule wins, and an entry matched by no rule is included; an empty list includes everything. The two lists compose: an entry is emitted only when both accept it. A file pattern without a path separator matches the source file's basename; a pattern containing a separator matches the full source path as it appears in the entry, so use the same path format as configured in `format.paths.file`. An invalid pattern is rejected during configuration validation with an error naming the offending rule. Typical use for file patterns is dropping machine-generated sources (Qt `moc` output, protobuf stubs) so that linters and editors act only on hand-written code; see EXAMPLES. ## duplicates Controls duplicate detection. - **match_on**: List of entry fields to compare (`file`, `arguments`, `directory`, `command`, `output`). Two entries are duplicates when all of the configured fields match; the first occurrence is kept. The default is `directory` and `file`, so one entry is kept per source file per directory, regardless of arguments; when a build compiles the same file with different flags, add `arguments` to keep an entry per configuration. Combined with `--append` (new entries first), the default means a rebuilt file's newest invocation replaces its stale entry. ## format Controls output formatting. - **paths.directory**, **paths.file**: How to format these fields' paths: `as-is` (default, no transformation), `canonical` (resolve symlinks; requires the path to exist), `relative` (to the `directory` field), or `absolute`. - **entries.use_array_format**: Use the `arguments` array (default) instead of the `command` string. Set to `false` for consumers that read only the `command` field. - **entries.include_output_field**: Include the `output` field in entries. - **arguments.from_response_files**: Replace `@file` response-file references with the file's tokenized contents (resolved relative to the compilation's working directory, expanded recursively, using the compiler's quoting convention). Disabled by default, in which case an `@file` argument is recorded verbatim; a missing or unreadable file is left literal with a warning. - **arguments.from_environment**: Fold compiler environment variables that act as implicit flags into the arguments. GCC/Clang header-search paths (`CPATH`, `C_INCLUDE_PATH`, `CPLUS_INCLUDE_PATH`, `OBJC_INCLUDE_PATH`) become include flags, and MSVC's `CL` / `_CL_` become leading / trailing options. Enabled by default; set to `false` to record only the command-line flags. (Unrelated to the `CC="gcc -std=c11"` convention handled during interception; see TROUBLESHOOTING.) ## headers Editors and linters often need compile flags for header files, not only for the translation units that were compiled. When enabled, Bear synthesizes an entry for a header by cloning the arguments of a compiled C/C++/Objective-C translation unit, with the source path replaced by the header path and the output flag removed. Off by default. - **enabled**: Turn header-entry synthesis on or off. Default `false`. - **strategy**: How headers are discovered and which translation unit donates the flags: - `siblings` (default): headers receive an entry cloned from a compiled source in the same directory. Zero prerequisites, but the flags are approximate, and headers in directories without compiled sources (the split `include/` + `src/` layout) get nothing. - `dependency-files`: reads the make-style `.d` dependency files the build already emitted (for example via `-MMD`) and synthesizes an entry for each header prerequisite that resolves inside the compilation's working directory. Accurate, and reaches headers in other directories, but requires the build to have left dependency files on disk. The header extension set is fixed (the built-in C-family headers). Synthesized entries pass through duplicate detection like any other entry, so a header with a real compilation entry is not duplicated. # EXIT STATUS In combined and intercept mode, Bear exits with the build command's exit code: 0 when the build succeeds, the same non-zero code when it fails. When the build is killed by a signal, Bear exits non-zero. When the build handles the signal and exits on its own, Bear exits with the build's code, 0 included. In semantic mode, Bear exits 0 on success and non-zero when the analysis fails. In parse-sh mode, Bear exits 0 when the input could be parsed - even when the database comes out empty because the text named no compiler invocations - and also on empty input (with a warning on standard error). It exits non-zero when every non-empty input line was skipped, so a run that understood nothing cannot pass for a successful one. If Bear itself encounters an internal error, it exits non-zero regardless of the build command's status. # ENVIRONMENT **RUST_LOG** : Selects the diagnostic format and verbosity on standard error. When unset, Bear uses the UNIX style, `bear: message` (and `wrapper:` or `preload:` from its helper processes), showing warnings and errors only. When set to a level (`error`, `warn`, `info`, or `debug`, in increasing verbosity), Bear switches to a developer format that tags every line with a timestamp, the level, the emitting process and its pid, and the source location. Debug output is essential for troubleshooting; see TROUBLESHOOTING. # FILES The configuration file `bear.yml` is searched in the following locations, in order; the first file found is loaded: **`./bear.yml`** : The current working directory. **`$XDG_CONFIG_HOME/bear.yml`**, **`$XDG_CONFIG_HOME/Bear/bear.yml`** (Unix) : When `$XDG_CONFIG_HOME` is set. **`$HOME/.config/bear.yml`**, **`$HOME/.config/Bear/bear.yml`** (Unix) : When `$XDG_CONFIG_HOME` is unset. **`%LOCALAPPDATA%\bear.yml`**, **`%LOCALAPPDATA%\Bear\bear.yml`** (Windows) : When `%LOCALAPPDATA%` is set. **`%APPDATA%\bear.yml`**, **`%APPDATA%\Bear\bear.yml`** (Windows) : When `%APPDATA%` is set. # EXAMPLES Generate a database for a Make project: bear -- make For a CMake project in preload mode, only the build needs Bear: cmake -B build bear -- cmake --build build In wrapper mode the configure step must also run under Bear, so it discovers the wrapper as the compiler; discard that run's output: bear -- cmake -B build bear -- cmake --build build Capture once, analyze many times. This allows trying output configurations without rebuilding: bear intercept --output events.json -- make bear semantic --input events.json bear --config strict.yml semantic --input events.json --output strict.json Recover a database from a dry run, without building: make -n | bear parse-sh For a recursive Make build, add `-w`. The top-level make then prints `Entering directory` markers too, and parse-sh resolves every command against the right directory: make -nw | bear parse-sh Update the database after rebuilding one part of the project: bear --append -- make -C src/module Drop generated sources (Qt moc output, protobuf stubs) with a `bear.yml`: ```yaml sources: files: - pattern: "moc_*.cpp" action: exclude - pattern: "*.pb.cc" action: exclude - pattern: "*.pb.h" action: exclude ``` Synthesize entries for headers in a split `include/` + `src/` layout, when the build emits `.d` dependency files: ```yaml headers: enabled: true strategy: dependency-files ``` # TROUBLESHOOTING ## Debug logging Before reporting any issue, run Bear with debug logging enabled and include the output in the report: RUST_LOG=debug bear -- <build command> ## Empty output The most common cause is that the build executed no compiler: an incremental build with everything up to date runs nothing. Bear intercepts executed commands only; it does not read the build files. Run a clean build. In wrapper mode, a "configure" step that detects compilers captures the real compiler path before Bear can substitute the wrapper. Run the configure step under Bear too (discarding that output), then run the build under Bear. ## Compiler variables with flags In wrapper mode Bear accepts the GNU Make convention of a compiler variable carrying a trailing flag or two (`CC="gcc -std=c11" make`): it splits the value on whitespace, resolves the first token as the compiler, and rewrites the variable so the build still sees the flags. This is a Unix / GNU Make convention; native Windows build systems (MSBuild, `nmake`) do not consume `CC`/`CXX` from the environment. For anything beyond simple whitespace-separated tokens (quoting, metacharacters, substitutions), use `CFLAGS` / `CXXFLAGS` / `LDFLAGS` instead; Bear passes those through untouched. ## Preload errors in cross-compilation An error like `version 'GLIBC_2.33' not found (required by .../libexec.so)` means Bear's preload library was built against a newer glibc than the one the SDK's compilers load: the library must be ABI-compatible with the libc of the intercepted process, not only with the host's. Use a Bear build linked against a glibc no newer than the SDK's. The project wiki's Troubleshooting page lists diagnostic commands. ## Language servers on Vala projects vala-language-server reads the `command` field and ignores the `arguments` array; build the database with `format.entries.use_array_format: false`. In a mixed C/Vala project, clangd also indexes the `valac` entries and emits unknown-argument noise on them; suppress it with a `.clangd` file: ```yaml If: PathMatch: .*\.vala Diagnostics: Suppress: '*' ``` ## Getting help Consult the documentation site for known problems and search existing issues before opening a new report. Follow the bug report template, and always include the `RUST_LOG=debug` output. # SEE ALSO **clangd**(1), **clang-tidy**(1), **make**(1) Documentation site, with task pages for build systems, platforms, and toolchains: <https://rizsotto.github.io/Bear/> The Clang JSON Compilation Database specification: <https://clang.llvm.org/docs/JSONCompilationDatabase.html> Project homepage and issue tracker: <https://github.com/rizsotto/Bear> # COPYRIGHT Copyright (C) 2012-2026 by László Nagy <https://github.com/rizsotto/Bear> �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/scripts/����������������������������������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0016302�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/scripts/check-crate-table-coverage.sh�����������������������������������������0000775�0000000�0000000�00000006667�15271777203�0023707�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/sh # Verify that the "### Workspace crates" table in the root CLAUDE.md lists # every workspace member. The root Cargo.toml is the source of truth for # which crates exist; the table must document each of them. # # Run from the repo root: # ./scripts/check-crate-table-coverage.sh # # Exit codes: # 0 - every workspace member appears in the CLAUDE.md crate table # 1 - at least one workspace member is missing from the table # 2 - invocation error (e.g. expected files not found) set -eu script_dir="$(cd "$(dirname "$0")" && pwd)" repo_root="$(cd "${script_dir}/.." && pwd)" cargo_toml="${repo_root}/Cargo.toml" claude_md="${repo_root}/CLAUDE.md" if [ ! -f "${cargo_toml}" ]; then echo "error: workspace manifest not found at ${cargo_toml}" >&2 exit 2 fi if [ ! -f "${claude_md}" ]; then echo "error: root CLAUDE.md not found at ${claude_md}" >&2 exit 2 fi # Members that legitimately should not appear in the table. Keep this list # empty unless a member is genuinely not a documentable crate; prefer # documenting every member instead. skip_members="" # Extract the [workspace] members glob patterns (e.g. "crates/*") from the # manifest, then expand them to the directories that actually contain a # Cargo.toml. Globbing is the source of truth, matching how cargo resolves # the workspace. patterns="$(awk ' /^\[workspace\]/ { in_ws = 1; next } /^\[/ { in_ws = 0 } in_ws && /members[[:space:]]*=/ { collecting = 1 } collecting { line = $0 while (match(line, /"[^"]+"/)) { print substr(line, RSTART + 1, RLENGTH - 2) line = substr(line, RSTART + RLENGTH) } if (line ~ /\]/) { collecting = 0 } } ' "${cargo_toml}")" if [ -z "${patterns}" ]; then echo "error: no [workspace] members found in ${cargo_toml}" >&2 exit 2 fi members="" for pattern in ${patterns}; do # Unquoted on purpose: the glob in ${pattern} (e.g. "crates/*") must # undergo pathname expansion to enumerate the member directories. # shellcheck disable=SC2086 for dir in ${repo_root}/${pattern}; do [ -f "${dir}/Cargo.toml" ] || continue rel="${dir#"${repo_root}/"}" members="${members} ${rel}" done done if [ -z "${members}" ]; then echo "error: workspace member globs matched no crates" >&2 exit 2 fi # Extract the backtick-quoted path from the first column of each table row in # the "### Workspace crates" section. The section ends at the next heading. documented="$(awk ' /^### Workspace crates/ { in_section = 1; next } in_section && /^#/ { in_section = 0 } in_section && /^\|/ { if (match($0, /`[^`]+`/)) { print substr($0, RSTART + 1, RLENGTH - 2) } } ' "${claude_md}")" missing=0 checked=0 for member in ${members}; do skip=0 for s in ${skip_members}; do if [ "${member}" = "${s}" ]; then skip=1 break fi done [ "${skip}" -eq 1 ] && continue checked=$((checked + 1)) found=0 for entry in ${documented}; do if [ "${member}" = "${entry}" ]; then found=1 break fi done if [ "${found}" -eq 0 ]; then echo "MISSING: ${member} is a workspace member but not in the CLAUDE.md crate table" missing=$((missing + 1)) fi done echo echo "Checked ${checked} workspace member(s); ${missing} missing from the crate table." if [ "${missing}" -gt 0 ]; then exit 1 fi �������������������������������������������������������������������������rizsotto-Bear-0c30b27/scripts/check-docs-site.sh����������������������������������������������������0000775�0000000�0000000�00000041133�15271777203�0021610�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/sh # Build the documentation site under `site/` and verify its internal # links. This is the build-and-link check that `.github/workflows/pages.yml` # runs; run it before committing any change under `site/`. # # Run from anywhere: # ./scripts/check-docs-site.sh # # The check has eight parts: # 1. `mdbook build` must succeed AND print no WARN or ERROR line. # Warnings are fatal on purpose: mdBook reports preprocessor and # include failures that way, and they would otherwise ship silently. # A chapter listed in SUMMARY.md whose file is missing is a hard # error because `book.toml` sets `build.create-missing = false`. # Only mdBook's log-level field is matched, not the words "warn" or # "error" anywhere in the output: the log echoes the absolute output # path, and a checkout under, say, /home/warner would otherwise # always fail. # 2. Every inline Markdown link to a `.md` target must resolve to an # existing file. mdBook does NOT check this itself (verified against # mdBook 0.5.4: a link to a nonexistent page builds clean), and the # site uses no plugins, `mdbook-linkcheck` included. Scope: the scan # is textual, so it also reads fenced code blocks and HTML comments. # An example link to a deliberately nonexistent file inside a code # block would be reported as broken; there is no such instance # today. Reference-style link definitions are not scanned: by # convention they carry external URLs only. # 3. Every page under `site/src/` must be listed in SUMMARY.md. mdBook # ignores files that are not, so such a page would never be served. # A commented-out SUMMARY entry does not count as listed, and a # `./page.md` entry counts the same as `page.md`. 404.md is exempt: # mdBook renders it as the not-found page, and listing it would put # it in the navigation. # 4. Every absolute `/Bear/<page>.html` link in 404.md must have a # matching `<page>.md`. That page links absolutely because Pages # serves it for any missing path, so part 2 cannot follow its links. # 5. `src/reference/supported-compilers.md` is generated, in part, from # `crates/semantic/compilers/*.yaml` by # `scripts/generate-supported-compilers.py`. Regenerate it into a # scratch file and diff against the committed page: any difference # means the YAML changed, or someone hand-edited the generated # block, without re-running the generator. Uses only the local # checkout (no network access) and never writes to the committed # page. # 6. `site/src/reference/configuration.md` must not name a config key # that `man/bear.1.md` does not also mention. `man/bear.1.md` is # the owner of configuration keys and defaults (see # `site/CLAUDE.md`), so configuration.md is reference content that # overlaps it by design; this is the drift check for that overlap, # in place of a generator because there is no schema export to # generate the page from. Extraction rule: every backticked, # dot-separated, lowercase/underscore identifier of two or more # segments in configuration.md (for example `format.paths.directory`) # is a candidate key. For each one, its last segment (`directory`) # must appear as a whole word somewhere in man/bear.1.md. This # covers every key documented so far: man/bear.1.md always names a # key's last segment, even where it does not spell out the full # dotted path (compilers, sources, duplicates, and headers keys are # written there under their own section heading, undotted). It does # NOT cover a single-segment top-level key (`schema` is not # dotted, so it is not extracted), and it does not verify a key's # accepted values or default, only that the key name itself is not # invented. A whole-word match is deliberately loose: it does not # confirm the man page documents the key as configuration (a # coincidental prose word would also pass), so this check only # catches a key that man/bear.1.md never mentions at all, such as a # typo or an invented key. That one-directional, name-only check is # enough to catch the drift that matters here: a key added to the # site page without ever being added to the man page. # 8. Every `page.md#anchor` link must resolve to a heading that exists # in the rendered HTML. mdBook does not check anchors and part 2 # strips them, so a renamed heading otherwise ships a link that # lands on the right page at the wrong place. # 7. `src/reference/command-line.md` is generated, in part, from the # installed `bear` binary's own `--help` output by # `scripts/generate-command-line-reference.py`. Same shape as part # 5: regenerate into a scratch file and diff against the committed # page. CRITICAL DIFFERENCE from part 5: that generator only reads # the checkout, but this one needs a BUILT AND INSTALLED `bear` # binary (see site/CLAUDE.md, "Authoritative sources"), which CI # may not have staged at the point this script runs, and a # contributor may not have built at all. So this part does not # treat a missing binary as failure: the generator itself exits 2 # when it finds no usable binary (checking $BEAR_BIN, then the # throwaway-install default), and this script treats exactly that # exit code as "SKIPPED", printing a notice, rather than failing the # whole check. Any other non-zero exit (malformed --help output, a # missing template, ...) is a real problem and still fails part 7. # This makes the script's own exit status depend on whether a # binary happens to be present, which is unusual for this file, but # the alternative (always failing without a binary) would make # ./scripts/check-docs-site.sh unusable for anyone who has not # just run a full build, including most CI stages other than the # one that builds bear itself. # # mdBook must be on PATH. Install the version pinned in # `.github/workflows/pages.yml`, which is the single source of truth for # it. # # Exit codes: # 0 - the site builds clean, every link and page checks out, the # generated compiler and command-line pages are up to date (or # part 7 was skipped for lack of a binary), and every # configuration key on configuration.md is also named in the man # page # 1 - at least one build warning, broken link, unlisted page, a # stale generated page, or a configuration key missing from the # man page # 2 - invocation error (mdbook or python3 missing, site directory not # found) set -eu script_dir="$(cd "$(dirname "$0")" && pwd)" repo_root="$(cd "${script_dir}/.." && pwd)" site_dir="${repo_root}/site" src_dir="${site_dir}/src" summary="${src_dir}/SUMMARY.md" if [ ! -f "${site_dir}/book.toml" ]; then echo "error: no book.toml found at ${site_dir}" >&2 exit 2 fi if ! command -v mdbook >/dev/null 2>&1; then echo "error: mdbook not found on PATH" >&2 echo "hint: install the version pinned in .github/workflows/pages.yml" >&2 exit 2 fi if ! command -v python3 >/dev/null 2>&1; then echo "error: python3 not found on PATH" >&2 exit 2 fi log="$(mktemp)" problems="$(mktemp)" toc="$(mktemp)" scratch="$(mktemp)" keys="$(mktemp)" trap 'rm -f "${log}" "${problems}" "${toc}" "${scratch}" "${keys}"' EXIT # 1. Build, then treat any WARN or ERROR log line as a failure. mdBook # 0.5 prints the level as the first field of the line. if ! mdbook build "${site_dir}" >"${log}" 2>&1; then cat "${log}" echo "FAILED: mdbook build returned a non-zero status" >&2 exit 1 fi cat "${log}" if grep -E '^[[:space:]]*(WARN|ERROR)[[:space:]]' "${log}" >/dev/null 2>&1; then echo "FAILED: mdbook printed WARN or ERROR lines (above)" >&2 exit 1 fi # 2. Inline links to Markdown files must resolve. An optional link title # (`[text](page.md "title")`) and an optional `#anchor` are stripped # before the file is looked up. find "${src_dir}" -name '*.md' | while read -r page; do page_dir="$(dirname "${page}")" grep -oE '\]\([^)]*\.md[^)]*\)' "${page}" 2>/dev/null | sed -E 's/^\]\(//; s/\)$//; s/[[:space:]].*$//; s/#.*$//' | grep -vE '^[a-zA-Z][a-zA-Z0-9+.-]*:' | while read -r target; do if [ ! -e "${page_dir}/${target}" ]; then echo "BROKEN LINK: ${page#"${repo_root}"/} -> ${target}" \ >>"${problems}" continue fi # A link that resolves OUTSIDE src/ exists on disk but not in # the built book: mdBook rewrites it to a sibling .html that # was never rendered, and on the deployed site it escapes the # book root entirely. Repo files must be linked by URL. resolved="$(cd "${page_dir}" && cd "$(dirname "${target}")" \ && pwd)/$(basename "${target}")" case "${resolved}" in "${src_dir}"/*) ;; *) echo "LINK ESCAPES src/: ${page#"${repo_root}"/} -> ${target}" \ >>"${problems}" ;; esac done done # 3. Every page must appear in the table of contents. Collect the paths # SUMMARY.md actually links from its chapter lines: HTML comments are # removed first (a commented-out entry is not served), single-line # ones before the multi-line range so that a self-contained comment # does not open a range that swallows the rest of the file. Only lines # that are a list item or a prefix/suffix chapter are read, and a # leading `./` is normalized away. sed -e 's/<!--.*-->//g' -e '/<!--/,/-->/d' "${summary}" | grep -E '^[[:space:]]*([-*+][[:space:]]+)?\[' | grep -oE '\]\([^)]*\)' | sed -E 's/^\]\(//; s/\)$//; s/[[:space:]].*$//; s/#.*$//; s|^\./||' \ >"${toc}" find "${src_dir}" -name '*.md' | while read -r page; do relative="${page#"${src_dir}"/}" # SUMMARY.md is the table of contents itself. 404.md is rendered by # mdBook as the not-found page and must NOT be listed, or it would # appear in the navigation as a chapter. if [ "${relative}" = "SUMMARY.md" ] || [ "${relative}" = "404.md" ]; then continue fi if ! grep -qxF "${relative}" "${toc}"; then echo "NOT IN SUMMARY.md: ${relative}" >>"${problems}" fi done # 4. The not-found page links with absolute `/Bear/...` URLs, because it # is served for any missing path and relative links would resolve # against that path. Part 2 cannot check those, so check them here: # every `/Bear/<path>.html` must have a `<path>.md` under src/. if [ -f "${src_dir}/404.md" ]; then grep -oE '\(/Bear/[^)]*\.html\)' "${src_dir}/404.md" 2>/dev/null | sed -E 's|^\(/Bear/||; s|\.html\)$||' | while read -r target; do if [ ! -e "${src_dir}/${target}.md" ]; then echo "BROKEN 404 LINK: /Bear/${target}.html has no ${target}.md" \ >>"${problems}" fi done fi if [ -s "${problems}" ]; then cat "${problems}" >&2 echo "FAILED: the site has broken links or unlisted pages" >&2 exit 1 fi # 5. reference/supported-compilers.md must match the generator's output from # the current compiler YAML right now. Render into a scratch file # (the generator's own template read comes from the committed page, # so this never modifies it) and diff. if ! python3 "${script_dir}/generate-supported-compilers.py" "${scratch}" \ >"${log}" 2>&1; then cat "${log}" >&2 echo "FAILED: generate-supported-compilers.py could not run" >&2 exit 1 fi if ! diff -u "${src_dir}/reference/supported-compilers.md" "${scratch}" >"${log}"; then cat "${log}" >&2 echo "FAILED: supported-compilers.md is stale" >&2 echo "fix: python3 scripts/generate-supported-compilers.py" >&2 exit 1 fi # 6. reference/configuration.md must not name a configuration key that # man/bear.1.md never mentions. man/bear.1.md owns configuration keys # and defaults (see site/CLAUDE.md), so configuration.md necessarily # overlaps it; this is the drift check for that overlap, standing in # for a generator since there is no schema export to generate the # page from. Extraction: every backticked, dot-separated, # lowercase/underscore identifier of two or more segments in # configuration.md is a candidate key, for example # `format.paths.directory`. Only the LAST segment (`directory`) is # checked, as a whole word, against man/bear.1.md: that page always # names a key's last segment, even for sections where it does not # spell out the full dotted path (compilers, sources, duplicates, and # headers keys are written there under their own section heading, # undotted). See the header comment for what this rule does and does # not cover. config_page="${src_dir}/reference/configuration.md" man_page="${repo_root}/man/bear.1.md" if [ -f "${config_page}" ]; then if [ ! -f "${man_page}" ]; then echo "error: man page not found at ${man_page}" >&2 exit 2 fi grep -oE '`[a-z][a-z0-9_]*(\.[a-z0-9_]+)+`' "${config_page}" | sed -E 's/^`//; s/`$//' | sort -u >"${keys}" while read -r key; do leaf="${key##*.}" if ! grep -qw -F -- "${leaf}" "${man_page}"; then echo "CONFIG KEY NOT IN MAN PAGE: configuration.md names" \ "'${key}' (checked as '${leaf}'), which man/bear.1.md" \ "never mentions" >>"${problems}" fi done <"${keys}" fi if [ -s "${problems}" ]; then cat "${problems}" >&2 echo "FAILED: configuration.md names a key man/bear.1.md does not mention" >&2 exit 1 fi # 7. reference/command-line.md must match the generator's output from the # currently installed bear binary. Unlike part 5, a binary is not # guaranteed to exist here (see header comment), so a missing binary # is not a failure: the generator signals that case with exit code 2, # and this part turns exactly that into a skip, with a notice, rather # than failing the whole script. Only a present binary is compared. command_line_status="SKIPPED (no bear binary)" set +e python3 "${script_dir}/generate-command-line-reference.py" "${scratch}" \ >"${log}" 2>&1 gen_exit=$? set -e if [ "${gen_exit}" -eq 2 ]; then echo "SKIPPED: command-line.md staleness check, no bear binary found:" sed 's/^/ /' "${log}" elif [ "${gen_exit}" -ne 0 ]; then cat "${log}" >&2 echo "FAILED: generate-command-line-reference.py could not run" >&2 exit 1 else if ! diff -u "${src_dir}/reference/command-line.md" "${scratch}" >"${log}"; then cat "${log}" >&2 echo "FAILED: command-line.md is stale" >&2 echo "fix: python3 scripts/generate-command-line-reference.py" \ "(after building and installing bear to a throwaway prefix," \ "or pointing BEAR_BIN at one; see the script's header comment)" >&2 exit 1 fi command_line_status="up to date" fi # 8. Every `page.md#anchor` link must point at a heading that exists. # mdBook does not check anchors, and part 2 strips them before looking # the file up, so a renamed heading leaves a link that lands on the # right page at the wrong place. The rendered HTML is the only honest # source of anchor ids, because mdBook slugifies headings, drops # inline code markup, and de-duplicates repeats; part 1 has just # produced it. anchors="$(mktemp)" trap 'rm -f "${log}" "${problems}" "${toc}" "${keys}" "${scratch}" "${anchors}"' EXIT python3 - "${src_dir}" "${site_dir}/book" >"${anchors}" <<'PYEOF' import os, re, sys, pathlib src, book = pathlib.Path(sys.argv[1]), pathlib.Path(sys.argv[2]) ids = {} for html in book.rglob("*.html"): text = html.read_text(encoding="utf-8", errors="replace") ids[str(html.relative_to(book))] = set(re.findall(r'id="([^"]+)"', text)) for page in sorted(src.rglob("*.md")): for match in re.finditer(r"\]\(([^)\s]+\.md)#([^)\s]+)\)", page.read_text()): target, anchor = match.group(1), match.group(2) rendered = os.path.normpath( os.path.join(page.parent.relative_to(src), target) ).replace(".md", ".html") where = f"{page.relative_to(src)} -> {target}#{anchor}" if rendered not in ids: print(f"BROKEN ANCHOR: {where} (no such page)") elif anchor not in ids[rendered]: print(f"BROKEN ANCHOR: {where} (no such heading)") PYEOF if [ -s "${anchors}" ]; then cat "${anchors}" >&2 echo "FAILED: a link points at a heading that does not exist" >&2 exit 1 fi echo "OK: site builds clean, links resolve, every page is in SUMMARY.md, supported-compilers.md is up to date, command-line.md is ${command_line_status}, and every configuration.md key is in the man page" �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/scripts/check-requirements-coverage.sh����������������������������������������0000775�0000000�0000000�00000004055�15271777203�0024234�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/sh # Verify that every `implemented` requirement has at least one test # referencing it via a `Requirements: <id>` tag. # # Run from the repo root: # ./scripts/check-requirements-coverage.sh # # Exit codes: # 0 - every implemented requirement has at least one test reference # 1 - at least one implemented requirement has zero references # 2 - invocation error (e.g. script run from wrong directory) set -eu script_dir="$(cd "$(dirname "$0")" && pwd)" repo_root="$(cd "${script_dir}/.." && pwd)" if [ ! -d "${repo_root}/docs/requirements" ]; then echo "error: requirements directory not found at ${repo_root}/docs/requirements" >&2 exit 2 fi # Space-separated search roots. Word splitting on the expansion below is # intentional, so the paths must not contain spaces (they don't). search_roots="${repo_root}/crates ${repo_root}/tests" missing=0 checked=0 for file in "${repo_root}/docs/requirements"/*.md; do [ -e "${file}" ] || continue base="$(basename "${file}" .md)" # Skip the non-requirement files if [ "${base}" = "CLAUDE" ] || [ "${base}" = "TEMPLATE" ]; then continue fi # Extract status from YAML frontmatter (first match wins) status="$(awk '/^status:[[:space:]]*/ { sub(/^status:[[:space:]]*/, ""); print; exit }' "${file}")" if [ "${status}" != "implemented" ]; then continue fi checked=$((checked + 1)) # Count matches across the search roots. A match is any line that contains # "Requirements:" followed (anywhere on the line) by the requirement ID. # Word-boundary on both sides prevents "output-path" from matching # "output-path-format". pattern="Requirements:.*\\b${base}\\b" # shellcheck disable=SC2086 if grep -RnE "${pattern}" ${search_roots} >/dev/null 2>&1; then : else echo "MISSING: ${base} (status: implemented) has no test tagged with its ID" missing=$((missing + 1)) fi done echo echo "Checked ${checked} implemented requirement(s); ${missing} without coverage." if [ "${missing}" -gt 0 ]; then exit 1 fi �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/scripts/check-requirements-references.sh��������������������������������������0000775�0000000�0000000�00000011646�15271777203�0024566�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/sh # Verify that every cross-reference in a requirement file resolves to an # existing requirement or rationale entry, and that every link in a rationale # entry to another entry or to a requirement file resolves, then print review # hints for text that may not belong in a contract. # # A cross-reference is a backticked ID with a known area prefix (`output-append`), # a relative Markdown link to a .md file in the same directory (with or # without a ./ prefix), or a relative Markdown link to ../rationale/*.md. # Area prefixes are taken from the existing filenames. # # In a rationale entry every reference is a Markdown link. A backticked ID # that names an existing requirement (`output-append`) outside a link label # is an unlinked reference and fails the check, printed as file:line. # # Review hints do not affect the exit code. They are heuristic: an ID written # with a .md suffix, and possible implementation leakage (Rust paths, source # file names, crate names). # # Run from the repo root: # ./scripts/check-requirements-references.sh # # Exit codes: # 0 - every cross-reference resolves, and every rationale reference is linked # 1 - at least one cross-reference points to a missing file, or a # rationale entry names a requirement without linking it # 2 - invocation error (e.g. script run from wrong directory) set -eu script_dir="$(cd "$(dirname "$0")" && pwd)" repo_root="$(cd "${script_dir}/.." && pwd)" req_dir="${repo_root}/docs/requirements" rat_dir="${repo_root}/docs/rationale" if [ ! -d "${req_dir}" ]; then echo "error: requirements directory not found at ${req_dir}" >&2 exit 2 fi files="" for file in "${req_dir}"/*.md; do [ -e "${file}" ] || continue base="$(basename "${file}" .md)" # Skip the non-requirement files if [ "${base}" = "CLAUDE" ] || [ "${base}" = "TEMPLATE" ]; then continue fi files="${files} ${file}" done if [ -z "${files}" ]; then echo "error: no requirement files found in ${req_dir}" >&2 exit 2 fi # shellcheck disable=SC2086 prefixes="$(for file in ${files}; do basename "${file}" | sed 's/-.*//'; done | sort -u | paste -sd '|' -)" dangling=0 # shellcheck disable=SC2086 ids="$( { grep -hoE "\`(${prefixes})-[a-z0-9-]+(\.md)?\`" ${files} | tr -d '`' grep -hoE '\]\((\./)?[a-z0-9-]+\.md(#[^)]*)?\)' ${files} | sed 's/^](//; s/^\.\///; s/[#)].*//' } | sed 's/\.md$//' | sort -u )" for id in ${ids}; do if [ ! -f "${req_dir}/${id}.md" ]; then echo "DANGLING: ${id}" grep -nE "\`${id}(\.md)?\`|\]\((\./)?${id}\.md" ${files} | sed "s|^${req_dir}/| |" dangling=$((dangling + 1)) fi done # shellcheck disable=SC2086 rationale="$(grep -hoE '\]\(\.\./rationale/[a-z0-9-]+\.md(#[^)]*)?\)' ${files} | sed 's|^](\.\./rationale/||; s/[#)].*//; s/\.md$//' | sort -u)" for name in ${rationale}; do if [ ! -f "${rat_dir}/${name}.md" ]; then echo "DANGLING: ../rationale/${name}.md" grep -nE "\]\(\.\./rationale/${name}\.md" ${files} | sed "s|^${req_dir}/| |" dangling=$((dangling + 1)) fi done rat_files="" for file in "${rat_dir}"/*.md; do [ -e "${file}" ] || continue base="$(basename "${file}" .md)" # Skip the non-entry files if [ "${base}" = "CLAUDE" ] || [ "${base}" = "TEMPLATE" ]; then continue fi rat_files="${rat_files} ${file}" done rat_links="" if [ -n "${rat_files}" ]; then # shellcheck disable=SC2086 rat_links="$(grep -hoE '\]\((\./|\.\./requirements/)?[a-z0-9-]+\.md(#[^)]*)?\)' ${rat_files} | sed 's/^](//; s/^\.\///; s/[#)].*//' | sort -u || true)" fi for link in ${rat_links}; do if [ ! -f "${rat_dir}/${link}" ]; then echo "DANGLING (from rationale): ${link}" # shellcheck disable=SC2086 grep -nE "\]\((\./)?$(printf '%s' "${link}" | sed 's/\./\\./g')" ${rat_files} | sed "s|^${rat_dir}/| |" dangling=$((dangling + 1)) fi done unlinked=0 for file in ${rat_files}; do # Drop every Markdown link on the line, then look for backticked IDs. found="$(sed -E 's/\[[^]]*\]\([^)]*\)//g' "${file}" | grep -noE "\`(${prefixes})-[a-z0-9-]+\`" || true)" for hit in ${found}; do line="${hit%%:*}" id="$(printf '%s' "${hit#*:}" | tr -d '`')" if [ -f "${req_dir}/${id}.md" ]; then echo "UNLINKED (from rationale): $(basename "${file}"):${line}: ${id}" unlinked=$((unlinked + 1)) fi done done # shellcheck disable=SC2086 grep -nE "\`(${prefixes})-[a-z0-9-]+\.md\`" ${files} | sed "s|^${req_dir}/|REVIEW (write the ID without .md): |" || true # shellcheck disable=SC2086 grep -nE '::|\.(rs|yaml|yml)\b|\bcrate\b' ${files} | sed "s|^${req_dir}/|REVIEW (implementation detail?): |" || true checked="$(printf '%s\n' "${ids}" "${rationale}" "${rat_links}" | grep -c . || true)" echo echo "Checked ${checked} cross-reference(s); ${dangling} dangling, ${unlinked} unlinked." if [ "${dangling}" -gt 0 ] || [ "${unlinked}" -gt 0 ]; then exit 1 fi ������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/scripts/generate-command-line-reference.py������������������������������������0000664�0000000�0000000�00000031513�15271777203�0024746�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python3 # Generate the enumerated tables on site/src/reference/command-line.md from # the installed `bear` binary's own `--help` output -- the actual source of # truth for flags, arguments, subcommands, and defaults, since clap (the # argument parser Bear is built with) derives that text from the same # definitions that parse the command line at runtime. This keeps the page # from hand-copying (and silently drifting from) the man page's OPTIONS and # COMMANDS sections, which are themselves written by hand. # # Locating the binary: there is no `target/debug/bear` to run directly -- # `bear` is a shell wrapper that `scripts/install.sh` generates around # `bear-driver`, embedding the install prefix as a literal path (see # site/CLAUDE.md, "Authoritative sources"). So this script never builds or # installs anything itself; it expects a throwaway install to already exist, # resolved in this order: # # 1. The BEAR_BIN environment variable, if set: an explicit path to a # `bear` executable. # 2. /tmp/bear-review/bin/bear, the throwaway-install location already # documented in site/CLAUDE.md for exactly this purpose. # # If neither resolves to an executable file, the script FAILS LOUDLY with # exit code 2 and a message naming the commands to run -- it never falls # back to stale or partial output. A contributor sets this up with: # # cargo build # SRCDIR=target/debug PREFIX=/tmp/bear-review INTERCEPT_LIBDIR=lib \ # ./scripts/install.sh # python3 scripts/generate-command-line-reference.py # # Determinism: the rendered page is a pure function of the four `--help` # texts (top level, intercept, semantic, parse-sh). No timestamp, hostname, # or path from the machine that ran this script is written to the page, and # the binary's resolved location itself never appears in the output. The # version string is deliberately NOT embedded: `--version` output changes on # every release, which would make this page diff on every release commit for # no reader benefit; the page instead describes the current command-line # shape, not a specific version of it. # # Parsing approach: clap's default help formatter has a small, fixed # structure -- an optional one-line description, one or more "Usage:" # lines, then "Commands:", "Arguments:", and "Options:" sections (each a # list of "<name-or-flags> <description>" lines, two-plus spaces apart), # followed by optional trailing prose with an indented example. This script # parses exactly that shape with a small line-oriented state machine; it # does not implement a general clap-help or argparse-help parser. # # Run from anywhere: # python3 scripts/generate-command-line-reference.py # # Only the region between the BEGIN/END markers in # site/src/reference/command-line.md is replaced; everything outside it # (the page's hand-written explanatory prose) is read from the current file # and carried through unchanged. An optional argument overrides the output # path (used by scripts/check-docs-site.sh to render into a scratch file # without touching the committed page): # # python3 scripts/generate-command-line-reference.py /tmp/scratch.md # # Exit codes: # 0 - the page was (re)generated # 1 - --help output could not be parsed in the expected shape, or the # markers are missing from the template page # 2 - invocation error (no usable bear binary found, or the template page # is missing) import os import re import subprocess import sys from pathlib import Path BEGIN_MARKER = ( "<!-- BEGIN GENERATED: scripts/generate-command-line-reference.py -- " "DO NOT EDIT. Edits inside this block are lost the next time the " "script runs. -->" ) END_MARKER = "<!-- END GENERATED -->" DEFAULT_BINARY = Path("/tmp/bear-review/bin/bear") # (subcommand, invocation shown in headings and usage) SUBCOMMANDS = ["intercept", "semantic", "parse-sh"] DEFAULT_RE = re.compile(r"\s*\[default:\s*(.*?)\]\s*$") SECTION_HEADERS = {"Commands:", "Arguments:", "Options:"} def resolve_binary() -> Path: """Find a usable `bear` executable, or exit loudly. Never returns a path that does not exist and is not executable.""" env_bin = os.environ.get("BEAR_BIN") candidates = [Path(env_bin)] if env_bin else [] candidates.append(DEFAULT_BINARY) for candidate in candidates: if candidate.is_file() and os.access(candidate, os.X_OK): return candidate print("error: no usable bear binary found", file=sys.stderr) print( " checked: " + ", ".join(str(c) for c in candidates) + " (BEAR_BIN env var, then the default throwaway install)", file=sys.stderr, ) print("hint: build and install a throwaway copy first:", file=sys.stderr) print(" cargo build", file=sys.stderr) print( " SRCDIR=target/debug PREFIX=/tmp/bear-review INTERCEPT_LIBDIR=lib " "./scripts/install.sh", file=sys.stderr, ) sys.exit(2) def run_help(binary: Path, args: list[str]) -> str: result = subprocess.run( [str(binary), *args, "--help"], capture_output=True, text=True, check=False, ) if result.returncode != 0: print( f"error: `{binary} {' '.join(args)} --help` exited " f"{result.returncode}", file=sys.stderr, ) print(result.stderr, file=sys.stderr, end="") sys.exit(1) return result.stdout def split_two_spaces(text: str) -> tuple[str, str]: """Split a "<label> <description>" help line on the first run of two or more spaces, the same convention clap uses to align its columns.""" parts = re.split(r"\s{2,}", text.strip(), maxsplit=1) if len(parts) != 2: raise ValueError(f"cannot split into label/description: {text!r}") return parts[0].strip(), parts[1].strip() def parse_help(text: str, context: str) -> dict: """Parse one --help output into a structured dict: description (str), usage (list[str]), commands/arguments/options (list of dict), trailing (list of ('prose'|'example', str) blocks).""" lines = text.splitlines() i = 0 n = len(lines) def skip_blank(): nonlocal i while i < n and not lines[i].strip(): i += 1 skip_blank() description_lines = [] while i < n and not lines[i].startswith("Usage:"): if lines[i].strip(): description_lines.append(lines[i].strip()) i += 1 if i >= n: raise ValueError(f"{context}: no 'Usage:' line found") description = " ".join(description_lines) usage = [lines[i][len("Usage:") :].strip()] i += 1 while i < n and lines[i].strip() and lines[i][0].isspace(): usage.append(lines[i].strip()) i += 1 commands: list[dict] = [] arguments: list[dict] = [] options: list[dict] = [] section = None while i < n: stripped = lines[i].strip() if not stripped: i += 1 continue if stripped in SECTION_HEADERS: section = stripped i += 1 continue if not lines[i][0].isspace(): # A column-0 line that is not a known section header: this is # the start of trailing prose, handled below. break label, desc = split_two_spaces(lines[i]) if section == "Commands:": commands.append({"name": label, "description": desc}) elif section == "Arguments:": arguments.append({"name": label, "description": desc}) elif section == "Options:": m = DEFAULT_RE.search(desc) default = m.group(1) if m else None if m: desc = desc[: m.start()].rstrip() options.append({"flags": label, "description": desc, "default": default}) else: raise ValueError(f"{context}: indented line outside a known section: {lines[i]!r}") i += 1 trailing: list[tuple[str, str]] = [] while i < n: skip_blank() if i >= n: break indented = lines[i][0].isspace() block_lines = [] while i < n and lines[i].strip() and (lines[i][0].isspace() == indented): block_lines.append(lines[i].strip()) i += 1 kind = "example" if indented else "prose" block_lines_text = "\n".join(block_lines) if kind == "example" else " ".join(block_lines) trailing.append((kind, block_lines_text)) return { "description": description, "usage": usage, "commands": commands, "arguments": arguments, "options": options, "trailing": trailing, } def escape_cell(text: str) -> str: return text.replace("|", "\\|") def render_options_table(options: list[dict]) -> list[str]: lines = ["| Flag | Description | Default |", "|---|---|---|"] for opt in options: default = f"`{opt['default']}`" if opt["default"] else "-" lines.append( f"| `{escape_cell(opt['flags'])}` | {escape_cell(opt['description'])} " f"| {default} |" ) lines.append("") return lines def render_arguments_table(arguments: list[dict]) -> list[str]: lines = ["| Argument | Description |", "|---|---|"] for arg in arguments: lines.append(f"| `{escape_cell(arg['name'])}` | {escape_cell(arg['description'])} |") lines.append("") return lines def render_trailing(trailing: list[tuple[str, str]]) -> list[str]: lines = [] for kind, block in trailing: if kind == "prose": lines.append(block) lines.append("") else: lines.append("```sh") lines.extend(block.splitlines()) lines.append("```") lines.append("") return lines def render_usage(usage: list[str]) -> list[str]: return ["```", *usage, "```", ""] def render_top_level(parsed: dict) -> list[str]: lines = ["## Global usage", ""] lines.extend(render_usage(parsed["usage"])) lines.append(parsed["description"]) lines.append("") if parsed["commands"]: lines.append("### Subcommands") lines.append("") lines.append("| Command | Description |") lines.append("|---|---|") for cmd in parsed["commands"]: lines.append(f"| `{escape_cell(cmd['name'])}` | {escape_cell(cmd['description'])} |") lines.append("") if parsed["arguments"]: lines.append("### Arguments") lines.append("") lines.extend(render_arguments_table(parsed["arguments"])) if parsed["options"]: lines.append("### Options") lines.append("") lines.extend(render_options_table(parsed["options"])) lines.extend(render_trailing(parsed["trailing"])) return lines def render_subcommand(name: str, parsed: dict) -> list[str]: lines = [f"## bear {name}", ""] lines.append(parsed["description"]) lines.append("") lines.extend(render_usage(parsed["usage"])) if parsed["arguments"]: lines.append("### Arguments") lines.append("") lines.extend(render_arguments_table(parsed["arguments"])) if parsed["options"]: lines.append("### Options") lines.append("") lines.extend(render_options_table(parsed["options"])) lines.extend(render_trailing(parsed["trailing"])) return lines def render_generated_block(top: dict, subs: dict) -> str: lines = render_top_level(top) for name in SUBCOMMANDS: lines.extend(render_subcommand(name, subs[name])) while lines and lines[-1] == "": lines.pop() return "\n".join(lines) + "\n" def main() -> int: repo_root = Path(__file__).resolve().parent.parent template_path = repo_root / "site" / "src" / "reference" / "command-line.md" if not template_path.is_file(): print(f"error: no template page at {template_path}", file=sys.stderr) return 2 output_path = Path(sys.argv[1]) if len(sys.argv) > 1 else template_path binary = resolve_binary() try: top = parse_help(run_help(binary, []), "bear") subs = { name: parse_help(run_help(binary, [name]), f"bear {name}") for name in SUBCOMMANDS } except ValueError as exc: print(f"error: {exc}", file=sys.stderr) return 1 text = template_path.read_text() if BEGIN_MARKER not in text or END_MARKER not in text: print( f"error: {template_path} is missing the generated-block markers", file=sys.stderr, ) return 1 begin_idx = text.index(BEGIN_MARKER) + len(BEGIN_MARKER) end_idx = text.index(END_MARKER) if end_idx < begin_idx: print("error: END marker precedes BEGIN marker", file=sys.stderr) return 1 generated = render_generated_block(top, subs) new_text = ( text[:begin_idx] + "\n\n" + generated.rstrip("\n") + "\n\n" + text[end_idx:] ) output_path.write_text(new_text) return 0 if __name__ == "__main__": sys.exit(main()) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/scripts/generate-supported-compilers.py���������������������������������������0000664�0000000�0000000�00000026233�15271777203�0024472�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python3 # Generate the enumerated tables on site/src/reference/supported-compilers.md # crates/semantic/compilers/*.yaml, the actual source of truth for compiler # recognition. Those YAML files already drive `bear semantic # --print-compilers` and the codegen recognition table; this script keeps # the site page from hand-copying (and silently drifting from) the same # data. # # Run from anywhere: # python3 scripts/generate-supported-compilers.py # # Only the region between the BEGIN/END markers in # site/src/reference/supported-compilers.md is replaced; everything outside it # page's hand-written explanatory prose) is read from the current file # and carried through unchanged. An optional argument overrides the # output path (used by scripts/check-docs-site.sh to render into a # scratch file without touching the committed page): # # python3 scripts/generate-supported-compilers.py /tmp/scratch.md # # Parsing approach: crates/semantic/compilers/*.yaml follows one fixed, # narrow shape (see crates/semantic/compilers/README.md) -- a handful of # top-level keys, of which only four matter here (`type`, `compiler`, # `recognize`, `ignore_when`); the rest (`flags`, `environment`, # `options`, ...) are irrelevant to this page and skipped whole. No YAML # library is available in this environment (`import yaml` fails; adding # a dependency for four fields read from 23 well-known files is not # justified), so this parses those four shapes directly with a small # line-oriented state machine plus targeted regexes, rather than # implementing anything close to general YAML. # # Exit codes: # 0 - the page was (re)generated # 1 - a YAML file could not be parsed in the expected shape, or the # markers are missing from the template page # 2 - invocation error (compilers directory or template page missing) import re import sys from pathlib import Path from urllib.parse import urlparse BEGIN_MARKER = ( "<!-- BEGIN GENERATED: scripts/generate-supported-compilers.py -- " "DO NOT EDIT. Edits inside this block are lost the next time the " "script runs. -->" ) END_MARKER = "<!-- END GENERATED -->" TOP_LEVEL_KEY = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*):\s*(.*)$") QUOTED = re.compile(r'"([^"]*)"') def parse_bracket_list(text: str) -> list[str]: """Extract the quoted strings out of a flow list like '["a", "b"]'.""" start = text.index("[") end = text.index("]", start) return QUOTED.findall(text[start : end + 1]) def parse_compiler_yaml(path: Path) -> dict: """Parse the four shapes this page needs out of one compiler/wrapper YAML file: `type`, `compiler.id`, `recognize` entries, and `ignore_when`. Everything else in the file (flags, environment, slash_prefix, options, ...) is inert as far as this function is concerned: it is skipped by virtue of not being one of the four top-level keys tracked below. """ kind = None compiler_id = None recognize: list[dict] = [] ignore_when = {"executables": [], "flags": []} section = None current_entry = None in_references = False for raw_line in path.read_text().splitlines(): if raw_line and not raw_line[0].isspace(): m = TOP_LEVEL_KEY.match(raw_line) if m: section = m.group(1) if section == "type": kind = m.group(2).strip() continue # A column-0 comment line outside any key: not a section # change, falls through to the indented handling below. stripped = raw_line.strip() if not stripped or stripped.startswith("#"): continue if section == "compiler": if stripped.startswith("id:"): compiler_id = stripped.split(":", 1)[1].strip() elif section == "recognize": if stripped.startswith("- description:"): m = QUOTED.search(stripped) current_entry = { "description": m.group(1) if m else "", "references": [], "executables": [], "versioned": False, "cross_compilation": False, } recognize.append(current_entry) in_references = False elif stripped == "references:": in_references = True elif in_references and stripped.startswith("-"): m = QUOTED.search(stripped) if m: current_entry["references"].append(m.group(1)) elif stripped.startswith("executables:"): in_references = False current_entry["executables"] = parse_bracket_list(stripped) elif stripped.startswith("versioned:"): in_references = False current_entry["versioned"] = "true" in stripped.split(":", 1)[1] elif stripped.startswith("cross_compilation:"): in_references = False current_entry["cross_compilation"] = ( "true" in stripped.split(":", 1)[1] ) elif section == "ignore_when": if stripped.startswith("executables:"): ignore_when["executables"] = parse_bracket_list(stripped) elif stripped.startswith("flags:"): ignore_when["flags"] = parse_bracket_list(stripped) if kind not in ("compiler", "wrapper"): raise ValueError(f"{path}: no recognized 'type:' (got {kind!r})") if not recognize: raise ValueError(f"{path}: no 'recognize:' entries found") for entry in recognize: if not entry["executables"]: raise ValueError(f"{path}: recognize entry with no executables") if not entry["references"]: raise ValueError(f"{path}: recognize entry with no references") # Wrapper files carry no `compiler.id`: the launcher's own basename # (the file stem, by the directory's own convention) doubles as its # `as:` spelling -- see crates/semantic/compilers/README.md, "the launcher # basename is emitted into WRAPPER_AS_NAMES so `as: mywrapper` is # accepted". family_id = compiler_id if kind == "compiler" else path.stem return { "id": family_id, "kind": kind, "recognize": recognize, "ignore_when": ignore_when, } def load_families(compilers_dir: Path) -> tuple[list[dict], list[dict]]: families = [parse_compiler_yaml(p) for p in sorted(compilers_dir.glob("*.yaml"))] compilers = sorted( (f for f in families if f["kind"] == "compiler"), key=lambda f: f["id"] ) wrappers = sorted( (f for f in families if f["kind"] == "wrapper"), key=lambda f: f["id"] ) return compilers, wrappers def doc_link(url: str) -> str: netloc = urlparse(url).netloc if netloc.startswith("www."): netloc = netloc[4:] return f"[{netloc}]({url})" def doc_links(urls: list[str]) -> str: return ", ".join(doc_link(u) for u in urls) def version_example(base: str) -> str: return f"`{base}-12`" def cross_example(base: str) -> str: return f"`arm-linux-gnueabihf-{base}`" def render_compiler_family(fam: dict) -> list[str]: lines = [f"#### `{fam['id']}`", ""] lines.append(f"Configuration `as:` value: `{fam['id']}`.") lines.append("") lines.append( "| Executable names | Recognized as | Version suffix | " "Cross-compilation prefix | Documentation |" ) lines.append("|---|---|---|---|---|") for entry in fam["recognize"]: names = ", ".join(f"`{e}`" for e in entry["executables"]) base = entry["executables"][0] version = version_example(base) if entry["versioned"] else "not recognized" cross = ( cross_example(base) if entry["cross_compilation"] else "not recognized" ) lines.append( f"| {names} | {entry['description']} | {version} | {cross} | " f"{doc_links(entry['references'])} |" ) lines.append("") ignored_executables = fam["ignore_when"]["executables"] if ignored_executables: names = ", ".join(f"`{e}`" for e in ignored_executables) lines.append( f"Internal, not user-facing: {names}. Bear recognizes these " "only so it can filter them back out; they never produce a " "database entry." ) lines.append("") ignored_flags = fam["ignore_when"]["flags"] if ignored_flags: flags = ", ".join(f"`{f}`" for f in ignored_flags) lines.append( f"An invocation is also ignored when its arguments include " f"{flags}: that is an internal frontend or codegen call, not " "a user-facing compile." ) lines.append("") return lines def render_wrapper_table(wrappers: list[dict]) -> list[str]: lines = [ "### Compiler launchers", "", "| Executable name | Recognized as | Configuration `as:` value | " "Documentation |", "|---|---|---|---|", ] for fam in wrappers: entry = fam["recognize"][0] names = ", ".join(f"`{e}`" for e in entry["executables"]) lines.append( f"| {names} | {entry['description']} | `{fam['id']}` | " f"{doc_links(entry['references'])} |" ) lines.append("") return lines def render_generated_block(compilers: list[dict], wrappers: list[dict]) -> str: lines = ["### Compiler families", ""] for fam in compilers: lines.extend(render_compiler_family(fam)) lines.extend(render_wrapper_table(wrappers)) # Drop the table's own trailing blank line so the block ends cleanly # regardless of which section rendered last. while lines and lines[-1] == "": lines.pop() return "\n".join(lines) + "\n" def main() -> int: repo_root = Path(__file__).resolve().parent.parent compilers_dir = repo_root / "crates" / "semantic" / "compilers" template_path = repo_root / "site" / "src" / "reference" / "supported-compilers.md" if not compilers_dir.is_dir(): print(f"error: no compilers directory at {compilers_dir}", file=sys.stderr) return 2 if not template_path.is_file(): print(f"error: no template page at {template_path}", file=sys.stderr) return 2 output_path = Path(sys.argv[1]) if len(sys.argv) > 1 else template_path try: compilers, wrappers = load_families(compilers_dir) except ValueError as exc: print(f"error: {exc}", file=sys.stderr) return 1 text = template_path.read_text() if BEGIN_MARKER not in text or END_MARKER not in text: print( f"error: {template_path} is missing the generated-block markers", file=sys.stderr, ) return 1 begin_idx = text.index(BEGIN_MARKER) + len(BEGIN_MARKER) end_idx = text.index(END_MARKER) if end_idx < begin_idx: print("error: END marker precedes BEGIN marker", file=sys.stderr) return 1 generated = render_generated_block(compilers, wrappers) new_text = ( text[:begin_idx] + "\n\n" + generated.rstrip("\n") + "\n\n" + text[end_idx:] ) output_path.write_text(new_text) return 0 if __name__ == "__main__": sys.exit(main()) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/scripts/install.sh������������������������������������������������������������0000775�0000000�0000000�00000013013�15271777203�0020305�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/sh # SPDX-License-Identifier: GPL-3.0-or-later # # Install script for Bear. # # Environment variables: # DESTDIR - staging directory prepended to all paths (default: empty) # PREFIX - installation prefix (default: /usr/local if root, $HOME/.local otherwise) # LIBEXECDIR - absolute directory that holds bear/ (default: $PREFIX/libexec) # INTERCEPT_LIBDIR - library directory name (default: lib) # SRCDIR - directory containing build artifacts (default: target/release) # # Usage: # ./scripts/install.sh # # PREFIX=/usr INTERCEPT_LIBDIR=lib64 ./scripts/install.sh # DESTDIR=/tmp/staging PREFIX=/usr ./scripts/install.sh # DESTDIR=/tmp/staging PREFIX=/usr LIBEXECDIR=/usr/lib ./scripts/install.sh set -eux # --- configuration ----------------------------------------------------------- REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" PREFIX="${PREFIX:-}" if [ -z "$PREFIX" ]; then if [ "$(id -u)" -eq 0 ]; then PREFIX="/usr/local" else PREFIX="$HOME/.local" fi fi # Resolve to absolute path PREFIX="$(cd "$PREFIX" 2>/dev/null && pwd || echo "$PREFIX")" LIBEXECDIR="${LIBEXECDIR:-$PREFIX/libexec}" INTERCEPT_LIBDIR="${INTERCEPT_LIBDIR-lib}" DESTDIR="${DESTDIR:-}" # Strip trailing slash from DESTDIR to avoid double slashes DESTDIR="${DESTDIR%/}" # --- safety guards ------------------------------------------------------------ if [ "$PREFIX" = "/" ]; then echo "error: refusing to operate with PREFIX=/ (would clobber the root filesystem)" >&2 exit 1 fi if [ -n "$DESTDIR" ]; then case "$DESTDIR" in /*) ;; *) echo "error: DESTDIR must be an absolute path, got: $DESTDIR" >&2 exit 1 ;; esac fi # The entry script embeds LIBEXECDIR as a runtime path case "$LIBEXECDIR" in /) echo "error: refusing to operate with LIBEXECDIR=/ (would install into /bear)" >&2 exit 1 ;; /*) ;; *) echo "error: LIBEXECDIR must be an absolute path, got: $LIBEXECDIR" >&2 exit 1 ;; esac LIBEXECDIR="${LIBEXECDIR%/}" # Reject empty or whitespace-only INTERCEPT_LIBDIR if [ -z "$(echo "$INTERCEPT_LIBDIR" | tr -d '[:space:]')" ]; then echo "error: INTERCEPT_LIBDIR must not be empty or whitespace-only" >&2 exit 1 fi # Reject absolute paths case "$INTERCEPT_LIBDIR" in /*) echo "error: INTERCEPT_LIBDIR must be a relative path, got: $INTERCEPT_LIBDIR" >&2 exit 1 ;; esac # --- artifact discovery ------------------------------------------------------- if [ -n "${SRCDIR:-}" ]; then SRCDIR="$SRCDIR" elif [ -d "$REPO_ROOT/target/release" ]; then SRCDIR="$REPO_ROOT/target/release" else echo "error: cannot find build artifacts in target/release/" >&2 exit 1 fi # --- platform detection ------------------------------------------------------- OS="$(uname -s)" case "$OS" in Linux|FreeBSD|NetBSD|OpenBSD|DragonFly) PRELOAD_NAME="libexec.so" HAS_PRELOAD=true ;; Darwin) PRELOAD_NAME="libexec.dylib" HAS_PRELOAD=true ;; *) PRELOAD_NAME="" HAS_PRELOAD=false ;; esac # --- install ------------------------------------------------------------------ # bear-driver and bear-wrapper mkdir -p "$DESTDIR$LIBEXECDIR/bear/bin" install -m 755 "$SRCDIR/bear-driver" "$DESTDIR$LIBEXECDIR/bear/bin/bear-driver" install -m 755 "$SRCDIR/bear-wrapper" "$DESTDIR$LIBEXECDIR/bear/bin/bear-wrapper" # preload library (Unix only) if [ "$HAS_PRELOAD" = true ] && [ -f "$SRCDIR/$PRELOAD_NAME" ]; then mkdir -p "$DESTDIR$LIBEXECDIR/bear/$INTERCEPT_LIBDIR" install -m 755 "$SRCDIR/$PRELOAD_NAME" "$DESTDIR$LIBEXECDIR/bear/$INTERCEPT_LIBDIR/$PRELOAD_NAME" fi # bear entry script mkdir -p "$DESTDIR$PREFIX/bin" tmp_bear_sh="$(mktemp)" trap 'rm -f "$tmp_bear_sh"' EXIT cat > "$tmp_bear_sh" <<ENTRY_SCRIPT #!/bin/sh exec $LIBEXECDIR/bear/bin/bear-driver "\$@" ENTRY_SCRIPT install -m 755 "$tmp_bear_sh" "$DESTDIR$PREFIX/bin/bear" rm -f "$tmp_bear_sh" trap - EXIT # man page if [ -f "$REPO_ROOT/man/bear.1" ]; then mkdir -p "$DESTDIR$PREFIX/share/man/man1" install -m 644 "$REPO_ROOT/man/bear.1" "$DESTDIR$PREFIX/share/man/man1/bear.1" fi # documentation mkdir -p "$DESTDIR$PREFIX/share/doc/bear" if [ -f "$REPO_ROOT/README.md" ]; then install -m 644 "$REPO_ROOT/README.md" "$DESTDIR$PREFIX/share/doc/bear/README.md" fi if [ -f "$REPO_ROOT/COPYING" ]; then install -m 644 "$REPO_ROOT/COPYING" "$DESTDIR$PREFIX/share/doc/bear/COPYING" fi # shell completions (optional - only installed when generated files are present) COMPLETIONS_DIR="$SRCDIR/completions" if [ -d "$COMPLETIONS_DIR" ]; then if [ -f "$COMPLETIONS_DIR/bear.bash" ]; then mkdir -p "$DESTDIR$PREFIX/share/bash-completion/completions" install -m 644 "$COMPLETIONS_DIR/bear.bash" "$DESTDIR$PREFIX/share/bash-completion/completions/bear" fi if [ -f "$COMPLETIONS_DIR/_bear" ]; then mkdir -p "$DESTDIR$PREFIX/share/zsh/site-functions" install -m 644 "$COMPLETIONS_DIR/_bear" "$DESTDIR$PREFIX/share/zsh/site-functions/_bear" fi if [ -f "$COMPLETIONS_DIR/bear.fish" ]; then mkdir -p "$DESTDIR$PREFIX/share/fish/vendor_completions.d" install -m 644 "$COMPLETIONS_DIR/bear.fish" "$DESTDIR$PREFIX/share/fish/vendor_completions.d/bear.fish" fi if [ -f "$COMPLETIONS_DIR/bear.elv" ]; then mkdir -p "$DESTDIR$PREFIX/share/elvish/lib" install -m 644 "$COMPLETIONS_DIR/bear.elv" "$DESTDIR$PREFIX/share/elvish/lib/bear.elv" fi fi echo "Bear installed to $PREFIX" ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/scripts/postprocess-docs-site.sh����������������������������������������������0000775�0000000�0000000�00000052665�15271777203�0023133�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/sh # Post-process the built documentation site under `site/book/` for search # and AI-assistant discovery: generate a sitemap and robots.txt, give # each page its own meta description, and inject a canonical link plus # JSON-LD structured data into every rendered page's <head>. # # Run after `mdbook build site` (the same build that # `./scripts/check-docs-site.sh` performs): # mdbook build site # ./scripts/postprocess-docs-site.sh # # Why this cannot be done through mdBook's theme/head.hbs: # `head.hbs` is rendered once per SOURCE page and only exposes the # source `.md` path (`{{ path }}`); mdBook does not expose the # computed output URL to templates (verified against mdBook 0.5.4), so # a per-page canonical href cannot be produced there. This script # instead edits the already-rendered HTML in `site/book/`, where the # final path is simply the file's own location on disk. # # What it does, in order: # 1. Writes `site/book/sitemap.xml`, one <url> per rendered page, in # the site's live https://rizsotto.github.io/Bear/ namespace. # Excluded: any page mdBook itself marks # `<meta name="robots" content="noindex">` (this covers the # whole-book concatenation `print.html` and the sidebar-iframe # fragment `toc.html`), `404.html` (error pages do not belong in a # sitemap regardless of any meta tag), and any redirect stub (a # page whose only job is a meta-refresh to another URL). Listing a # noindex or error page is a Search Console "submitted URL not # indexed" report waiting to happen. # 2. Writes `site/book/robots.txt`, allowing all crawlers and pointing # them at the sitemap. # 3. Rewrites every rendered page's `<meta name="description">` (mdBook # stamps the single book.toml description onto all of them) with a # description built from that page's OWN prose, read out of `<main>` # in document order: `<p>` elements are collected (skipping anything # nested inside `<pre>`, `<table>`, `<ul>`, `<ol>`, or `<blockquote>`, # none of which is prose) and joined with a single space until the # joined text reaches roughly 120 characters or the page runs out of # paragraphs, because this site's answer-first authoring rule means # a lone first paragraph is often just a lead-in clause ending in # `:` ("Run this from the directory holding the Makefile:"), too # thin on its own to work as a search snippet. Tags are stripped and # entities decoded as the paragraphs are read, and the joined text # is truncated to about 155 characters on a word boundary. See # compute_description below for the exact rule, including the # trailing-colon guard, and its fallback to the book-level # description (used for the home page, and any page with no usable # paragraph at all, such as 404.html). A page with no `<meta # name="description">` element at all (the sidebar-iframe fragment # toc.html) is left untouched rather than gaining one, since # nothing else in the pipeline expects it to carry meta tags. # 4. Injects, into every rendered page's <head>, a # `<link rel="canonical">` to that page's own live URL, plus # JSON-LD in a `<script type="application/ld+json">` block: a # `BreadcrumbList`, and, on the home page only, a # `SoftwareApplication` block. The pages that make up a page's # breadcrumb trail come from its OUTPUT path (SUMMARY.md is never # parsed), but each crumb's label is the real chapter name read out # of that page's own rendered <title> (see get_page_title below); a # title-cased path segment is only a fallback for the rare page # with no <title> to read, or no index page for a directory # segment. See docs/rationale/structured-data-scope.md for which # other structured-data types were considered and rejected, and # why. A single HTML comment marker guards the whole injected # block, so re-running this script against already-processed # output is a no-op (checked per page, not by diffing the whole # file). # # mdBook must already have built `site/book/`. This script does not # invoke mdbook itself, so it can be re-run standalone against existing # output. # # Exit codes: # 0 - sitemap, robots.txt, and per-page rewrite all succeeded # 2 - invocation error: `site/book` is missing, `awk` or `python3` is # not on PATH, or a rendered page has no `<head>` element to # inject into set -eu script_dir="$(cd "$(dirname "$0")" && pwd)" repo_root="$(cd "${script_dir}/.." && pwd)" book_dir="${repo_root}/site/book" base_url="https://rizsotto.github.io/Bear/" marker="<!-- postprocess-docs-site: generated, do not edit by hand -->" # The single description book.toml stamps onto every page (see # site/book.toml). Used verbatim for the home page and the # SoftwareApplication JSON-LD, and as the fallback for any other page # whose first paragraph cannot be turned into a description (see # compute_description below). book_description="Generate compile_commands.json for any C or C++ build." if [ ! -d "${book_dir}" ]; then echo "error: no book directory found at ${book_dir}" >&2 echo "hint: run 'mdbook build site' first" >&2 exit 2 fi if ! command -v awk >/dev/null 2>&1; then echo "error: awk not found on PATH" >&2 exit 2 fi if ! command -v python3 >/dev/null 2>&1; then echo "error: python3 not found on PATH" >&2 exit 2 fi page_list="$(mktemp)" tmp_sitemap="$(mktemp)" tmp_block="$(mktemp)" tmp_page="$(mktemp)" trap 'rm -f "${page_list}" "${tmp_sitemap}" "${tmp_block}" "${tmp_page}"' EXIT find "${book_dir}" -name '*.html' | sort >"${page_list}" # --- 1. sitemap.xml --------------------------------------------------- { printf '<?xml version="1.0" encoding="UTF-8"?>\n' printf '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n' } >"${tmp_sitemap}" while read -r page; do relative="${page#"${book_dir}"/}" base="$(basename "${page}")" if [ "${base}" = "404.html" ]; then continue fi if grep -q '<meta name="robots" content="noindex">' "${page}"; then continue fi if grep -q 'http-equiv="refresh"' "${page}"; then continue fi if [ "${relative}" = "index.html" ]; then url="${base_url}" else url="${base_url}${relative}" fi printf ' <url><loc>%s</loc></url>\n' "${url}" done <"${page_list}" >>"${tmp_sitemap}" printf '</urlset>\n' >>"${tmp_sitemap}" mv "${tmp_sitemap}" "${book_dir}/sitemap.xml" # --- 2. robots.txt ------------------------------------------------------ { printf 'User-agent: *\n' printf 'Allow: /\n' printf 'Sitemap: %ssitemap.xml\n' "${base_url}" } >"${book_dir}/robots.txt" # --- 3. per-page <head> rewrite: description, canonical, JSON-LD ------- # Derives a page's own meta description from its own prose. This site's # answer-first rule (site/CLAUDE.md content rule 3) means a page's FIRST # paragraph is often just a lead-in clause to a code block ("Run this # from the directory holding the Makefile:"), too thin on its own to # read as a search snippet, so paragraphs are collected in document # order and joined with a single space until the joined text reaches # about 120 characters or the page runs out of paragraphs. Only <p> # elements directly inside <main> count as prose: anything nested inside # <pre> (a code block), <table>, <ul>/<ol> (list items, including a # "loose" list whose items render as inner <p> tags), or <blockquote> (an # admonition) is skipped, and the leading Diataxis-type comment and the # <h1> page header are skipped too, since neither is a <p>. Tags are # stripped and entities decoded per paragraph as they are read (Python's # HTMLParser with convert_charrefs=True does both), and the final joined # text is truncated to about 155 characters on a word boundary (an ASCII # "..." marks a truncation; text already at or under the limit is left # alone). A joined text that still ends with ":" means the loop ran out # of paragraphs before completing a real sentence; rather than ship a # dangling lead-in clause, it is cut back to the end of the last full # sentence, or dropped entirely if there is no earlier sentence to cut # back to. The final text is escaped for an HTML attribute (& and " at # minimum). Prints nothing, rather than a bad description, whenever none # of this yields usable text (no <main>, no <p> inside it, or a # trailing-colon page with no earlier sentence); the caller then falls # back to the book-level description. That fallback is also what covers # 404.html (its first paragraph reads fine in isolation, but a page # that is deliberately excluded from the sitemap as "not meant to be # indexed" should not get bespoke SERP copy either). # $1 = absolute path to the page's rendered HTML file compute_description() { page_file="$1" python3 - "${page_file}" <<'PYEOF' import sys from html.parser import HTMLParser class ProseExtractor(HTMLParser): """Collects the text of every <p> directly inside <main>, in document order, skipping anything nested inside a code block, table, list, or admonition (see compute_description's shell comment for why).""" SKIP_TAGS = ("pre", "table", "ul", "ol", "blockquote") def __init__(self): super().__init__(convert_charrefs=True) self.main_depth = 0 self.skip_depth = 0 self.in_p = False self.buf = [] self.paragraphs = [] def handle_starttag(self, tag, attrs): if tag == "main": self.main_depth += 1 return if self.main_depth == 0: return if tag in self.SKIP_TAGS: self.skip_depth += 1 return if self.skip_depth == 0 and tag == "p": self.in_p = True self.buf = [] def handle_endtag(self, tag): if tag == "main": self.main_depth = max(0, self.main_depth - 1) return if self.main_depth == 0: return if tag in self.SKIP_TAGS: self.skip_depth = max(0, self.skip_depth - 1) return if self.skip_depth == 0 and tag == "p" and self.in_p: text = " ".join("".join(self.buf).split()) if text: self.paragraphs.append(text) self.in_p = False self.buf = [] def handle_data(self, data): if self.in_p and self.skip_depth == 0: self.buf.append(data) def cut_dangling_colon(joined): """joined ends with ":" (a lead-in with nothing after it in the collected text yet). Cuts back to the end of the last complete sentence inside it, or returns "" if there is no earlier sentence to cut back to.""" cut_at = max(joined.rfind(". "), joined.rfind("! "), joined.rfind("? ")) return joined[: cut_at + 1] if cut_at != -1 else "" with open(sys.argv[1], encoding="utf-8") as f: source = f.read() parser = ProseExtractor() parser.feed(source) paragraphs = parser.paragraphs if not paragraphs: sys.exit(0) # A paragraph both SHORT and colon-terminated is a bare lead-in to # whatever code block follows it ("Then confirm it is on your PATH:"), # not free-standing prose, so a LATER one (the page's own first # paragraph is always kept regardless, see below) is dropped rather than # appended: installation.html has three such paragraphs in a row before # its first real sentence, and joining all three reads as three # disconnected instructions, not "one or two whole sentences". A LONG # paragraph that happens to end in ":" (introducing a JSON example, say) # is still real content and is kept; if it is the last thing collected, # the dangling-colon handling below trims it back to its last complete # sentence instead. lead_in_limit = 80 target = 120 eligible = paragraphs[:1] + [ p for p in paragraphs[1:] if not (p.endswith(":") and len(p) < lead_in_limit) ] text = "" collected = [] for paragraph in eligible: collected.append(paragraph) joined = " ".join(collected) if len(joined) < target: continue if joined.endswith(":"): resolved = cut_dangling_colon(joined) if not resolved: # The target length was reached only by way of a dangling # lead-in with no earlier sentence anywhere in it yet (e.g. # a single long compound clause: "Source the environment # script, then run the build exactly as you would for any # other compiler:"). Keep pulling paragraphs instead of # settling for that. continue text = resolved else: text = joined break else: # Ran out of paragraphs without a clean stop; use whatever was # collected, with the same dangling-colon cleanup as a last resort. joined = " ".join(collected) text = cut_dangling_colon(joined) if joined.endswith(":") else joined if not text: sys.exit(0) limit = 155 if len(text) > limit: cut = text[:limit] last_space = cut.rfind(" ") if last_space > 0: cut = cut[:last_space] text = cut.rstrip(" .,;:-") + "..." # Escape for an HTML attribute. Order matters: escape "&" first, so the # """ the next line introduces is not itself re-escaped. escaped = text.replace("&", "&").replace('"', """) print(escaped) PYEOF } # Capitalizes the first letter of each hyphen/underscore-separated word, # e.g. "getting-started" -> "Getting Started". Fallback only, used when a # path segment has no corresponding rendered page to read a real title # from (see get_page_title below, which is what breadcrumb labels use # whenever a page exists). title_case() { printf '%s' "$1" | tr -- '-_' ' ' | awk '{ out = "" for (i = 1; i <= NF; i++) { w = $i first = toupper(substr(w, 1, 1)) rest = substr(w, 2) out = out (i > 1 ? " " : "") first rest } print out }' } # Reads a rendered page's own chapter name out of its <title> element, # e.g. "<title>Recipes - Bear" -> "Recipes". This is reading the # BUILT OUTPUT, not SUMMARY.md: mdBook titles every page # " - " (book title is "Bear", from # book.toml), so stripping the " - Bear" suffix recovers the exact # query-shaped chapter name a human sees in the browser tab, instead of # a title-cased guess at the filename. is used rather than <h1> # because <h1> can carry inline markup (e.g. `<code>` around # compile_commands.json); <title> is the same string as plain text. # Prints nothing (caller falls back to title_case) if the page has no # <title>, which is the case for a couple of internal mdBook fragments # (the sidebar iframe toc.html has none). get_page_title() { file="$1" raw="$(grep -o '<title>[^<]*' "${file}" 2>/dev/null | head -n1)" [ -z "${raw}" ] && return 0 raw="${raw#}" raw="${raw%}" raw="${raw% - Bear}" # Decode the handful of entities mdBook can put in a title (order # matters: & must be decoded last, or a literal "<" produced # by decoding &lt; would be re-decoded into "<"). raw="$(printf '%s' "${raw}" | sed \ -e 's/<//g' \ -e 's/"/"/g' \ -e "s/'/'/g" \ -e 's/&/\&/g')" # JSON-escape backslash and double quote so this can be dropped # straight into a JSON string literal. Backslash first, so the # backslash the quote rule introduces is not itself re-escaped. raw="$(printf '%s' "${raw}" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g')" printf '%s' "${raw}" } # Prints one ListItem JSON object (no surrounding brackets, no comma). # $1 = position $2 = name $3 = item URL make_breadcrumb_item() { printf '{"@type":"ListItem","position":%d,"name":"%s","item":"%s"}' \ "$1" "$2" "$3" } # Builds the itemListElement array (without the enclosing brackets) of a # BreadcrumbList for one page. Every trail starts at Home, whose item is # the truthful site root URL. Each further crumb's label is the real # chapter name read from that page's own (get_page_title); the # output path is used only to find WHICH pages belong in the trail. A # directory segment with no index.html contributes NO crumb at all: a # crumb must link to a real page, and there is no honest URL to give a # "Platforms"-style crumb when platforms/index.html does not exist, so # it is dropped rather than pointed at the site root or a fabricated # page. Positions are renumbered contiguously from 1 as crumbs are # added, so a drop never leaves a gap. # $1 = page path without the .html extension, e.g. "recipes/docker", # "recipes/index", "platforms/linux", "index", "faq" # $2 = that page's own canonical URL (used as the final crumb's item) # $3 = absolute path to that page's own rendered HTML file build_breadcrumb_items() { path_no_ext="$1" page_canonical="$2" page_file="$3" old_ifs="${IFS}" IFS='/' set -- ${path_no_ext} IFS="${old_ifs}" count=$# eval "last_seg=\${${count}}" # A section's own index page (e.g. "recipes/index") is represented by # its directory segment, not by a trailing literal "index" crumb. if [ "${last_seg}" = "index" ] && [ "${count}" -gt 1 ]; then count=$((count - 1)) fi # The home page itself: a single fixed "Home" crumb, nothing more. if [ "${count}" -eq 1 ]; then eval "seg=\${1}" if [ "${seg}" = "index" ]; then make_breadcrumb_item 1 "Home" "${page_canonical}" return 0 fi fi pos=1 items="$(make_breadcrumb_item "${pos}" "Home" "${base_url}")" prefix="" i=1 while [ "${i}" -lt "${count}" ]; do eval "seg=\${${i}}" prefix="${prefix}${seg}/" candidate="${book_dir}/${prefix}index.html" if [ -f "${candidate}" ]; then name="$(get_page_title "${candidate}")" [ -z "${name}" ] && name="$(title_case "${seg}")" pos=$((pos + 1)) items="${items},$(make_breadcrumb_item "${pos}" "${name}" "${base_url}${prefix}index.html")" fi # else: no index page for this directory segment, drop the # crumb entirely rather than link a page that does not exist. i=$((i + 1)) done eval "seg=\${${count}}" name="$(get_page_title "${page_file}")" [ -z "${name}" ] && name="$(title_case "${seg}")" pos=$((pos + 1)) items="${items},$(make_breadcrumb_item "${pos}" "${name}" "${page_canonical}")" printf '%s' "${items}" } while read -r page; do if grep -qF "${marker}" "${page}"; then # Already processed by a previous run of this script: skip, so # re-running never double-injects. continue fi if ! grep -q '<head' "${page}"; then echo "error: no <head> element in ${page#"${repo_root}"/}" >&2 exit 2 fi relative="${page#"${book_dir}"/}" path_no_ext="${relative%.html}" if [ "${relative}" = "index.html" ]; then canonical="${base_url}" is_home=1 else canonical="${base_url}${relative}" is_home=0 fi # The home page keeps the book-level description; it is already the # right sentence for the site root. 404.html gets it too: part 1 # above excludes 404.html from the sitemap on the grounds that an # error page is not meant to be indexed regardless of its content, # and the same reasoning says it should not get bespoke SERP copy # either. Every other page tries its own first paragraph first. if [ "${is_home}" -eq 1 ] || [ "${relative}" = "404.html" ]; then description="${book_description}" else description="$(compute_description "${page}")" [ -z "${description}" ] && description="${book_description}" fi # A page with no <meta name="description"> element at all (the # sidebar-iframe fragment toc.html) is left untouched: there is # nothing to replace, and nothing downstream expects it to have one. if grep -q '<meta name="description" content="' "${page}"; then awk -v desc="${description}" ' /<meta name="description" content="/ { print " <meta name=\"description\" content=\"" desc "\">" next } { print } ' "${page}" >"${tmp_page}" mv "${tmp_page}" "${page}" fi breadcrumb_items="$(build_breadcrumb_items "${path_no_ext}" "${canonical}" "${page}")" breadcrumb_json=$(printf '{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[%s]}' \ "${breadcrumb_items}") { printf '%s\n' "${marker}" printf '<link rel="canonical" href="%s">\n' "${canonical}" printf '<script type="application/ld+json">\n' printf '%s\n' "${breadcrumb_json}" printf '</script>\n' if [ "${is_home}" -eq 1 ]; then software_json=$(printf '{"@context":"https://schema.org","@type":"SoftwareApplication","name":"Bear","description":"%s","url":"https://rizsotto.github.io/Bear/","applicationCategory":"DeveloperApplication","operatingSystem":"Linux, macOS, FreeBSD, OpenBSD, NetBSD, DragonFly BSD, Windows","sameAs":"https://github.com/rizsotto/Bear"}' \ "${book_description}") printf '<script type="application/ld+json">\n' printf '%s\n' "${software_json}" printf '</script>\n' fi } >"${tmp_block}" block="$(cat "${tmp_block}")" awk -v inject="${block}" ' !done && index($0, "</head>") > 0 { print inject done = 1 } { print } ' "${page}" >"${tmp_page}" mv "${tmp_page}" "${page}" done <"${page_list}" echo "OK: sitemap.xml, robots.txt written; canonical and JSON-LD injected" ���������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/�������������������������������������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0015557�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/CLAUDE.md����������������������������������������������������������������0000664�0000000�0000000�00000022274�15271777203�0017045�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# CLAUDE.md - `site/` guide This directory is Bear's user-facing documentation site: an [mdBook](https://rust-lang.github.io/mdBook/) book published to GitHub Pages at `https://rizsotto.github.io/Bear/`. The deploy job runs from `master` only. The site is for users; `docs/` (requirements and rationale) is for contributors, and the two do not overlap. The site follows [Diataxis](https://diataxis.fr/): every page except the three landing pages is one of four types (tutorial, how-to, reference, explanation), and the rules below apply it. ## What lives where | Path | Holds | |---|---| | `book.toml` | Book configuration. Title "Bear"; `site-url = "/Bear/"`. | | `src/SUMMARY.md` | The table of contents. Every page must be listed. | | `src/index.md`, `src/installation.md` | The only pages at the root: the home page, and installing, which sits above the sections. | | `src/404.md` | The not-found page. Deliberately NOT in `SUMMARY.md` (listing it would put it in the navigation). Its links are absolute (`/Bear/...`), because GitHub Pages serves it for any missing path. Keep them in step with `site-url` and the directories below. | | `src/tutorials/` | The tutorial pages. | | `src/guides/` | `troubleshooting.md`, plus `recipes/` below it. | | `src/guides/recipes/` | Task pages plus `index.md`, the recipe index. | | `src/reference/` | `command-line.md`, `configuration.md`, `exit-status.md` and `supported-compilers.md`. `command-line.md` and `supported-compilers.md` are partly generated: only the block between the `BEGIN GENERATED` and `END GENERATED` markers comes from the script; the prose outside it is edited here. | | `src/understanding/` | The explanation pages. | | `src/platforms/` | One page per operating system. | | `theme/head.hbs` | Site-wide static `<head>` content: the Google Search Console token and nothing else. Removing it un-verifies the property. | | `book/` | Build output. Git-ignored, never edited. | A page's folder matches its `SUMMARY.md` section and therefore its Diataxis type. Moving a page between sections means moving its file too. ## Authoritative sources - never document from memory - **Command-line flags, configuration keys, and their defaults**: check them against `man/bear.1.md` or `bear --help`. Pages link the site's `src/reference/` pages, not the man page. - **Recognized compiler and launcher names**: `build-support/compilers-codegen/tests/snapshots/snapshots__snapshot_recognition.snap`, or `bear semantic --print-compilers` for the live form. Do not copy such a list from the wiki, an old release note, or memory. - **Behaviour of a command**: a scratch install of a debug build, or a named integration test under `tests/integration/`. There is no `target/debug/bear`; `bear` is a shell wrapper that `scripts/install.sh` generates with the install prefix baked in: ```sh cargo build SRCDIR=target/debug PREFIX=/tmp/bear-review INTERCEPT_LIBDIR=lib \ ./scripts/install.sh /tmp/bear-review/bin/bear --version ``` ## Content rules 1. **One page per search intent, not per feature.** If a page answers two questions, split it. 2. **The H1 and the chapter name are phrased as the user asks the question**: "Generate compile_commands.json for a Makefile project", not "Combined mode". The `SUMMARY.md` entry becomes the HTML title, so keep it and the H1 in agreement. 3. **The first paragraph gives the working answer**, normally the command to run. Background and mechanism come after. `scripts/postprocess-docs-site.sh` lifts this paragraph as the page's `<meta name="description">` (about 155 characters), so make it a real, self-contained sentence. 4. **Every shell command shown must be verified.** Run it against a debug build, or cite the integration test that exercises it. The page never says so: no "Verified:", no "tested against", no note about what could not be checked. State the behaviour as fact, or leave it out. Put the evidence in the commit message or pull request. 5. **Wiki-migrated text is a source, not a truth.** Verify each claim against current 4.x behaviour. 6. **ASCII only**, as root `CLAUDE.md` ("Output format") says. 7. **Kebab-case file names that match the query**, for example `compile-commands-for-makefile.md`. 8. **Cross-link related pages, and link back to the recipe index.** Every recipe links to `recipes/index.md`, and `recipes/index.md` lists every recipe in the same order as `SUMMARY.md`. The check script only verifies `SUMMARY.md`. 9. **Exactly one Diataxis type per page, declared and obeyed.** The first line of every source file names its type: ```markdown <!-- Diataxis type: how-to --> ``` A how-to does not explain mechanism; it links to `how-it-works.md`. An explanation page carries no commands to copy; it links to the recipe. 10. **A page states facts about Bear, never facts about itself.** No roadmap of the headings ("The rest of this page covers ..."), no statement of what the page is for, no description of its relation to another document, no defending a writing decision. The rules in this file are for the writer; do not restate one on a page. Two forms stay, because they send the reader somewhere: - a tutorial's "By the end of this page you will have ..." opening; - a scope boundary that routes, as `troubleshooting.md` opens by pointing at the empty-database recipe. The test is direction. A sentence that sends the reader somewhere stays; a sentence that describes where they already are goes. ## Code-block tags mdBook's bundled highlight.js picks a grammar from the fence tag, so the tag matters: 1. **Commands with no output shown**: ```` ```sh ````. No `$` prompt, so the block stays copyable. 2. **A command together with its output**: a SINGLE fence tagged ```` ```shell ````. Prefix every command line with `$ ` and leave output lines unprefixed. Do not use `shell` on a block with no prompt. 3. **Output alone, or a file's contents**: a bare fence, no tag. `yaml`, `json`, `dockerfile`, and similar name their own grammar. ## Page types | Type | Purpose | Where | |---|---|---| | tutorial | One narrow path from start to finish, for a first-timer. Few of them. | `src/tutorials/` | | how-to | The paths a reader can take through a task, dwelling only where attention is needed. | `src/guides/`, `src/platforms/` | | reference | Enumeration, looked up rather than read. Must be exactly correct. | `src/reference/` | | explanation | Mechanism and design. No commands. | `src/understanding/` | Prefer generating a new reference page over writing one. `src/index.md`, `src/guides/recipes/index.md` and `src/404.md` are the only files allowed to declare `<!-- Diataxis type: landing (navigation page, not one of the four types) -->`. Do not add a fourth. ### What a how-to owes the reader - **Cover the paths, not one path.** Admit the branches the reader's situation can take, and say which one they are on. - **Dwell only where attention is needed.** The parts that just work get a sentence; spend the page on the step that fails, surprises, or needs a decision. - **Point at reference, constantly.** Every flag, key, compiler name and exit status has one home under `src/reference/`. Link it; do not restate it. - **Do not send the reader to troubleshooting instead of reference.** A how-to with nothing left once the reference facts are stripped is not a how-to. Merge it into a sibling, or give it the failure mode that justifies it. ## Navigation sections A new page goes in the `SUMMARY.md` section that matches its type: | Section | Holds | |---|---| | (ungrouped, first) | `installation.md` | | Tutorials | The tutorial pages. | | Guides | How-to pages: the recipes, and troubleshooting. | | Reference | The `src/reference/` pages. | | Understanding Bear | The explanation pages. | | Platform notes | How-to pages shelved by operating system. | Do not rename the sections. Why they are named this way is in [`../docs/rationale/docs-site-over-wiki.md`](../docs/rationale/docs-site-over-wiki.md). ## Before committing Run `./scripts/check-docs-site.sh`. It must print `OK`; its header lists what it checks. CI runs it from `.github/workflows/pages.yml`, which pins the mdBook version to install locally. Two habits it depends on: - Write intra-book links inline, `[text](../recipes/index.md)`, and keep reference-style definitions for external URLs. The link scan reads inline links only, and it also reads fenced code, so an example link to a missing page needs a non-`.md` name. - Regenerate the generated reference pages with their source change: `scripts/generate-supported-compilers.py` after a compiler YAML edit, `scripts/generate-command-line-reference.py` after a CLI change (needs the scratch install above). Do not add mdBook plugins. ## Writing checklist - The page is listed in `SUMMARY.md`, and a recipe on `recipes/index.md` too. - The type comment is on the first line and the page stays inside it. - The first paragraph answers the question. - No sentence describes the page itself. - Every command was run, or an integration test is cited. - Every flag, key, and default was checked against `man/bear.1.md`. - Every compiler name was checked against the recognition snapshot. - Links to related pages, and back to the recipe index. ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/book.toml����������������������������������������������������������������0000664�0000000�0000000�00000001174�15271777203�0017411�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������[book] title = "Bear" description = "Generate compile_commands.json for any C or C++ build." language = "en" src = "src" [build] # A chapter listed in SUMMARY.md whose file is missing must fail the # build. The default (true) silently creates an empty page instead. create-missing = false [output.html] # Root-relative path of the deployed site (https://rizsotto.github.io/Bear/). # mdBook wants the PATH here, not the full URL: a full URL breaks asset # links and the 404 page. site-url = "/Bear/" git-repository-url = "https://github.com/rizsotto/Bear" edit-url-template = "https://github.com/rizsotto/Bear/edit/master/site/{path}" ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/���������������������������������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0016346�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/404.md���������������������������������������������������������������0000664�0000000�0000000�00000003331�15271777203�0017177�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: landing (navigation page, not one of the four types) --> <!-- The links below are absolute on purpose. GitHub Pages serves this page for any path that does not exist, including deep ones such as /Bear/recipes/gone/, and mdBook renders Markdown links here as relative URLs, which would then resolve against that missing directory. Keep these as /Bear/... and keep them in step with site-url in book.toml. --> # Page not found That page does not exist, or it moved. The documentation was reorganized when it moved out of the project wiki, so a link from an older issue, forum post, or search result may point at a page that is now somewhere else. Where things are now: - [Generate compile_commands.json for any C or C++ build](/Bear/index.html) - the home page, and the shortest path to the rest. - [Getting started with Bear](/Bear/tutorials/getting-started.html) - install, run a first build, point clangd at the result. - [Recipes](/Bear/guides/recipes/index.html) - one page per task: build systems, containers, compiler launchers, and each supported toolchain. - [Bear produces an empty compile_commands.json](/Bear/guides/recipes/empty-compilation-database.html) and [Troubleshooting](/Bear/guides/troubleshooting.html) - when the output is wrong. - [How Bear works](/Bear/understanding/how-it-works.html) - the mechanism behind the results. The complete reference for command-line options and configuration keys is the [`bear(1)` man page][manpage]. If you followed a link from somewhere and landed here, the [issue tracker][issues] is the place to report it. [manpage]: https://github.com/rizsotto/Bear/blob/master/man/bear.1.md [issues]: https://github.com/rizsotto/Bear/issues �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/SUMMARY.md�����������������������������������������������������������0000664�0000000�0000000�00000004511�15271777203�0020026�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Summary [Generate compile_commands.json for any C or C++ build](index.md) - [Install Bear](installation.md) # Tutorials - [Getting started with Bear](tutorials/getting-started.md) - [Build an autotools project under Bear](tutorials/autotools-tutorial.md) - [Recover compile_commands.json from a build log](tutorials/compile-commands-from-a-build-log.md) # Guides - [Recipes](guides/recipes/index.md) - [Generate compile_commands.json for a Makefile project](guides/recipes/compile-commands-for-makefile.md) - [Generate compile_commands.json for a CMake project](guides/recipes/cmake.md) - [Capture a complete dry run for bear parse-sh](guides/recipes/dry-run-capture.md) - [Bear produces an empty compile_commands.json](guides/recipes/empty-compilation-database.md) - [Set up clangd for a project without CMake](guides/recipes/clangd-setup.md) - [clangd has no compile command for a header](guides/recipes/headers.md) - [Use Bear with ccache, distcc, or icecc](guides/recipes/ccache-distcc-icecc.md) - [Run Bear inside a Docker container](guides/recipes/docker.md) - [Generate compile_commands.json when cross-compiling](guides/recipes/cross-compilation.md) - [Intercept a 32-bit build on a 64-bit host](guides/recipes/multilib.md) - [Generate compile_commands.json for an STM32 or arm-none-eabi project](guides/recipes/embedded-arm.md) - [Use Bear with a vendor embedded toolchain](guides/recipes/vendor-embedded.md) - [Generate compile_commands.json for an Emscripten project](guides/recipes/emscripten.md) - [Generate compile_commands.json for a CUDA project](guides/recipes/cuda.md) - [Use Bear with Intel oneAPI compilers](guides/recipes/intel-oneapi.md) - [Use Bear with Cray and NVIDIA HPC compilers](guides/recipes/cray-hpc.md) - [Generate compile_commands.json for an MPI project](guides/recipes/mpi.md) - [Troubleshooting](guides/troubleshooting.md) # Reference - [Command-line options](reference/command-line.md) - [Configure Bear](reference/configuration.md) - [Supported compilers](reference/supported-compilers.md) - [Exit status](reference/exit-status.md) # Understanding Bear - [How Bear works](understanding/how-it-works.md) # Platform notes - [Bear on Linux, WSL2, and Docker](platforms/linux.md) - [Bear on macOS](platforms/macos.md) - [Bear on Windows](platforms/windows.md) - [Bear on BSD](platforms/bsd.md) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/guides/��������������������������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0017626�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/guides/recipes/������������������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0021260�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/guides/recipes/ccache-distcc-icecc.md��������������������������������0000664�0000000�0000000�00000015442�15271777203�0025331�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: how-to --> # Use Bear with ccache, distcc, or icecc In the common case Bear needs no configuration for a launcher in front of a compiler. Run the build exactly as you already do, launcher and all: ```sh bear -- ccache gcc -c main.c -o main.o ``` produces this entry in `compile_commands.json`: ```json [ { "file": "main.c", "arguments": ["gcc", "-c", "main.c", "-o", "main.o"], "directory": "/path/to/project", "output": "main.o" } ] ``` The `ccache` token is gone: `arguments` holds the real compiler's argv, taken verbatim from whatever followed the launcher's name, not a resolved or canonicalized path. distcc and icecc drop out the same way; the database never contains an entry whose command is the launcher itself: ```sh bear -- ccache gcc -c main.c -o main.o # -> "gcc", ... bear -- ccache cc -c main.c -o main.o # -> "cc", ... bear -- ccache /usr/bin/gcc -c main.c -o main.o # -> "/usr/bin/gcc", ... ``` In each case `arguments` starts with exactly the token shown in the comment. Bear does not chase the name further; if the build wrote a bare `cc`, that is what ends up in the database. ## The ccache masquerade case Some distributions (Fedora, Arch, Gentoo) put a directory of symlinks ahead of the real compilers on `PATH`, so a plain `cc` or `gcc` resolves to ccache without the build ever typing the word `ccache`. This is different from the case above: there is no launcher token for Bear to drop, because the build's command line never named one. On a host where `/usr/lib64/ccache/cc` is a symlink to `ccache`: ```sh bear -- cc -c main.c -o main.o ``` records `"cc"` as the compiler, exactly as invoked, not resolved past the symlink and not rewritten to `gcc`. Bear classifies the ambiguous `cc` name by probing `--version` (ccache passes that probe through to the real compiler), so the entry parses with the right flag family, but the recorded name stays what the build actually ran. This matches the contract in [Supported compilers](../../reference/supported-compilers.md) for ambiguous names. ## Preload mode versus wrapper mode Both interception modes handle an explicit launcher the same way: a build command of `ccache gcc -c main.c -o main.o` produces the identical single entry (compiler `gcc`, `ccache` dropped) whether [`intercept.mode`](../../reference/configuration.md#intercept) is `preload` or `wrapper`. Wrapper mode has one behavior preload mode does not need: when a build discovers its compiler through `PATH` or a `CC` variable and that lookup lands on a ccache masquerade directory (not an explicit `ccache` token), Bear resolves past the masquerade at discovery time and records the real compiler's absolute path. With `CC=gcc` and a masquerade directory first on `PATH`, Bear logs ``` resolve: masquerade wrapper at /usr/lib64/ccache/gcc; re-resolving 'gcc' past /usr/lib64/ccache ``` and the entry records the real compiler the masquerade pointed at, never the masquerade symlink and never Bear's own `.bear/` wrapper. See "The recursion hazard" below for why this step exists. ## distcc specifics distcc's own options that appear before the compiler name (`-j`, `--jobs`, `-v`, `--verbose`, `-i`, `--show-hosts`, `--scan-avail`, `--show-principal`) are recognized and skipped while Bear looks for the compiler, so they never get mistaken for it and never leak into the recorded arguments. `distcc -j4 clang -c main.c` therefore records `clang -c main.c`, not `-j4 clang -c main.c`. ## icecc specifics icecc follows the same contract as ccache, distcc, and sccache: the `icecc` token is dropped and the real compiler's arguments are recorded as invoked. icecream's `icerun` is a different tool (it runs arbitrary commands on the cluster, not compilations) and is not recognized as a launcher. ## The recursion hazard, and how Bear avoids it Wrapper mode works by prepending its own directory of compiler-named executables to `PATH`. A masquerade wrapper does the same thing for its own purpose. If Bear recorded the masquerade wrapper's path as "the compiler" instead of resolving past it, the two directories would look each other up indefinitely: Bear's wrapper would call the masquerade symlink, which would search `PATH` for the real compiler, find Bear's own wrapper first, and call back into it. Bear avoids this by resolving its lookup `PATH` past any masquerade directory before recording what a bare compiler name points to, so the real compiler is what gets wrapped and recorded, never the masquerade link or Bear's own wrapper. Detection is a filesystem check (does the resolved path's target look like a known launcher binary), not a subprocess probe, and it only changes what Bear resolves, not the build's own child environment: a build that actually wants ccache's caching behavior for other invocations still gets it. Setting `CCACHE_COMPILER` or `CCACHE_PATH`, or stripping the masquerade directory from the build's own `PATH`, were all considered and rejected, because each changes the build's behaviour rather than only Bear's lookup; the reasoning is recorded with the source, under `docs/rationale/`. ## What does not work - **A launcher wrapping another launcher** (`ccache distcc gcc -c main.c`) produces no entry. Bear does not chase a chain of launchers; the first launcher's argument has to be a real compiler. - **A launcher whose argument is not a recognized compiler** (`ccache make all`), or a bare launcher invocation with no argument at all, produces no entry. - **A launcher under a name Bear does not recognize** is not treated as a launcher at all: its own invocation produces no entry (it is not a compiler), though a real compiler it execs directly may still be recorded on its own. Add an override (below) to fix the launcher's own entry instead of relying on that side effect. ## A launcher at a nonstandard name or path If the build wraps the compiler with a launcher under a custom path or a renamed binary, teach Bear the mapping in the configuration file, the same way you would hint an unrecognized compiler: ```yaml schema: "4.2" compilers: - path: /opt/build-tools/my-ccache as: ccache ``` With this override, `my-ccache gcc -c main.c -o main.o` records the same single entry, compiler `gcc`, as a plain `ccache` invocation would. `as` accepts each launcher's own name for the equivalent case with those tools, and `wrapper` as the generic spelling for any of them; see [Configure Bear](../../reference/configuration.md#compilers) for the `compilers:` key in full, and [Supported compilers](../../reference/supported-compilers.md) for the accepted names, generated from Bear's own definitions. Related: [Supported compilers](../../reference/supported-compilers.md) for the recognized launcher and compiler names, [Troubleshooting](../troubleshooting.md) for a database that comes out wrong, and the [Recipes](index.md) index for other tasks. ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/guides/recipes/clangd-setup.md���������������������������������������0000664�0000000�0000000�00000007632�15271777203�0024200�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: how-to --> # Set up clangd for a project without CMake ```sh bear -- make ``` Run your build under Bear once to produce `compile_commands.json`, and clangd picks it up automatically the next time you open a source file in your editor - no CMake, no manual flag lists. ## Where the file has to be clangd searches for `compile_commands.json` starting from the file you are editing: the file's own directory, a `build/` subdirectory of it, then the parent directory, that parent's `build/` subdirectory, and so on up the tree (see clangd's [project setup documentation][clangd-install]). Running `bear -- make` from your project root places the file exactly where this search finds it for any source file under that root, which is why the plain invocation above is usually all you need. Two situations fall outside that search: - the database was generated somewhere other than the project root, for example a separate build directory named with [`--output`](../../reference/command-line.md) (`bear --output build/compile_commands.json`, or `bear intercept`'s own `--output`); - you are editing a source tree that clangd's search does not reach from the file's own location. ## Pointing clangd at a database in a different place Pass `--compile-commands-dir` on clangd's own command line (consult your editor's clangd-integration settings for where to add this): ```sh clangd --compile-commands-dir=/path/to/build ``` or, to keep it in the project instead of your editor configuration, add a `.clangd` file at the project root: ```yaml CompileFlags: CompilationDatabase: build ``` The `CompilationDatabase` key takes a directory (absolute, or relative to the `.clangd` file's own location) to search instead of walking upward from the edited file. See clangd's [configuration documentation][clangd-config] for the complete set of `.clangd` keys; this page only covers the one that matters for finding the database. With `compile_commands.json` in a `build/` directory and sources in a sibling `src/` directory, both `clangd --compile-commands-dir=build` and a `.clangd` file with `CompilationDatabase: build` at the project root make clangd load the database for a file under `src/`. ## Confirming clangd actually loaded it Ask clangd to parse a single file outside of your editor, and look at the lines it logs while loading: ```shell $ clangd --check=path/to/file.c Loaded compilation database from /path/to/build/compile_commands.json Compile command from CDB is: [...] /usr/bin/cc -o main ... ``` `Compile command from CDB is:` means clangd found and used an entry for that exact file. There are two other outcomes worth telling apart. ``` Compile command inferred from greet.c is: [...] -x c-header ... -- /path/to/greet.h ``` clangd found no entry for this file but borrowed one from a neighbour in the same directory. This is the normal, working result for a header, since Bear records the sources a build compiles and headers are not compiled on their own. Take it as success unless the flags it inferred are wrong for that file. ``` Generic fallback command is: [...] clang ... -- /path/to/file.c ``` clangd loaded *some* database but found nothing to work from, not even a neighbour - a sign the file was filtered out of `compile_commands.json`, or was never compiled (see [Bear produces an empty compile_commands.json](empty-compilation-database.md) if the database itself is empty). If it never mentions loading a database at all, it did not find one on its search path or at the configured location; see [Where the file has to be](#where-the-file-has-to-be) above. Related: [Bear produces an empty compile_commands.json](empty-compilation-database.md) if the file has no entries at all, [Generate compile_commands.json for a Makefile project](compile-commands-for-makefile.md), and the [Recipes](index.md) index. [clangd-install]: https://clangd.llvm.org/installation.html [clangd-config]: https://clangd.llvm.org/config.html ������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/guides/recipes/cmake.md����������������������������������������������0000664�0000000�0000000�00000010550�15271777203�0022663�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: how-to --> # Generate compile_commands.json for a CMake project CMake writes `compile_commands.json` itself; turn that on before reaching for Bear: ```sh cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON cmake --build build ``` CMake places the file in the build directory, the one passed to `-B` (`build` above), not the source tree. Setting the same variable through a `CMakePresets.json` `cacheVariables` entry, instead of the `-D` flag on the command line, has the same effect. Point your editor at the file this produces with [Set up clangd for a project without CMake](clangd-setup.md), which covers `--compile-commands-dir` and the `.clangd` `CompilationDatabase` key. For the ordinary CMake project that is the better answer. Bear is for the cases where it is not enough. ## Generators that ignore the variable CMake's own documentation for `CMAKE_EXPORT_COMPILE_COMMANDS` states: "This option is implemented only by Makefile Generators and Ninja Generators. It is ignored on other generators." The Visual Studio and Xcode generators fall into "other generators": setting the variable with either of those has no effect, and no `compile_commands.json` appears no matter how the project builds. Reach for Bear instead, pointed at whatever actually invokes the compiler (see [Command-line options](../../reference/command-line.md) for the `--` split between Bear's own flags and the build command): ```sh bear -- cmake --build build ``` This works regardless of which generator produced the build files; see [How Bear works](../../understanding/how-it-works.md) for why watching the build succeeds where the generator's own export does not. ## When the export does not match what actually ran CMake's export records the command it intends to run, generated at configure time. Three situations make that different from what the build actually executes: - a compiler launcher in front of the compiler (`CMAKE_C_COMPILER_LAUNCHER`, `CMAKE_CXX_COMPILER_LAUNCHER`, or a `ccache`/`distcc` setup baked into the toolchain file); - a wrapper script standing in for the compiler; - a custom command or custom target that compiles something outside CMake's own compile rules. Interception records what the build actually ran instead of what CMake planned: ```sh bear -- cmake --build build ``` See [Use Bear with ccache, distcc, or icecc](ccache-distcc-icecc.md) for the launcher case in detail. ## A superbuild, or CMake as one step of a larger build When CMake is only part of the build - a superbuild that configures and builds several subprojects, or a top-level Makefile that drives `cmake` alongside other steps - CMake's own export only ever covers the CMake part. Run the whole thing under Bear instead, and every compiler invocation ends up in the same database regardless of which step produced it: ```sh bear -- make ``` or, when the top-level driver is itself CMake: ```sh bear -- cmake --build . ``` ## If the project does not use CMake CMake is not the only build system that writes `compile_commands.json` itself. Check whether yours already does before reaching for Bear: - Ninja: `ninja -t compdb > compile_commands.json` - Qbs: `qbs generate --generator clangdb` - Waf: load the `clang_compilation_database` tool from `wscript`, then run `waf clangdb` - Bazel: no native flag, but [Hedron's Compile Commands Extractor](https://github.com/hedronvision/bazel-compile-commands-extractor) does the same job as a third-party target - Clang itself: pass `-MJ <fragment>.json` to every invocation and concatenate the fragments into a single JSON array Use your build system's own export when it has one. Reach for Bear when it does not, or when what it exports does not match what actually ran. ## clangd cannot find the file CMake wrote `compile_commands.json` living in the build directory rather than the project root is the usual reason clangd does not pick it up automatically. See [Set up clangd for a project without CMake](clangd-setup.md) for how clangd searches and for pointing it at the exact directory, and [Bear produces an empty compile_commands.json](empty-compilation-database.md) if the file exists but is missing entries rather than missing entirely. Related: [Generate compile_commands.json for a Makefile project](compile-commands-for-makefile.md) for the equivalent Bear-only workflow when there is no CMake export to fall back on, and the [Recipes](index.md) index. ��������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/guides/recipes/compile-commands-for-makefile.md����������������������0000664�0000000�0000000�00000017174�15271777203�0027402�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: how-to --> # Generate compile_commands.json for a Makefile project Run this from the directory holding the Makefile: ```sh bear -- make ``` Bear runs `make` exactly as it would run on its own, watches every command it executes, and writes `compile_commands.json` next to it, one entry per compiled source file. Nothing about the Makefile changes. Make has no built-in way to export a compilation database (unlike CMake or Meson, which do; see [the home page](../../index.md) if that is your build system instead), so intercepting the real build is the standard way to get one for a Make project. For the full walkthrough of a first run, including what the output looks like, see [Getting started with Bear](../../tutorials/getting-started.md); this page covers everything beyond that first run. ## Autotools: ./configure and make Run `configure` on its own, then build under Bear: ```sh ./configure bear -- make ``` This is enough whenever preload is the active interception method: `configure` only needs to run so it can write a Makefile, and `make` is the command Bear needs to watch. In wrapper mode a build step that discovers the compiler on its own has to run under Bear too, or it never sees the wrapper. See [Configure Bear](../../reference/configuration.md#intercept) for which mode is the default on your platform and how to force one or the other. Practically, the wrapper-mode requirement means running the configure step under Bear as well, and discarding its output: ```sh bear -- ./configure make clean bear -- make ``` Do not combine `./configure` and `make` into a single Bear run (for example `bear -- sh -c './configure && make'`, or a Makefile target that re-runs `configure` itself): `configure`'s throwaway toolchain probes (`conftest.c` and similar) end up recorded alongside the real build. Run `configure` and `make` as two separate Bear invocations instead, as above; the second run's output overwrites the first's, so nothing from the probe phase survives. See [Troubleshooting](../troubleshooting.md#the-output-has-extra-entries) for the mechanics, and for excluding probe files by pattern instead, if you would rather keep one combined run. ## Incremental builds Make only rebuilds what is out of date, and Bear only records commands the build actually executes, so a `make` with nothing to do records nothing: running `bear -- make` a second time in a row against an unchanged tree overwrites `compile_commands.json` with an empty `[]`. Build from clean when you want a complete database: ```sh make clean bear -- make ``` To fold in just the parts you rebuild, add `--append`. It places new entries before the existing ones in the output file, so accumulate the database as you touch different parts of the project instead of rebuilding everything at once: ```sh bear --append -- make main.o bear --append -- make greet.o ``` After the first call the database holds one entry (`main.c`); after the second it holds two, with the newest entry first. On the first call there is nothing to append to, so Bear warns and ignores the option. If a file is later rebuilt with different flags, its newest entry wins by default; see [Configure Bear](../../reference/configuration.md#duplicates) for the matching rule, and [Bear produces an empty compile_commands.json](empty-compilation-database.md) if entries still seem to go missing. ## Parallel builds `bear -- make -j4` needs nothing special: every compiler process, however many run concurrently, reports back to Bear on its own. A parallel build produces the same entries as the serial one. ## Recursive Makefiles A real build under Bear needs no extra flags for `make -C` or `$(MAKE)` recursion: each intercepted command carries its own working directory, so entries come out correctly scoped to the subdirectory they were compiled in, however deep the recursion goes. In a project whose top-level Makefile recurses into a `libgreet/` subdirectory, the database holds `main.c` with `directory` set to the project root and `greet.c` with `directory` set to `libgreet`, with no `-w`/`--print-directory` needed. The `-w` flag matters only for the dry-run path below, not for a real build. Bear itself, not `make`, decides where `compile_commands.json` is written: it always lands in the directory Bear was started from, even when the Makefile it drives is elsewhere. Running `bear -- make -C libgreet` from inside `libgreet/` writes the file there; running the same command from the project root writes it at the root instead. ## A build you cannot re-run When you cannot run the build again, for example to recover a database from a CI log after the fact, feed a dry run into `bear parse-sh` instead of running anything: ```sh make -n | bear parse-sh ``` For a recursive build add `-w`, so the top-level `make` also prints the `Entering directory` markers that sub-makes already print by default; `parse-sh` uses them to resolve each command to the right directory: ```sh make -nw | bear parse-sh ``` Parsing a plain `make -n` for a recursive project from a directory other than the build root puts the top-level source's `directory` at the parsing directory rather than the build root; `make -nw` from the same place resolves it correctly. Without `-w`, the top-level `make` prints no directory marker of its own, so `parse-sh` falls back to its own working directory (or `--directory`, if given) for commands issued before any marker appears. This path is reconstruction from text, not observation, and is lower fidelity than a real build: a dry run can omit commands entirely (recursive `make` does not always propagate `-n` to sub-makes, and commands behind not-yet-generated sources never print), the environment and `PATH` used to resolve bare executable names is the parsing one, not the real build's, and only a documented subset of POSIX shell syntax is understood (subshells, command substitution, and here-documents are not, among others; an unparsed line is skipped and reported on standard error with its line number). Prefer `bear -- make` whenever you can actually run the build; reach for `parse-sh` only when you cannot. See `bear parse-sh` in the [`bear(1)` man page][manpage] for the full list of what it understands, and [Capture a complete dry run for `bear parse-sh`](dry-run-capture.md) for the rest of the capture flags and the cases where the dry run leaves commands out altogether. ## Where the output goes, and how to change it By default Bear writes `compile_commands.json` in the directory it was started from. Use `-o`/`--output` (see [Command-line options](../../reference/command-line.md#global-usage)) to write it somewhere else: ```sh bear -o build/compile_commands.json -- make ``` `-` (standard output) is not accepted for `-o` in combined mode, since standard output belongs to the build; see the [`bear(1)` man page][manpage] for the split `intercept`/`semantic` modes, which do accept it. ## Related pages - [Bear produces an empty compile_commands.json](empty-compilation-database.md) when even a clean build gives you nothing. - [Set up clangd for a project without CMake](clangd-setup.md) for pointing your editor at the database once it exists. - [Use Bear with ccache, distcc, or icecc](ccache-distcc-icecc.md) if your Makefile invokes the compiler through a launcher. - [Run Bear inside a Docker container](docker.md) if the build itself runs in a container. - [How Bear works](../../understanding/how-it-works.md) for the interception mechanism. - [Command-line options](../../reference/command-line.md) and [Configure Bear](../../reference/configuration.md) for the flags and keys named above. - [Recipes](index.md) for the other tasks. [manpage]: https://github.com/rizsotto/Bear/blob/master/man/bear.1.md ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/guides/recipes/cray-hpc.md�������������������������������������������0000664�0000000�0000000�00000005225�15271777203�0023314�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: how-to --> # Use Bear with Cray and NVIDIA HPC compilers Load the programming environment module for the current shell, then run the build under Bear: ```sh module load PrgEnv-cray bear -- make ``` On an HPE Cray system the toolchain is selected by which module is loaded, not by which package is installed: `module load` rewrites `PATH` (and other environment variables) for the shell that runs it. Once the right compiler is on `PATH`, Bear needs nothing else; it watches whatever the build executes, the same as anywhere else. The same applies to an NVIDIA HPC SDK module (`module load nvhpc`) on a cluster that provides one. ## Recognized drivers Cray's own compiler drivers - `craycc`, `crayCC`, `craycxx` (`cray_cc`, built on Clang, with a few Cray-specific extensions such as `-fcray-*`) and `crayftn`/`ftn` (`cray_fortran`) - and the NVIDIA HPC SDK's `nvc`, `nvc++`, `nvfortran` (`nvidia_hpc`) are all recognized directly. The legacy PGI names, `pgcc`, `pgc++`, `pgfortran`, map to the same `nvidia_hpc` family and its flag table (`-Mvect`, `-acc`, `-gpu`, and the rest), since the PGI compiler became the NVIDIA HPC SDK. See [Supported compilers](../../reference/supported-compilers.md) for the full name table. ## `cc`, `CC`, and `ftn`: the PrgEnv wrapper names `cc` and `CC` are ambiguous on a Cray system - either can front CCE, GCC, or another vendor's compiler depending on the loaded programming environment module - so neither is in Bear's static table; Bear classifies them by probing `--version` once and caching the result, the mechanism [Supported compilers](../../reference/supported-compilers.md#ambiguous-names) documents in general. The probe answers `gcc` or `clang` only, never `cray_cc`, so add a `compilers:` entry when you need the family pinned exactly, or when the probe cannot identify the compiler at all. `ftn`, by contrast, is recognized directly as Cray Fortran (`as: cray_fortran`) regardless of which programming environment module is loaded. If a loaded module makes `ftn` front a different Fortran compiler on your system, override it the same way you would any misclassified compiler: ```yaml schema: "4.2" compilers: - path: /opt/cray/pe/craype/default/bin/ftn as: intel_fortran ``` ## Related pages - [Supported compilers](../../reference/supported-compilers.md) for the full recognized-name table and the ambiguous-name probe. - [Generate compile_commands.json for an MPI project](mpi.md) if the build also calls `mpicc`/`mpifort`-style wrappers directly instead of the `cc`/`CC`/`ftn` drivers. - [Configure Bear](../../reference/configuration.md) for the `compilers:` section. - [Recipes](index.md) for the other tasks. ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/guides/recipes/cross-compilation.md����������������������������������0000664�0000000�0000000�00000007413�15271777203�0025254�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: how-to --> # Generate compile_commands.json when cross-compiling Run the cross build under Bear exactly as you would run it directly: ```sh bear -- make ``` Bear records the cross compiler the same way it records a native one: the executable path the build actually invoked, and every argument verbatim, including a cross-compilation target prefix in the name (`arm-none-eabi-gcc`, `aarch64-linux-gnu-g++`) and flags such as `--sysroot` or a target-specific `-I`. Bear does not interpret or rewrite argument values (only `@file` response-file expansion and a couple of environment-variable foldings are optional exceptions, see `format.arguments` in [Configure Bear](../../reference/configuration.md)), so a value like `--sysroot=/opt/arm-sdk/sysroot` lands in the entry's `arguments` exactly as the build passed it. Nothing about the build changes; what is worth knowing is which names get recognized as cross-compiled, and one limitation of the default interception method that shows up specifically in cross builds. ## Prefixed and version-suffixed names GCC, Clang, NVIDIA's `nvcc`, Flang, and Vala's `valac` are recognized under a cross-compilation target prefix, a version suffix, or both together; every such spelling is parsed with its base name's own flag rules. This support varies by family - it is not a blanket rule applied to every entry - so [Cross-compiler prefixes and version suffixes](../../reference/supported-compilers.md#cross-compiler-prefixes-and-version-suffixes) is the source of truth for which families it covers and how the pattern works; this page only covers the cross-build-specific failure below. ## When preload cannot reach the compiler Preload is the default interception method on Linux and the BSDs, and it works for a cross build the same way it works for a native one, with one exception: the preload library is built for the host's ELF class. Many vendor cross-toolchains still ship a 32-bit compiler driver binary even on a 64-bit host (common for older embedded SDKs distributed as prebuilt binaries); loading a 64-bit preload library into that 32-bit process fails with a "wrong ELF class" error from the dynamic linker. The build still completes, but that invocation is not intercepted, so its source file is silently missing from `compile_commands.json`. A related but different failure is a glibc symbol-version mismatch, which [Troubleshooting](../troubleshooting.md#glibc-version-errors-in-cross-compilation) covers in full, including the commands to compare glibc versions and the fix (link a Bear build against a glibc no newer than the SDK's). Either failure means preload cannot inject into that particular compiler process. Switch to wrapper mode instead, since it substitutes the compiler executable rather than injecting into it, so it does not care about the target binary's ELF class or its glibc: ```yaml schema: "4.2" intercept: mode: wrapper ``` Wrapper mode needs the build to discover the wrapper as the compiler, so a "configure" step that detects compilers on its own has to run under Bear too; see [Configure Bear](../../reference/configuration.md) for how to set the mode and [How Bear works](../../understanding/how-it-works.md#two-ways-to-capture-a-real-build) for what each method can and cannot reach. ## Related pages - [Generate compile_commands.json for an STM32 or arm-none-eabi project](embedded-arm.md) for the Arm bare-metal case. - [Use Bear with a vendor embedded toolchain](vendor-embedded.md) for QNX, Microchip XC8, and Texas Instruments. - [Supported compilers](../../reference/supported-compilers.md) for the full name table and the `compilers:` override. - [Generate compile_commands.json for a Makefile project](compile-commands-for-makefile.md) for the general Make workflow. - [Recipes](index.md) for the other tasks. �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/guides/recipes/cuda.md�����������������������������������������������0000664�0000000�0000000�00000007652�15271777203�0022530�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: how-to --> # Generate compile_commands.json for a CUDA project Run the build under Bear exactly as you already run it: ```sh bear -- make ``` Bear recognizes `nvcc`, NVIDIA's CUDA compiler driver, under the `cuda` family (`as: cuda`), including a cross-compilation target prefix and a version suffix (`aarch64-linux-gnu-nvcc`, `nvcc-12.0`). The recorded entry looks like any other compiler entry, with `nvcc`-specific flags (`--gpu-architecture`, `-gencode`, `-Xcompiler`, and similar) parsed and kept in `arguments` verbatim: ```json [ { "file": "main.cu", "arguments": ["nvcc", "-c", "main.cu", "-o", "main.o"], "directory": "/path/to/project", "output": "main.o" } ] ``` See [Supported compilers](../../reference/supported-compilers.md) for the full recognized-name table and how prefix/suffix recognition works in general. ## clangd and clang-tidy do not speak the same nvcc dialect `nvcc`'s command-line flags are its own, not Clang's CUDA mode: options such as `-gencode`, `--generate-code`, `-Xcompiler`, `-Xptxas`, and `--extended-lambda` have no equivalent that Clang-based tooling accepts, so an entry recorded straight from an `nvcc` invocation can make clangd or clang-tidy reject the command line outright when they try to parse a `.cu` file with it. Bear itself has no option to filter or rewrite individual flags in a recorded entry; `format.arguments` in the [`bear(1)` man page][manpage] only covers `@file` expansion and a couple of environment-variable foldings, and `sources` only decides which files get an entry, not what their recorded arguments contain. The fix is on the consumer side. A `.clangd` file can strip the unsupported flags before clangd parses the entry: ```yaml CompileFlags: Remove: [-gencode*, --generate-code*, -Xcompiler*, -Xptxas*, --extended-lambda] ``` If you would rather not have clangd touch `.cu` files at all, suppress its diagnostics on them the same way [the `bear(1)` man page][manpage] recommends for the `valac` entries of a mixed C/Vala project: ```yaml If: PathMatch: .*\.cu Diagnostics: Suppress: '*' ``` Or drop CUDA sources from the database entirely with a `sources` rule in `bear.yml` (see CONFIGURATION in the man page): ```yaml sources: files: - pattern: "*.cu" action: exclude ``` ## nvcc's host-compiler sub-invocations `nvcc` splits a `.cu` file into a device-code path and a host-code path, and for the host side it generates an intermediate file and compiles that with the host compiler (`gcc`, `clang`, or MSVC's `cl`, depending on platform) as a separate process. Bear intercepts that process the same way it intercepts `nvcc` itself, and because the host compiler's intermediate file has a different name than the original `.cu` source, this sub-invocation does not collide with `nvcc`'s own entry under Bear's default duplicate matching (`directory` and `file`): both land in `compile_commands.json` as separate entries, one for the `.cu` file and one for the generated intermediate file the host compiler actually saw. The intermediate file's entry is rarely useful to an editor, since the file it names does not exist once the build finishes; exclude it the same way as `.cu` files above, with a pattern matching the intermediate filename your local `nvcc` generates (for example `"*.cudafe1.cpp"`), or scope a `sources` rule to `directories` instead if the host-compiler pass runs from its own temporary directory. ## Related pages - [Supported compilers](../../reference/supported-compilers.md) for the full recognized-name table and the `compilers:` override for a path Bear gets wrong. - [Generate compile_commands.json for a Makefile project](compile-commands-for-makefile.md) for the general Make workflow. - [Set up clangd for a project without CMake](clangd-setup.md) for pointing your editor at the database once it exists. - [Recipes](index.md) for the other tasks. [manpage]: https://github.com/rizsotto/Bear/blob/master/man/bear.1.md ��������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/guides/recipes/docker.md���������������������������������������������0000664�0000000�0000000�00000012451�15271777203�0023054�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: how-to --> # Run Bear inside a Docker container Run Bear inside the container, as part of the build it observes, the same way you would on a bare host: ```dockerfile RUN bear -- make -j4 ``` or against an already-running container: ```sh docker exec my-container sh -c "bear -- make -j4" ``` This is the working answer for the common case: a normal, dynamically linked build toolchain, building for the container's own architecture. One problem is specific to containers: the paths Bear records belong to the container's filesystem, not the host's. ## Installing Bear in the image Add Bear to the image the same way you would install it anywhere else: your distribution's package if it carries a current-enough one, or a source build otherwise. [Install Bear](../../installation.md) covers both; a source build inside a `Dockerfile` follows the same three commands as [`INSTALL.md`][install]: ```dockerfile # Install the Rust toolchain, a C compiler, and lld or mold first # (see INSTALL.md for the exact prerequisites for your base image). COPY . /src WORKDIR /src RUN cargo build --release && ./scripts/install.sh ``` Build Bear in the same image (or one with the same libc) it will run in. The preload library is a compiled shared object, so a copy built elsewhere only works if that build's libc is no newer than the image's, the same ABI constraint [Troubleshooting](../troubleshooting.md) describes for cross-compilation. ## Why the build has to run inside the container `bear -- docker exec ...` from the host does not work: `docker exec` hands the command to the Docker daemon, which runs it in the container's own process tree, one the host-side Bear process never sees, so it never observes the compiler invocations. [Bear on Linux, WSL2, and Docker](../../platforms/linux.md) explains this in full. ## What the container needs for preload interception [Preload mode](../../reference/configuration.md#intercept) works inside a container with no extra privileges: `LD_PRELOAD` is a plain dynamic-linker feature, not a ptrace-based interception, so it needs no `--privileged` flag and no added capabilities. It does need: - a dynamic linker in the image. A `scratch` or fully static final image has none, so a build that runs in such an image cannot use preload mode there; build in a normal base image instead, or use wrapper mode (below). - the container's libc to match what Bear's preload library was built against, per the ABI note above. - a build tool that is itself dynamically linked. A statically linked build tool never loads the dynamic linker, so its own `exec()` calls are invisible to preload the same way they are on a bare host; this is not a Docker-specific limitation. - working loopback networking, for the TCP connection intercepted processes use to report back to `bear-driver`. Bear binds this on `127.0.0.1` (or `[::1]`), and the driver and the processes it observes are in the same network namespace, so the connection stays inside the container whichever `--network` mode started it. ## When to fall back to wrapper mode If the build tool inside the container is statically linked (a Go build using cgo is the common case; see [How Bear works](../../understanding/how-it-works.md)), set wrapper mode in the configuration file the build reads: ```yaml schema: "4.2" intercept: mode: wrapper ``` As on any platform, a "configure" step that discovers compilers before the real build runs must itself run under Bear so it discovers the wrapper; see [Bear produces an empty compile_commands.json](empty-compilation-database.md#wrapper-mode-the-build-never-picks-up-the-wrapper). ## The path-mapping problem By default Bear records `directory` and `file` exactly as the build used them, with no transformation ([`format.paths`](../../reference/configuration.md#formatpaths)). Inside a container that path is almost always absolute under the container's own filesystem root, for example `/src/main.c`. clangd, clang-tidy, and similar tools normally run on the host, where `/src` does not exist; a `compile_commands.json` copied out of the container as-is points at files the host-side tool cannot find. Three ways to deal with it, in order of how little they change: - **Bind-mount the project at the same absolute path inside the container as on the host**, so the recorded paths are already correct on both sides: ```sh docker run -v "$(pwd):$(pwd)" -w "$(pwd)" myimage bear -- make ``` This needs no post-processing and is the simplest fix when the host and container can agree on a path. - **Rewrite the path prefix after copying the database out**, when the layouts cannot match (a different OS, or a host path the container cannot mount at the same location): substitute the container's mount point for the host's project root in `directory` and `file` with `sed` or `jq` before pointing clangd at the file. - **Run the editor and language server inside the container too** (a dev-container setup), so nothing outside the container ever reads the database, and the mismatch does not arise. Related: [Bear on Linux, WSL2, and Docker](../../platforms/linux.md) for the platform-level notes, [Troubleshooting](../troubleshooting.md) for a database that comes out wrong, and the [Recipes](index.md) index for other tasks. [install]: https://github.com/rizsotto/Bear/blob/master/INSTALL.md �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/guides/recipes/dry-run-capture.md������������������������������������0000664�0000000�0000000�00000021615�15271777203�0024650�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: how-to --> # Capture a complete dry run for bear parse-sh A dry run is only worth parsing if it prints every compiler command, so ask make for all of them at once: ```sh LC_ALL=C make -Bnwk | bear parse-sh ``` Each flag closes off one way the text comes up short. `-n` prints recipes instead of running them. `-B` prints them even in a tree that is already built. `-k` carries on past an error rather than stopping at the first one. `-w` makes the top-level make announce its directory, which sub-makes already do. `LC_ALL=C` keeps those announcements in the English wording Bear matches. Three things no flag fixes: a recursion that never descends, a build tool whose dry run prints progress instead of commands, and a log that was already captured without them. ## A recursive make that never descends Under `-n` make prints a recipe line instead of running it, and that applies to a recipe that starts a make in a subdirectory too: the line prints, the sub-make never runs, and nothing from that subdirectory reaches the text. The exception is a line make recognizes as recursion, which it does by the text, `$(MAKE)` or `${MAKE}` somewhere in the line, or a leading `+`. A Makefile that spells the recursion out literally: ``` all: make -C sub ``` gives a dry run that stops at the top level: ```shell $ make -nw make: Entering directory '/home/you/project' make -C sub make: Leaving directory '/home/you/project' ``` `bear parse-sh` writes `[]` from that and has nothing to report: `make -C sub` is a valid command, it is just not a compiler. Writing the same rule as `$(MAKE) -C sub` makes the sub-make run under `-n` as well and print its own recipes, `Entering directory` markers included. When the Makefile is not yours to change, dry-run each subdirectory separately and combine the results with `--append` (see [Command-line options](../../reference/command-line.md#bear-parse-sh)): ```sh make -nw | bear parse-sh make -nw -C sub | bear parse-sh --append ``` `make -C` announces the directory it changed into, so the appended entries carry the right `directory` whichever directory you run the command from. ## A recipe that leaves its wildcard to the shell An entry whose `file` is `*.c` comes from a recipe that never had its wildcard expanded. `-n` prints the recipe line as written, and the shell that would have expanded the glob never ran, so the glob reaches Bear intact and is recorded as the source it claims to be: ``` lib.a : *.c $(CC) -c $(CFLAGS) *.c ``` ```shell $ make -Bnw cc -c -Wall *.c $ make -Bnw | bear parse-sh -o - [ { "file": "*.c", "arguments": [ "cc", "-c", "-Wall", "*.c" ], "directory": "/home/you/project" } ] ``` Have make expand the wildcard instead of the shell. `$(wildcard)` runs when make reads the Makefile, before any recipe is printed, so the dry run carries the file names: ``` SRCS = $(wildcard *.c) lib.a : $(SRCS) $(CC) -c $(CFLAGS) $(SRCS) ``` ```shell $ make -Bnw cc -c -Wall a.c b.c $ make -Bnw | bear parse-sh -o - | grep '"file"' "file": "a.c", "file": "b.c", ``` The two recipes build the same thing; only the expansion moves. When the Makefile is not yours to change, run the build under interception instead: the shell expands the glob before the compiler starts, so the `exec()` Bear observes already names both sources, and the same two entries come out. ## An up-to-date tree, or one that stops on an error Neither case is loud: everything Bear does get is valid text, so no line is skipped and the run exits 0, however little reached the database. A tree that is already built has no recipe left to print: ```shell $ make -nw make: Entering directory '/home/you/project' make: Nothing to be done for 'all'. make: Leaving directory '/home/you/project' ``` That middle line is a valid command that is not a compiler, so there is no skip to report. `-B` (`--always-make`) treats every target as out of date and prints every recipe. An error stops the dry run where it would stop a real build. The compiles make had already reached are printed, the rest never are, and `-k` (`--keep-going`) is what gets the unaffected targets printed anyway: ```shell $ make -nw make: Entering directory '/home/you/project' make: *** No rule to make target 'gone.c', needed by 'gone.o'. Stop. make: Leaving directory '/home/you/project' $ make -nwk make: Entering directory '/home/you/project' make: *** No rule to make target 'gone.c', needed by 'gone.o'. gcc -c -o a.o a.c gcc -c -o b.o b.c make: Leaving directory '/home/you/project' ``` ## Directory markers, and the locale they are printed in Sub-makes announce their directory on their own; the top-level make announces its own only under `-w`. Without it, every command printed before the first `Entering directory` line lands in whatever directory `parse-sh` itself was run in, which is the build root only by accident. The markers are matched in English. A make running under a translated locale announces its directories in words Bear does not match, so no directory change is tracked at all and every entry silently carries the starting directory. `LC_ALL=C` on the capture is what keeps both of these right: ```sh LC_ALL=C make -nw | bear parse-sh ``` ## Ninja prints progress, not commands `ninja -n` prints each edge's description rather than the command line behind it: ```shell $ ninja -n ninja: Entering directory `build' [1/3] Building C object CMakeFiles/demo.dir/src/a.c.o [2/3] Building C object CMakeFiles/demo.dir/src/b.c.o [3/3] Linking C static library libdemo.a ``` There is no command in that text, so every line is skipped and the run exits non-zero: ``` bear: warning: line 2: skipped (glob in executable) bear: warning: line 3: skipped (glob in executable) bear: warning: line 4: skipped (glob in executable) bear: warning: parse-sh: 0 command(s) parsed, 3 line(s) skipped bear: error: Event production failed: every non-empty line was skipped; no commands parsed (see warnings above) ``` `ninja -t commands` prints the real command lines. It prints no directory markers with them, so name the build directory with `--directory`: ```sh ninja -C build -t commands | bear parse-sh -C "$PWD/build" ``` Make needs no equivalent: `-n` prints a recipe line even when a leading `@` would have silenced it in a real build, so automake's silent rules and CMake's Makefile generator both put the full compiler command in the dry run without `V=1` or `VERBOSE=1`. ## A log that was captured without these flags Nothing recovers a command the text never contained, so a log missing whole subdirectories has to be captured again, or the build run under Bear instead. What is still fixable after the fact is the directory the parse starts in: `--directory` sets it, for a log that came from another machine or another checkout: ```sh bear parse-sh -i build.log -C /home/you/project ``` [Recover compile_commands.json from a build log](../../tutorials/compile-commands-from-a-build-log.md) walks a handed-over log end to end. ## Read the skip report A line using shell syntax outside the supported subset is skipped, and each skip is reported on standard error with its line number and reason, followed by a summary: ```shell $ make -nw | bear parse-sh bear: warning: line 3: skipped (shell keyword) bear: warning: line 4: skipped (subshell) bear: warning: parse-sh: 1 command(s) parsed, 2 line(s) skipped ``` Both lines there hid a compile: a `for` loop over sources, and a `(cd sub && gcc ...)` subshell. A skip costs an entry only when the skipped line was a compilation, and the run still exits 0 as long as one line parsed, so read the summary rather than the exit code. The line numbers are the input's, so capture to a file when you want to look them up: ```sh make -nw > build.log bear parse-sh -i build.log ``` `bear parse-sh` in the [`bear(1)` man page][manpage] lists the shell constructs the subset covers. ## When the text cannot be made complete Every limit above is the dry run's, not the parser's, and interception has none of them: it observes the `exec()` calls the build really makes, so recursion, shell loops, silenced recipes, and the locale all stop mattering. ```sh bear -- make ``` Keep `parse-sh` for the build that cannot be run again. ## Related pages - [Generate compile_commands.json for a Makefile project](compile-commands-for-makefile.md) for running the build under Bear instead. - [Recover compile_commands.json from a build log](../../tutorials/compile-commands-from-a-build-log.md) for the end-to-end walkthrough of parsing a saved log. - [Bear produces an empty compile_commands.json](empty-compilation-database.md) when the build did run under Bear and still produced nothing. - [Command-line options](../../reference/command-line.md#bear-parse-sh) for `--input`, `--output`, `--append`, and `--directory`. - [How Bear works](../../understanding/how-it-works.md) for what interception observes that text cannot carry. - [Recipes](index.md) for the other tasks. [manpage]: https://github.com/rizsotto/Bear/blob/master/man/bear.1.md �������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/guides/recipes/embedded-arm.md���������������������������������������0000664�0000000�0000000�00000010561�15271777203�0024113�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: how-to --> # Generate compile_commands.json for an STM32 or arm-none-eabi project Run the project's Makefile under Bear: ```sh bear -- make ``` Cross-prefixed GNU Arm Embedded GCC (`arm-none-eabi-gcc`, `arm-none-eabi-g++`) and Arm Compiler 6 (`armclang`, `armclang++`, the compiler behind Keil MDK) are the two toolchains Bear recognizes for a bare-metal Arm target; see [Supported compilers](../../reference/supported-compilers.md) for the full name table. `arm-none-eabi-gcc` is recognized through GCC's general cross-compilation prefix support, covered in [Generate compile_commands.json when cross-compiling](cross-compilation.md); `armclang` has its own family instead. ## Where STM32CubeIDE's Makefile actually is STM32CubeIDE does not build from a Makefile at the project root: it generates one per build configuration, under a subdirectory named for that configuration - `Debug/Makefile`, `Release/Makefile`, or a custom configuration's own name - and that generated file is what the IDE's "Build" button actually runs. Once a configuration has been built at least once from the IDE, run Bear against that Makefile directly: ```sh cd Debug bear -- make ``` If the IDE already finished building that configuration, `make` there has nothing left to do and Bear records nothing; force a full rebuild the same way [Generate compile_commands.json for a Makefile project](compile-commands-for-makefile.md#incremental-builds) recommends for any other up-to-date build tree, for example `bear -- make clean all`. A plain CubeMX-generated Makefile project (no IDE) writes its Makefile at the project root instead of a per-configuration subdirectory, but the same rule applies: run Bear against the Makefile that actually compiles your sources, from clean if it is already up to date. ## When preload cannot inject into the compiler Vendor Arm toolchains, including some STM32CubeIDE and Keil MDK installations, still ship a 32-bit compiler driver binary even when installed on a 64-bit host. Preload, the default interception method on Linux, is built for the host's own ELF class, so injecting it into a 32-bit process fails with a "wrong ELF class" error from the dynamic linker: the build still completes, but that invocation goes unintercepted and its source file is silently missing from `compile_commands.json`. [Generate compile_commands.json when cross-compiling](cross-compilation.md#when-preload-cannot-reach-the-compiler) covers this failure and its glibc-mismatch cousin in full, including the `intercept.mode: wrapper` fix, which does not care about the target binary's ELF class. ## What clangd needs for a bare-metal entry The entry Bear records names the cross compiler itself (`arm-none-eabi-gcc` or `armclang`) with the project's flags intact: `-mcpu`, `-mthumb`, `--specs`, `-I` for the vendor HAL and CMSIS headers, and any `-D` for the target MCU. Without [`--query-driver`](https://clangd.llvm.org/config#compileflags) pointed at that same cross compiler, clangd parses the entry with its own host Clang, and host Clang rejects flags that only GCC's embedded target support understands: `-mcpu=cortex-m4`, `-mthumb`, and `--specs=nano.specs` each produce an "unknown argument" diagnostic instead of the target and header search path they set on the real compiler. `--query-driver=/path/to/arm-none-eabi-gcc` fixes this by asking the real driver for its target and built-in include paths instead of guessing from host Clang's own, the same mechanism used for a wrapper's baked-in paths on [Generate compile_commands.json for an MPI project](mpi.md#what-actually-lands-in-the-entry). Without that option, strip the GCC-specific flags in `.clangd` instead so clangd stops rejecting the command line outright: ```yaml CompileFlags: Remove: [-mcpu=*, -mthumb, --specs=*] ``` See [Set up clangd for a project without CMake](clangd-setup.md) for pointing your editor at the database once it exists. ## Related pages - [Generate compile_commands.json when cross-compiling](cross-compilation.md) for cross-prefix recognition and the preload/wrapper trade-off in general. - [Use Bear with a vendor embedded toolchain](vendor-embedded.md) for QNX, Microchip XC8, and Texas Instruments' Arm toolchain. - [Set up clangd for a project without CMake](clangd-setup.md). - [Supported compilers](../../reference/supported-compilers.md) for the full recognized-name table. - [Recipes](index.md) for the other tasks. �����������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/guides/recipes/empty-compilation-database.md�������������������������0000664�0000000�0000000�00000025511�15271777203�0027022�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: how-to --> # Bear produces an empty compile_commands.json The most common cause is that the build compiled nothing: an incremental build with everything already up to date runs no compiler at all, and Bear only records commands the build actually executes. Rebuild from clean: ```sh make clean bear -- make ``` If a clean rebuild still leaves `[]`, work through the other causes below, most common first. Each one shows the command that confirms it and the command that fixes it. If the `[]` came out of `bear parse-sh` instead of a build run under Bear, go to [Capture a complete dry run for `bear parse-sh`](dry-run-capture.md), where the causes are the dry run's rather than interception's. ## The build had nothing to compile An incremental build that finds every target up to date runs no compiler, so there is nothing for Bear to intercept. This is not a bug: Bear observes execution, it does not read your build files to guess what "should" happen. **Confirm**: check whether the build itself said it did nothing, for example make's ``` make: Nothing to be done for 'all'. ``` **Fix**: clean first, or build into an empty directory, as shown above. When you only rebuilt part of a larger project and want to keep the rest of a previous run's entries instead of doing a full clean, use `--append` (see [Command-line options](../../reference/command-line.md) for its exact semantics, and [Troubleshooting](../troubleshooting.md) for the duplicate-handling rules that apply when you do). ## The compiler is not recognized Bear decides whether a command is a compilation by matching the executable's filename against a table of known names; a name outside that table produces no entry, even though the compiler ran and built your code successfully. **Confirm**: check whether your compiler's name is one Bear knows, on [Supported compilers](../../reference/supported-compilers.md), or ask the installed build directly: ```sh bear semantic --print-compilers ``` A custom or renamed executable will not appear here. The [Diagnostics](#diagnostics) section below shows a more precise way to confirm the exact command that went unrecognized. **Fix**: add a `compilers:` hint mapping the executable's path to the family it behaves like: ```yaml schema: "4.2" compilers: - path: /usr/local/bin/my-custom-cc as: gcc ``` A Makefile rule invoking `./my-totally-custom-cc`, itself a plain copy of `gcc`, produces `[]` on its own, and a working entry once the hint above names its path. The accepted `as:` values are listed on [Supported compilers](../../reference/supported-compilers.md#as-and-ignore-hints). ## Preload interception is blocked or unavailable Bear has two interception methods, chosen once per invocation: preload injects a library into every process the build starts; wrapper substitutes the known compilers on `PATH` instead. See [Configure Bear](../../reference/configuration.md#intercept) for which one is the default on your platform and how to force the other, and [How Bear works](../../understanding/how-it-works.md) for the mechanism. A few situations keep preload from seeing a compilation at all: **A statically linked build tool.** Preload works by hooking library calls inside the process that makes them; a statically linked executable never loads the dynamic linker, so no hook is ever installed in it, and any compiler it execs directly is invisible to Bear. This is a fundamental limit of the approach, not a bug. Confirm with: ```sh file "$(command -v your-build-tool)" ``` A report of "statically linked" (rather than "dynamically linked") means preload cannot see what that tool executes. A statically linked launcher that directly `exec`s a compiler produces `[]` under the default (preload) mode on Linux, and a correct entry once wrapper mode is forced: ```yaml schema: "4.2" intercept: mode: wrapper ``` **macOS with System Integrity Protection enabled.** SIP strips the preload environment variable from protected executables before it can take effect, so preload cannot run there; it is wrapper mode's reason for being the macOS default in the first place. See [Bear on macOS](../../platforms/macos.md) for the detail. Forcing preload mode where it is unavailable (SIP-protected macOS, or Windows, which has no preload library at all) is a startup error naming wrapper mode as the alternative - Bear does not silently fall back, and the build does not run, so this case shows up as an explicit error rather than an empty `compile_commands.json`. **Running against a container from the outside.** `bear -- docker exec ...` on the host does not work: the daemon runs the command inside the container's own process tree, which the host-side Bear process never observes, so the database comes out empty with no other symptom. Bear must run **inside** the container as part of the build. See [Run Bear inside a Docker container](docker.md) for the correct invocation. In every one of these cases, wrapper mode is the general fallback, with one condition of its own: the build has to actually pick the wrapper up, covered below. ## The preload library failed to load A missing or broken `libexec.so` install is not a failure the dynamic linker treats as fatal: it drops a preload target it cannot open, prints a warning on standard error, and lets the build run anyway. Only the top-level command Bear launched is observed directly; everything that command itself spawns runs unwatched, so a build whose compiler is invoked through `make` or a shell script comes back with an empty database, and Bear still exits `0` because the build succeeded. See [Exit status](../../reference/exit-status.md) for why success and an empty database are not mutually exclusive. **Confirm**: look for this line in the build's own standard error, not Bear's own log output: ``` ERROR: ld.so: object '.../libexec.so' from LD_PRELOAD cannot be preloaded: ignored. ``` **Fix**: see [Troubleshooting](../troubleshooting.md#ld_preload-errors) for the install-path check. ## Wrapper mode: the build never picks up the wrapper Wrapper mode works by putting reporting executables ahead of the real compilers on `PATH`. A build step that discovers the compiler on its own, before the wrapper directory is in place, bakes in the real compiler's path directly, and every later invocation goes straight to the real compiler with no wrapper in between: a `./configure` script run outside Bear, or a CMake configure step that resolves `cc` to an absolute path, are the common versions of this. **Confirm**: look for the real compiler's absolute path already baked into a generated build file (`config.mk`, `CMakeCache.txt`, or similar) from before the build ran under Bear. **Fix**: run the configure step under Bear too, discarding its output, then run the real build under Bear: ```sh bear --config bear.yml -- ./configure bear --config bear.yml -- make ``` A `configure.sh` that resolves `cc` to an absolute path and writes it into a makefile fragment produces `[]` when only the subsequent `make` runs under Bear in wrapper mode. Running `configure.sh` under Bear as well, with the same config and discarding that run's own output, makes the following build's database come out populated. This matches the two-step CMake example in the [`bear(1)` man page][manpage]'s EXAMPLES section. ## The build failed before it reached a compiler Bear records the compiler invocations that actually happen; if the build fails before starting any of them - a missing include in a Makefile, a dependency that does not exist, a configure step that aborts - there is nothing to record. Note the distinction: a compile that fails *after* the compiler was invoked (a syntax error in the source) still produces an entry, because Bear records the attempted command, not its outcome. An empty database from a failed build specifically means the failure happened before any compiler ran at all. **Confirm**: check the build's own error output and Bear's exit code (see [Exit status](../../reference/exit-status.md)); a non-zero exit with no compiler messages at all points here. **Fix**: fix whatever stops the build before it reaches a compile step, then rebuild under Bear. ## Source filtering removed everything A `sources:` rule in the configuration file that is broader than intended can exclude every compiled file, leaving a `[]` even though the build compiled normally. (A `duplicates:` rule cannot do this by itself: it always keeps the first occurrence of a group of duplicates, so it can shrink a database but never empty it out on its own; see [Troubleshooting](../troubleshooting.md#the-output-has-duplicate-entries) for that case.) **Confirm**: temporarily remove the `sources:` section from your configuration and rebuild; if entries appear, a rule there is the cause. **Fix**: narrow the rule. For example, excluding every `.c` file: ```yaml schema: "4.2" sources: files: - pattern: "*.c" action: exclude ``` produces `[]` for a project with only `.c` sources, by design. See [Configure Bear](../../reference/configuration.md#sources) for the full `sources:` syntax. ## Diagnostics Before anything else, re-run with logging turned on: ```sh RUST_LOG=info bear -- <build command> ``` Near the end of the output is a summary that tells you which side of the pipeline lost your entries: ``` Output pipeline: semantic events: 5 current entries: 2 previous entries: 0 filtered entries by duplicate: 0 filtered entries by source: 0 synthesized header entries: 0 dropped entries (invalid): 0 total entries written: 1 ``` - `semantic events` near the minimum (as low as 1, just the build command itself) means the build never executed a compiler - the "nothing to compile" or "build failed early" cases above. - `semantic events` clearly greater than zero but `current entries: 0` means commands ran but none were recognized as a compiler - the "compiler not recognized" case. - `filtered entries by source` or `filtered entries by duplicate` greater than zero, with `total entries written` at zero, means recognized entries were filtered out by your configuration. Set `RUST_LOG=debug` instead for more detail: every intercepted command is tagged `Recognized(...)` or `NotRecognized(...)`, which pins down exactly which invocation Bear saw and how it classified it. Without `RUST_LOG` set, Bear prints only warnings and errors; see the `bear(1)` man page's ENVIRONMENT section for the exact levels. Related: [Troubleshooting](../troubleshooting.md) for a database that is short or wrong rather than empty, [Supported compilers](../../reference/supported-compilers.md) for the recognized names, [How Bear works](../../understanding/how-it-works.md) for the interception and analysis mechanism, the [Recipes](index.md) index, and the platform notes for [Linux](../../platforms/linux.md), [macOS](../../platforms/macos.md), [Windows](../../platforms/windows.md), and [BSD](../../platforms/bsd.md). [manpage]: https://github.com/rizsotto/Bear/blob/master/man/bear.1.md ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/guides/recipes/emscripten.md�����������������������������������������0000664�0000000�0000000�00000006124�15271777203�0023756�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: how-to --> # Generate compile_commands.json for an Emscripten project Run the build under Bear, with Bear as the outermost command: ```sh bear -- emmake make ``` Bear captures every process the build tree starts, however many wrapper scripts sit in between, so it does not matter that `emmake` itself sets `CC`/`CXX` to `emcc`/`em++` before handing off to `make`: those exec calls happen inside the tree Bear is already watching. Bear has to be the outermost command for that to hold, so `bear -- emmake make` is the order to use. `emcc`, `em++`, and their `.py`-suffixed spellings are recognized under the `clang` family, not a separate `emscripten` id, since Emscripten's driver is a Clang wrapper whose command line follows Clang's flag syntax closely enough for Bear's Clang rules to parse it correctly. There is no `emscripten` `as` value to write in a `compilers:` override; use `clang` if a nonstandard `emcc` path needs a hint. See [Supported compilers](../../reference/supported-compilers.md) for the full recognized-name table. ## CMake projects: emcmake For a CMake-based Emscripten project, `emcmake` only needs to run once, to generate a build tree configured for `emcc`/`em++`; the actual build is what Bear needs to watch: ```sh emcmake cmake -B build bear -- cmake --build build ``` This is the same two-step shape as the general CMake case in the [`bear(1)` man page][manpage]'s EXAMPLES: the generation step does not compile your project's sources, so it does not need to run under Bear. In wrapper mode (the default on macOS and Windows, or forced with `intercept.mode: wrapper`), a step that discovers the compiler on its own has to run under Bear too, so run the generation step under Bear as well and discard its output, the same way [Generate compile_commands.json for a Makefile project](compile-commands-for-makefile.md#autotools-configure-and-make) handles `./configure`: ```sh bear -- emcmake cmake -B build bear -- cmake --build build ``` ## clangd and an `emcc` entry clangd reads the recorded compiler as `emcc`, a binary it does not run and does not know the way it knows a plain `clang`. When it cannot infer a driver's built-in include paths and default target from the recorded command alone, point it at the real driver directly with `--query-driver`, the same mechanism the [`bear(1)` man page][manpage] documents for MPI compiler wrappers: ```sh clangd --query-driver=/path/to/emcc ``` See [Set up clangd for a project without CMake](clangd-setup.md) for getting the database itself into a place clangd finds it. ## Related pages - [Supported compilers](../../reference/supported-compilers.md) for the full recognized-name table and the `compilers:` override for a path Bear gets wrong. - [Generate compile_commands.json for a Makefile project](compile-commands-for-makefile.md) for the Autotools-style two-step pattern `emcmake` needs. - [Set up clangd for a project without CMake](clangd-setup.md) for pointing your editor at the database once it exists. - [Recipes](index.md) for the other tasks. [manpage]: https://github.com/rizsotto/Bear/blob/master/man/bear.1.md ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/guides/recipes/headers.md��������������������������������������������0000664�0000000�0000000�00000012157�15271777203�0023223�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: how-to --> # clangd has no compile command for a header clangd already guesses a compile command for a header with no entry of its own, by borrowing the flags of a compiled source file nearby, and for many projects that guess is good enough as it stands. When it is not - the borrowed flags are wrong for this header, or there is no source nearby to borrow from - turn on header-entry synthesis in Bear's configuration file: ```yaml schema: "4.2" headers: enabled: true strategy: siblings ``` This makes Bear write a real entry for the header itself, cloned from a compiled source's arguments with the source path swapped in for the header's and the output flag removed, instead of leaving clangd to infer one. The full set of `headers:` keys and their defaults is on [Configure Bear](../../reference/configuration.md#headers). ## Telling the situations apart Run clangd against the header directly and read the log line it prints while loading the database: ```sh clangd --check=path/to/header.h ``` - `Compile command from CDB is: ...` - the database already has a real entry for this header. This is not the problem this page is about. - `Compile command inferred from some_source.c is: ...` - clangd found no entry but borrowed one from a neighboring source in the same directory. This is often fine as it stands; look at whether the borrowed flags actually apply to the header (same defines, same target) before changing anything. - `Generic fallback command is: ...` - clangd found no entry and no neighbor to borrow from at all. This is the case synthesis exists for. [Set up clangd for a project without CMake](clangd-setup.md) documents these same three log lines with the fuller context of a working database; this page only adds the header-specific reading of them. ## Synthesis is not always the difference between broken and working In a split `include/` + `src/` project (`include/greet.h` used by `src/greet.c`, which lives in a different directory), running with header synthesis off still leaves clangd able to work on `include/greet.h`: it infers a command from `src/greet.c` and logs `Compile command inferred from src/greet.c is: ... -x c-header ...`. Turning on the `dependency-files` strategy replaces that with a real entry, and the log line changes to `Compile command from CDB is: ...` - but the header was already usable before that change. Reach for synthesis when one of these is actually true, not by default: - the flags a neighboring source would donate are wrong for this header (a different target, different defines, a header shared across parts of the build that compile differently); - no compiled source is in reach to donate flags from at all (the `Generic fallback command` case above); - a consumer needs a real database entry rather than clangd's own inference - running `clang-tidy` directly against the header, or another tool that reads `compile_commands.json` without clangd's fallback logic. ## Choosing a strategy Two strategies are available, and they reach different headers: - **`siblings`** clones flags from a compiled source in the same directory as the header. It needs nothing from the build, but it reaches only headers that share a directory with a compiled source: in a split `include/` + `src/` layout it picks up a header living beside `.c` files in `src/`, but not one living alone in `include/`, because that directory has no compiled source to clone from. The flags it clones are approximate, borrowed from whichever compiled source happens to sit next to the header, not necessarily the one that actually includes it. - **`dependency-files`** reads the make-style `.d` files the build already left on disk (from `-MMD`, `-MD`, or an equivalent flag) and synthesizes an entry for every header named as a prerequisite, wherever it lives. This is how it reaches a header in `include/` from a `.d` file written while compiling a source in `src/`. It needs the build to have actually emitted those files: look for `*.d` files next to the object files after a build, or check the compiler flags for `-MMD`/`-MD`, before choosing this strategy. A build that never passes one of those flags leaves nothing for `dependency-files` to read, and synthesis silently reaches no headers. Start with `siblings` when the project's headers all sit next to the sources that use them; reach for `dependency-files` when they do not, or when `siblings`'s approximate flags are visibly wrong, and the build already produces `.d` files. A third strategy is conceivable: post-process an existing compilation database and determine, for each translation unit, which header files it actually used. That would be more accurate than cloning a neighbor's flags, and would not require the build to have emitted dependency files. Bear does not implement this strategy yet; external tools exist that do this kind of post-processing. Related: [Bear produces an empty compile_commands.json](empty-compilation-database.md) for a database missing more than just header entries, [Generate compile_commands.json for a Makefile project](compile-commands-for-makefile.md), and the [Recipes](index.md) index. �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/guides/recipes/index.md����������������������������������������������0000664�0000000�0000000�00000005257�15271777203�0022722�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: landing (navigation page, not one of the four types) --> # Recipes How to generate `compile_commands.json` for a specific build system, toolchain, or environment, and what to do when the result is wrong. - [Generate compile_commands.json for a Makefile project](compile-commands-for-makefile.md) - the main recipe: plain Make, autotools, incremental and parallel builds, recursive Makefiles, and recovering a database without running the build. - [Generate compile_commands.json for a CMake project](cmake.md) - when CMake's own export is enough, and when it is not. - [Capture a complete dry run for bear parse-sh](dry-run-capture.md) - get every compiler command into the text, for a build you cannot run under Bear. - [Bear produces an empty compile_commands.json](empty-compilation-database.md) - what to check when the file comes out empty or short. - [Set up clangd for a project without CMake](clangd-setup.md) - point your editor at the database once Bear has written it. - [clangd has no compile command for a header](headers.md) - when to let clangd infer one, and when to have Bear synthesize it. - [Use Bear with ccache, distcc, or icecc](ccache-distcc-icecc.md) - keep a compiler launcher in the build while Bear still records the real compiler invocation. - [Run Bear inside a Docker container](docker.md) - capture a build that runs inside a container. ## Toolchains - [Generate compile_commands.json when cross-compiling](cross-compilation.md) - a target-prefixed GCC or Clang. - [Intercept a 32-bit build on a 64-bit host](multilib.md) - a second preload library, for a build that runs 32-bit compilers or build tools. - [Generate compile_commands.json for an STM32 or arm-none-eabi project](embedded-arm.md) - the bare-metal Arm GCC toolchain. - [Use Bear with a vendor embedded toolchain](vendor-embedded.md) - QNX, Microchip XC8 / MPLAB X, and Texas Instruments. - [Generate compile_commands.json for an Emscripten project](emscripten.md) - compiling to WebAssembly with `emcc`/`em++`. - [Generate compile_commands.json for a CUDA project](cuda.md) - `nvcc` alongside a host compiler. - [Use Bear with Intel oneAPI compilers](intel-oneapi.md) - the oneAPI and Classic Intel C/C++/Fortran drivers. - [Use Bear with Cray and NVIDIA HPC compilers](cray-hpc.md) - HPE Cray's and NVIDIA's HPC SDK drivers. - [Generate compile_commands.json for an MPI project](mpi.md) - the `mpicc`-style wrappers. Related pages: [Getting started with Bear](../../tutorials/getting-started.md) for the first run, [Troubleshooting](../troubleshooting.md) for a database that came out wrong, and [How Bear works](../../understanding/how-it-works.md) for the mechanism. �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/guides/recipes/intel-oneapi.md���������������������������������������0000664�0000000�0000000�00000004646�15271777203�0024200�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: how-to --> # Use Bear with Intel oneAPI compilers Source the oneAPI environment script for the current shell, then run the build under Bear exactly as you would for any other compiler: ```sh source /opt/intel/oneapi/setvars.sh bear -- make ``` `setvars.sh` is what puts `icx`, `icpx`, `ifx`, and the rest of the oneAPI drivers on `PATH`. Bear itself does nothing toolchain-specific at that point: it watches whatever executable the build actually runs, the same as for GCC or Clang. Skipping the `source` step is the most common reason a build "under Bear" still records the system compiler: the shell running `make` never had the oneAPI drivers on `PATH` to begin with, so `bear -- make` and a plain `make` invoke exactly the same programs. ## Which family each driver is recognized as `icx`/`icpx` (current oneAPI) and `icc`/`icpc` (the older Classic drivers) share one family id, `intel_cc`, and one Intel-specific flag table (`-qopenmp`, `-ipo`, `-fp-model`, and the rest). The Fortran drivers, `ifort` and `ifx`, are a separate family, `intel_fortran`. See [Supported compilers](../../reference/supported-compilers.md) for the full recognized-name table; `bear semantic --print-compilers` prints the same mapping for the version you have installed. Intel's MPI wrappers (`mpiicc`, `mpiifx`, and the rest) reuse these same two families rather than a generic MPI wrapper id. See [Generate compile_commands.json for an MPI project](mpi.md) for that mapping and why the split exists. ## If a driver is not recognized A build that calls the compiler through a renamed copy or a nonstandard path (a custom install, a CI image that symlinks `icx` to something else) needs a `compilers:` hint: ```yaml schema: "4.2" compilers: - path: /opt/custom/bin/icx-wrapper as: intel_cc ``` Use `as: intel_fortran` for a renamed Fortran driver. See [`as` and `ignore` hints](../../reference/supported-compilers.md#as-and-ignore-hints) for the accepted values, and the `compilers` section of [Configure Bear](../../reference/configuration.md) for the key itself. ## Related pages - [Generate compile_commands.json for an MPI project](mpi.md) for the Intel MPI wrappers specifically. - [Supported compilers](../../reference/supported-compilers.md) for the complete recognized-name table and the ambiguous-name rules. - [Configure Bear](../../reference/configuration.md) for the `compilers:` section. - [Recipes](index.md) for the other tasks. ������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/guides/recipes/mpi.md������������������������������������������������0000664�0000000�0000000�00000006727�15271777203�0022403�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: how-to --> # Generate compile_commands.json for an MPI project Run the build under Bear exactly as you would without MPI: ```sh bear -- make ``` A build that compiles through `mpicc`, `mpicxx`, `mpifort`, or one of the Intel MPI wrappers needs no special configuration: Bear recognizes all of them and records the wrapper invocation itself. See [MPI compiler wrappers](../../reference/supported-compilers.md#mpi-compiler-wrappers) and [Supported compilers](../../reference/supported-compilers.md) for the full name table and the wrapper-is-recorded-as-invoked contract in general. This page covers what that contract means in practice: what actually lands in the recorded entry, and the one place the database can differ from what a reader expects. ## Open MPI/MPICH vs. Intel Open MPI's and MPICH's wrappers share one family id, `mpi`, and parse with the GCC flag table. Intel's wrappers (`mpiicc`, `mpiifort`, and the rest) are recognized too, but as extra names on the `intel_cc` and `intel_fortran` families instead of the generic `mpi` id: `mpiicc` is `icc`/`icx` with MPI's paths baked in, and Intel's flag table has arity rules (for example `-debug` takes a following argument) that the GCC table would mis-parse. See [Use Bear with Intel oneAPI compilers](intel-oneapi.md) for those two families in general. ## What actually lands in the entry Given ```sh mpicc -c main.c -o main.o ``` where `mpicc` is a script that execs `gcc` with `-I/opt/mpi/include -L/opt/mpi/lib` prepended, the entry Bear writes is ```json [ { "file": "main.c", "arguments": ["mpicc", "-c", "main.c", "-o", "main.o"], "directory": "/path/to/project", "output": "main.o" } ] ``` `arguments` holds the command exactly as the build wrote it: no `gcc`, and none of the wrapper's baked-in `-I`/`-L` flags, since Bear does not run the wrapper to learn them. Clang tooling that needs those flags to resolve MPI headers should point at the wrapper directly; clangd does this with `--query-driver`. See [Set up clangd for a project without CMake](clangd-setup.md) for pointing an editor at the database once it exists. ## The compiler the wrapper execs is usually not a second entry In preload mode Bear does intercept the compiler the wrapper execs internally, not just the wrapper itself, but that rarely shows up as a second entry in `compile_commands.json`: the default duplicate filter (`directory` and `file`) treats the two invocations as the same compilation and keeps whichever comes first, the wrapper's. That lower-level invocation surfaces as its own entry only if you widen `duplicates.match_on` to include `arguments` (see [Configure Bear](../../reference/configuration.md#duplicates)); ordinarily that is not useful, since it names an intermediate compiler call rather than the one your build system actually asked for. MPICH's compiler-override options, `-cc=`, `-cxx=`, and `-fc=`, are also accepted without the `=` (`-cc gcc`); either form is recognized as a driver option that takes a value, so it never gets misread as compiling a source file named `gcc`. ## Related pages - [Use Bear with Intel oneAPI compilers](intel-oneapi.md) for the `intel_cc`/`intel_fortran` families the Intel MPI wrappers reuse. - [Use Bear with ccache, distcc, or icecc](ccache-distcc-icecc.md) for the launcher case, where Bear records the wrapped compiler instead. - [Supported compilers](../../reference/supported-compilers.md) for the full recognized-name table. - [Recipes](index.md) for the other tasks. �����������������������������������������rizsotto-Bear-0c30b27/site/src/guides/recipes/multilib.md�������������������������������������������0000664�0000000�0000000�00000015734�15271777203�0023435�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: how-to --> # Intercept a 32-bit build on a 64-bit host A build that runs 32-bit compilers or 32-bit build tools on a 64-bit host records nothing from them: the build still exits `0`, no error appears, and `compile_commands.json` just comes out short or empty. The preload library a normal Bear install ships is built for the host's 64-bit ELF class, and the dynamic linker cannot inject a 64-bit shared library into a 32-bit process (see [How Bear works](../../understanding/how-it-works.md#two-ways-to-capture-a-real-build) for what preload does and why the ELF class has to match). [Exit status](../../reference/exit-status.md#notes) covers why a missing preload library never turns into a non-zero exit: the dynamic linker treats it as something to warn about and skip, not to fail on, so nothing about the run tells you a source file went unrecorded. On glibc Linux this is fixable: build a second, 32-bit preload library, install it alongside the normal 64-bit one, and point `bear-driver` at both with a single `INTERCEPT_LIBDIR` setting. `INTERCEPT_LIBDIR` names the directory `bear-driver` looks in for the preload library, relative to its own `bin/`, and on glibc it accepts the literal string `$LIB` as that name. The dynamic linker itself expands `$LIB` at load time, per process: `lib64` when loading into a 64-bit process, `lib` when loading into a 32-bit one. One install then serves both kinds of process, provided both libraries exist on disk. ## Get a 32-bit toolchain and Rust target ```sh # Fedora sudo dnf install glibc-devel.i686 libgcc.i686 libatomic.i686 # Debian / Ubuntu sudo apt install gcc-multilib rustup target add i686-unknown-linux-gnu ``` For any other distribution, install whatever it calls its 32-bit development packages; the two lines above are only confirmed for Fedora. ## Work around the missing cross driver The preload library's build script compiles a small C shim with `cc-rs`, which looks for a `i686-linux-gnu-gcc` cross driver. Fedora (and any distribution that provides multilib through `gcc -m32` rather than a separate cross-driver binary) has no such executable, so the build fails with: ``` error occurred in cc-rs: failed to find tool "i686-linux-gnu-gcc" ``` There is no supported fix for this yet; the workaround is a two-line shim on `PATH` that execs `gcc -m32`, pointed to by `CARGO_TARGET_I686_UNKNOWN_LINUX_GNU_LINKER`: ```sh cat >/tmp/i686-linux-gnu-gcc <<'EOF' #!/bin/sh exec gcc -m32 "$@" EOF chmod +x /tmp/i686-linux-gnu-gcc export PATH="/tmp:${PATH}" export CARGO_TARGET_I686_UNKNOWN_LINUX_GNU_LINKER=/tmp/i686-linux-gnu-gcc ``` ## Build the 32-bit preload library ```sh INTERCEPT_LIBDIR='$LIB' cargo build -p intercept-preload \ --target i686-unknown-linux-gnu ``` This produces `target/i686-unknown-linux-gnu/debug/libexec.so`, a 32-bit `libexec.so` distinct from the normal 64-bit one. ## Build and install the 64-bit Bear, then place both libraries Build and install the regular 64-bit Bear with the same `$LIB` setting: ```sh INTERCEPT_LIBDIR='$LIB' cargo build SRCDIR=target/debug PREFIX=<prefix> INTERCEPT_LIBDIR='$LIB' ./scripts/install.sh ``` (See [Install Bear](../../installation.md) for what `PREFIX`, `DESTDIR`, and `INTERCEPT_LIBDIR` each control; the `$LIB` value here is what makes this recipe work and is on top of what that page documents.) `install.sh` does not understand `$LIB` as a dynamic-linker token: it creates a directory literally named `$LIB` under `<prefix>/libexec/bear/` and installs the 64-bit library there. Finishing the layout is a manual step, and there is currently no supported way to have the build or install scripts do it for you: ```sh cd <prefix>/libexec/bear mkdir -p lib lib64 mv 'target/i686-unknown-linux-gnu/debug/libexec.so' lib/libexec.so mv '$LIB/libexec.so' lib64/libexec.so rmdir '$LIB' ``` The result differs from the ordinary layout in exactly one place: the single `$INTERCEPT_LIBDIR` directory becomes two, one per ELF class, and no directory named `$LIB` is left behind. ``` $PREFIX/ |-- bin/ | `-- bear (shell script) |-- libexec/ | `-- bear/ | |-- bin/ | | |-- bear-driver | | `-- bear-wrapper | |-- lib/ | | `-- libexec.so (ELF 32-bit) | `-- lib64/ | `-- libexec.so (ELF 64-bit) `-- share/ (unchanged) ``` Check the two libraries really are different ELF classes, and that the 32-bit one is the one under `lib`: ```shell $ file <prefix>/libexec/bear/lib/libexec.so <prefix>/libexec/bear/lib64/libexec.so libexec/bear/lib/libexec.so: ELF 32-bit LSB shared object libexec/bear/lib64/libexec.so: ELF 64-bit LSB shared object ``` Swapping them produces the same silent, short database as having no 32-bit library at all: a 32-bit process looks only in `lib`, finds an object of the wrong class, and skips it. That output is worth including in any report about a multilib install. The rest of the tree, and what `PREFIX` and `DESTDIR` mean, is in [Install Bear](../../installation.md). ## Confirm it worked Run the build under `bear -- <build>` as usual. If it drives any 32-bit compiler or a 32-bit build tool that execs a compiler on its own behalf, compare the entry count in `compile_commands.json` with and without `lib/libexec.so` in place: removing the 32-bit library drops exactly the entries that 32-bit process would have contributed, while the build itself still exits `0`. With the library missing, the dynamic linker also prints the warning covered in [Troubleshooting: LD_PRELOAD errors](../troubleshooting.md#ld_preload-errors) - `cannot be preloaded (cannot open shared object file): ignored.` - which is the tell that this is the missing-32-bit-library case rather than some other silent gap. ## Limitations - This works only on glibc Linux. musl, macOS, and the BSDs have no `$LIB`-style token: they need a concrete library directory name instead, so one install cannot serve two ELF classes the same way. - The `cc-rs` shim above is a workaround for a missing cross driver, not a fix; a distribution that ships `i686-linux-gnu-gcc` directly does not need it. - `scripts/install.sh` has no option to produce the `lib` / `lib64` split itself. Repeat the manual step after every reinstall. This is a different problem from a vendor cross-toolchain whose compiler driver is itself a 32-bit binary; that case, and wrapper mode as its fix, is covered in [Generate compile_commands.json when cross-compiling](cross-compilation.md#when-preload-cannot-reach-the-compiler). ## Related pages - [Generate compile_commands.json when cross-compiling](cross-compilation.md) for the general cross-prefix and wrong-ELF-class story. - [Install Bear](../../installation.md) for `PREFIX`, `DESTDIR`, and `INTERCEPT_LIBDIR`. - [Exit status](../../reference/exit-status.md) for why Bear exits `0` even when a preload library never loaded. - [Troubleshooting](../troubleshooting.md) for `LD_PRELOAD` errors and other silent interception failures. - [Recipes](index.md) for the other tasks. ������������������������������������rizsotto-Bear-0c30b27/site/src/guides/recipes/vendor-embedded.md������������������������������������0000664�0000000�0000000�00000011130�15271777203�0024622�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: how-to --> # Use Bear with a vendor embedded toolchain Run the project's Makefile under Bear exactly as you already build it: ```sh bear -- make ``` This covers three vendor toolchains: QNX Momentics, Microchip's MPLAB X (XC8), and Texas Instruments' compilers, both the Clang-based Arm driver and the classic Code Generation Tools. Each builds through a Makefile (MPLAB X and Code Composer Studio generate one), and Bear needs nothing toolchain-specific once that Makefile runs under it. What differs between them is which executable name gets recognized as what; the full name table, `as:` ids, and prefix/suffix rules for every family are on [Supported compilers](../../reference/supported-compilers.md) and are not repeated here. ## QNX Bear recognizes `qcc` and `q++`, QNX Neutrino's compiler drivers, under the `qnx` family, parsed with GCC's flag rules (QNX 8 ships a GCC 12.2-based toolchain). This is a fixed pair of names: unlike GCC or Clang, `qcc`/`q++` are not recognized under a cross-compilation prefix or a version suffix. A QNX SDP installation also carries a set of `ntoARCH`-prefixed GCC binaries underneath the driver (`ntoaarch64-gcc`, `ntox86_64-gcc`, and similar, one per target architecture). These are recognized too, but as `gcc`, not `qnx`: GCC's cross-compilation prefix pattern matches any name ending in `-gcc`, and QNX's target names happen to fit that shape. A Makefile that calls one of these directly, rather than through `qcc`, still gets an entry, parsed with GCC's own flag rules instead of QNX's. QNX's `-V` flag picks the target/compiler variant on the command line (`-Vgcc_ntoaarch64le`); the `qnx` family models it as a single token that is never split and never swallows a following source file, matching both the attached form and the bare `-V` (which lists available variants). ## Microchip XC8 / MPLAB X Bear recognizes both `xc8-cc` (the GCC-styled driver name used from XC8 v2.30 onward) and the legacy `xc8` name, and records both under the `gcc` family, not an XC8-specific id: XC8's command-line syntax is GCC-styled, so Bear parses it with GCC's own flag rules. XC16 and XC32 need no entry of their own: their driver names, `xc16-gcc` and `xc32-gcc`, already match the general `<prefix>-gcc` cross-compilation pattern GCC is recognized under (see [Generate compile_commands.json when cross-compiling](cross-compilation.md)). XC8 needed its own table row because `xc8-cc` and `xc8` do not follow that prefix pattern. MPLAB X generates a Makefile for every build configuration and uses it as the project's real build entry point, the same way its own "Build" button does internally. Run `bear -- make` from the project directory that holds that generated Makefile. ## Texas Instruments Bear recognizes `tiarmclang`, TI's Clang-based compiler for Arm targets, under the `clang` family (not a TI-specific id, and not `armclang`, which names Arm Ltd.'s own Compiler 6 instead). TI's earlier, non-Clang Code Generation Tools are recognized under the `ti_cgt` family: one driver per target, `armcl`, `cl6x`, `cl7x`, `cl2000`, `cl430`, and `clpru`, all sharing one option dialect that is not GCC's. Bear parses them with TI's own flag rules, so an include path spelled `--include_path=bsp/include` is classified as one, and the `output` field comes from `--output_file`, TI's spelling of `-o`. Code Composer Studio generates a makefile per build configuration under the configuration's own directory (`Debug`, `Release`), and drives the build through it. Run `bear -- make` from that directory. The command lines it generates carry a dependency file and an object directory (`--preproc_dependency=`, `--obj_directory=`) alongside the source, and still compile, because CCS pairs them with `--preproc_with_compile`. One TI invocation deliberately yields no entry: the link step. CCS links through the same driver with `-z`, which hands the rest of the command line to the linker, so the object list and the linker command file that follow are not read as translation units. ## Related pages - [Generate compile_commands.json when cross-compiling](cross-compilation.md) for cross-compilation prefix and version-suffix recognition in general. - [Generate compile_commands.json for an STM32 or arm-none-eabi project](embedded-arm.md) for the Arm bare-metal GCC / Arm Compiler 6 case. - [Supported compilers](../../reference/supported-compilers.md) for the full recognized-name table and the `compilers:` override. - [Generate compile_commands.json for a Makefile project](compile-commands-for-makefile.md) for the general Make workflow these toolchains build on. - [Recipes](index.md) for the other tasks. ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/guides/troubleshooting.md��������������������������������������������0000664�0000000�0000000�00000024777�15271777203�0023420�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: how-to --> # Troubleshooting What to do when Bear runs but the result is not what you expected: an empty or short `compile_commands.json`, entries with the wrong flags, or a build that behaves differently under Bear than without it. If the database is completely empty, start with [Bear produces an empty `compile_commands.json`](recipes/empty-compilation-database.md), which works through the causes one by one; this page collects the rest. ## Enable debug logging Before investigating anything else, re-run the build with debug logging on standard error: ```sh RUST_LOG=debug bear -- <build command> ``` Without `RUST_LOG` set, Bear prints only warnings and errors. Setting it to `debug` (or `info`/`warn`/`error` for less detail) switches every helper process (`bear-driver`, `bear-wrapper`, the preload library) to a verbose, timestamped format detailed enough to see which executables were intercepted, how each was classified, and why an entry was or was not written. Include this output when reporting a problem. ## The output is missing entries If the database was built from dry-run text by `bear parse-sh` rather than from a build run under Bear, the entries are missing from the text itself: go to [Capture a complete dry run for `bear parse-sh`](recipes/dry-run-capture.md) instead. Same causes as an empty database, but partial - a compiler ran and was recognized, yet its entry did not survive to the file. The most common cause is `--append` (see [Command-line options](../reference/command-line.md#global-usage)): each Bear run overwrites the output by default, so building incrementally without it discards the previous run's entries. Accumulate results across runs instead: ```sh bear --append -- make module_a bear --append -- make module_b ``` New entries are placed before the existing ones, so a later rebuild's entry for a given file takes precedence over the stale one (see the `duplicates` section below). ### A file you know is compiled twice shows up once If a project compiles the same source file more than once - once for a static library and once for a shared one, for example - and the database has only one entry for it, this is not `--append`: it is the default duplicate match dropping the second invocation. Building zlib 1.3.1 shows the effect directly. `./configure && bear -- make` produces 17 entries, one per source file, even though zlib compiles every one of its 17 sources twice in the same directory: once for the static build, and once with `-fPIC -DPIC` for the shared one. The two invocations recorded for `adler32.c`: ``` gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -c -o adler32.o adler32.c gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -DPIC -c -o objs/adler32.o adler32.c ``` Bear's default `duplicates.match_on` compares only `directory` and `file` (see [Configure Bear](../reference/configuration.md#duplicates)), and both invocations share both, so only the first is kept. Add `arguments` to the default match, keeping `directory` and `file` in it: ```yaml schema: "4.2" duplicates: match_on: - directory - file - arguments ``` Rebuilding zlib with this configuration produces all 34 entries, two per source file. Keep `directory` in the list: a project with a `util.c` in two subdirectories, compiled with the same flags in each, would otherwise have one of them treated as a duplicate of the other and dropped. This is a trade-off, not a strict improvement: one entry per file per directory is what most Clang tooling wants, and matching on `arguments` too gives you every invocation at the cost of a larger database in which clangd (and most other consumers) still picks only one entry per file. ## The database from a dry run is empty or missing directories `bear parse-sh` records exactly what the dry-run text contains, so an empty or short database means the build system printed less than you expected. Capture the dry run again with the flags that make it print everything: ```sh LC_ALL=C make -Bnwk | bear parse-sh ``` [Capture a complete dry run for `bear parse-sh`](recipes/dry-run-capture.md) works through what each of those answers, the cases no flag fixes (a sub-make that never descends, a ninja dry run that prints progress instead of commands), and how to read the skip report Bear writes on standard error. ## The output has duplicate entries Two entries are duplicates only when every field listed in `duplicates.match_on` matches; of a set of duplicates, Bear keeps the first one in the file - the build's first invocation within a single run, or (per the ordering described above) the newest one across `--append` runs. The default match is `directory` and `file`, so one entry survives per source file per directory whatever its arguments were. If that is dropping invocations you wanted to keep, see [A file you know is compiled twice shows up once](#a-file-you-know-is-compiled-twice-shows-up-once) above for the fix and its trade-off. [Configure Bear](../reference/configuration.md#duplicates) enumerates the fields `match_on` accepts. ## The output has extra entries There are two different causes, and they need different fixes. `--append` across runs carries forward entries from earlier invocations, including ones for files that no longer exist. Drop `--append` and rebuild from clean to get a database with only the current build's entries: ```sh make clean bear -- make -j4 ``` Without `--append` each Bear run overwrites the previous output, so `bear -- ./configure` followed by `bear -- make` leaves nothing behind from the configure run: the build run replaces the file wholesale. A configure step inside a *single* Bear run is the other cause, and `--append` has nothing to do with it. `bear -- sh -c './configure && make'`, or a Makefile that re-runs `configure` itself, puts the configure phase's throwaway compiles (`conftest.c` and friends) in the same output as the real build. Split the two, so the build run's output is the one that survives: ```sh bear -- ./configure make clean bear -- make -j4 ``` If the configure step has to run under Bear for the build to be intercepted at all - wrapper mode, where the configure step must discover the wrapper as its compiler - keep the two runs separate as above rather than chaining them with `--append`, or exclude the probe files with a `sources` rule. ## The build behaves differently with Bear **Usually harmless**: extra lines in the build's own output, such as a loader warning about `libexec.so`, or additional environment variables Bear sets for its own bookkeeping. These do not change what gets compiled. **Worth reporting as a bug** if the build's actual result changes: a different binary, a step that fails only under Bear, or output written to an unexpected place. Two places to check first, since they are the things Bear adds to the build environment: - the `.bear/` wrapper directory in wrapper mode, in case the build system has its own use for a directory of that name in the working directory; - environment variables Bear sets for interception (`LD_PRELOAD`/`DYLD_INSERT_LIBRARIES` in preload mode, a modified `PATH` in wrapper mode), in case the build inspects or forwards its environment in a way that is sensitive to them. Report these with `RUST_LOG=debug` output attached. ## LD_PRELOAD errors ``` ERROR: ld.so: object '.../libexec.so' from LD_PRELOAD cannot be preloaded: ignored. ``` This means the dynamic linker could not find or load `libexec.so` at the path `bear-driver` expects, relative to its own location (`../$INTERCEPT_LIBDIR/libexec.so`, `lib` by default). Check that Bear was built and installed with the same `INTERCEPT_LIBDIR` value; see [Bear on Linux](../platforms/linux.md) for the build/install commands. The warning does not stop the build or fail Bear: see [Bear produces an empty compile_commands.json](recipes/empty-compilation-database.md#the-preload-library-failed-to-load) for why this is a silent cause of a database missing exactly the entries a subprocess would have reported. ### glibc version errors in cross-compilation ``` .../libexec.so: version `GLIBC_2.33' not found (required by .../libexec.so) ``` This is a different failure from the one above: the linker finds `libexec.so`, but the library needs a newer glibc symbol version than the C library available to the process it was injected into. It shows up when the build runs compilers from a cross-compilation SDK whose sysroot ships an older glibc than the host Bear was built on - the preload library must be ABI-compatible with the libc the intercepted compiler process loads from the SDK sysroot, not only with the host's. The intercepted invocation fails outright, so that command is silently missing from the database rather than reported as a warning. Compare the highest glibc version Bear's preload library requires against the highest the SDK's libc provides: ```sh # Highest glibc version Bear's preload library requires strings <prefix>/libexec/bear/lib/libexec.so | grep -oE 'GLIBC_[0-9.]+' | sort -V | tail -1 # Highest glibc version the SDK toolchain's libc provides strings /path/to/sdk/sysroot/lib/libc.so.6 | grep -oE 'GLIBC_[0-9.]+' | sort -V | tail -1 ``` `objdump` reports the same requirement more precisely, if it is available: ```sh objdump -T <prefix>/libexec/bear/lib/libexec.so | grep -oE 'GLIBC_[0-9.]+' | sort -uV | tail ``` If the version Bear's library requires is newer than what the SDK's libc provides, build (or obtain) a Bear linked against a glibc no newer than the SDK's, and use that build for the cross-compilation. Building on a host whose own glibc is no newer than the SDK's avoids the mismatch entirely. ## Getting help 1. Check `bear --version` against the [latest release](https://github.com/rizsotto/Bear/releases/latest); distribution packages are often old, see [Install Bear](../installation.md). 2. Run with `RUST_LOG=debug` and read the output. 3. Search [existing issues](https://github.com/rizsotto/Bear/issues?q=is%3Aissue). 4. Check [How Bear works](../understanding/how-it-works.md) if the behaviour itself is the surprise. 5. Open a new issue with the debug log and your platform (OS, Bear version from `bear --version`, and build system). Related: [Command-line options](../reference/command-line.md) and [Configure Bear](../reference/configuration.md) for the flags and keys named above, [Bear produces an empty compile_commands.json](recipes/empty-compilation-database.md) for that case, the [Recipes](recipes/index.md) index for task-shaped pages, and the platform notes for [Linux](../platforms/linux.md), [macOS](../platforms/macos.md), [Windows](../platforms/windows.md), and [BSD](../platforms/bsd.md). �rizsotto-Bear-0c30b27/site/src/index.md�������������������������������������������������������������0000664�0000000�0000000�00000004522�15271777203�0020002�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: landing (navigation page, not one of the four types) --> # Generate compile_commands.json for any C or C++ build Bear gives you working code navigation, autocomplete, and diagnostics on any C or C++ project, whatever the build system. Prefix your build command with `bear --` and Bear produces the `compile_commands.json` file that Clang tooling (clangd, clang-tidy, and friends) needs to understand your code: ```sh bear -- make ``` Bear captures the compiler invocations while your build runs and writes the [JSON compilation database][jsoncdb] for you. No changes to your build system are required. [jsoncdb]: https://clang.llvm.org/docs/JSONCompilationDatabase.html ## When to use Bear Bear is the tool of choice when the build system cannot produce a compilation database for you: - Make, autotools, or custom script builds. - A build you cannot or do not want to modify: a third-party project, a codebase you are just exploring, a CI job. - Unusual toolchains: embedded, HPC, CUDA, cross-compilers, and compiler launchers such as ccache, distcc, and icecc. - A build you cannot run at all: Bear can read `make -n` dry-run output or a saved build log instead of running the build. If your project uses CMake or Meson, those tools can export a compilation database directly, and Bazel has it through Hedron's [bazel-compile-commands-extractor][hedron]. Use that when it is available; [Generate compile_commands.json for a CMake project](guides/recipes/cmake.md) covers those exports and the cases where they are not enough. [hedron]: https://github.com/hedronvision/bazel-compile-commands-extractor ## Where to go next - [Getting started with Bear](tutorials/getting-started.md) - the first successful run, start to finish. - [Install Bear](installation.md) - packages and building from source. - [Recipes](guides/recipes/index.md) - one page per task. - [Troubleshooting](guides/troubleshooting.md) - when the database is empty or incomplete. - [How Bear works](understanding/how-it-works.md) - the mechanism behind the results. Looking something up rather than reading? [Command-line options](reference/command-line.md) and [Configure Bear](reference/configuration.md) are generated from Bear itself, so they match the version you have; [Supported compilers](reference/supported-compilers.md) lists every executable Bear recognizes. ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/installation.md������������������������������������������������������0000664�0000000�0000000�00000011126�15271777203�0021372�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: how-to --> # Install Bear Install Bear with your platform's package manager: ```sh # Debian / Ubuntu sudo apt install bear # Fedora sudo dnf install bear # Arch Linux sudo pacman -S bear # macOS (Homebrew) brew install bear # FreeBSD pkg install bear ``` Then confirm it is on your `PATH`: ```sh bear --version ``` Compare that version against the [latest release][latest-release]. Distribution packages often lag several releases behind, so a problem you hit may already be fixed upstream; build the current release from source before reporting a bug. `bear --help`, or [Command-line options](reference/command-line.md), lists every flag once you have it installed. For the full list of distributions that carry a Bear package, see the [Repology page][repology]. Repology tracks the version each distribution ships; if yours lags behind, or does not package Bear at all, build it from source instead. Bear is not published on crates.io, so `cargo install bear` will not find it; `cargo build --release` plus the install script below is the source-build path. ## Build from source Follow [`INSTALL.md`][install] in the repository for prerequisites (the Rust toolchain and a C compiler) and the full steps. The short version: ```sh cargo build --release ./scripts/install.sh ``` `scripts/install.sh` installs to `/usr/local` when run as root (for example under `sudo`), or `$HOME/.local` otherwise - a plain, unelevated run already gives you a working, no-`sudo` install. Override either default explicitly with `PREFIX`: ```sh sudo PREFIX=/usr ./scripts/install.sh ``` ## `DESTDIR` for packaging, separately from `PREFIX` `scripts/install.sh` accepts two different path variables, and they are not interchangeable. `PREFIX` is the final, on-system path; the `bear` entry script it installs embeds the driver path under it literally, so `PREFIX` must match where the package actually runs, not where it is being built. `DESTDIR`, when set, is a staging root prepended to every install path on top of `PREFIX`, for building a package without touching the real system; it must be an absolute path. Building a distribution package sets both together: ```sh DESTDIR=/tmp/pkgroot PREFIX=/usr ./scripts/install.sh ``` ## `LIBEXECDIR` for distributions without `libexec` By default the helper binaries and the preload library go into `$PREFIX/libexec/bear/`. Distributions without a `libexec` directory set `LIBEXECDIR` to the directory that should hold `bear/` instead. The entry script then embeds the driver path under `LIBEXECDIR` rather than `PREFIX`, so it is an on-system path too, and `DESTDIR` still goes on top: ```sh DESTDIR=/tmp/pkgroot PREFIX=/usr LIBEXECDIR=/usr/lib ./scripts/install.sh ``` It needs no matching build-time setting: `bear-driver` finds its siblings relative to its own location. ## `INTERCEPT_LIBDIR` must match between build and install On systems that use `lib64` or another non-default library directory name, set `INTERCEPT_LIBDIR` for both the build and the install step, so `bear-driver` finds the preload library where it actually ends up: ```sh INTERCEPT_LIBDIR=lib64 cargo build --release INTERCEPT_LIBDIR=lib64 ./scripts/install.sh ``` Let the two commands disagree and Bear still installs without complaint, but preload interception silently fails at runtime: the dynamic linker cannot find the library at the path `bear-driver` expects, so it skips loading it and lets the build run anyway. Nothing about that failure is loud - the build succeeds, Bear exits `0`, and the only symptom is a `compile_commands.json` that came out empty or suspiciously short, because a missing preload library is not one of the failures Bear itself reports (see [Exit status](reference/exit-status.md#notes)). [Bear on Linux](platforms/linux.md#installing-the-preload-library-to-a-non-standard-lib-directory) has the matching commands for the common `lib64` case. ## Platform-specific notes A distribution package or a source build both give you a working `bear`, but a few platforms have their own constraints on how Bear intercepts a build: - [Bear on Linux, WSL2, and Docker](platforms/linux.md) - [Bear on macOS](platforms/macos.md) - [Bear on Windows](platforms/windows.md) - [Bear on BSD](platforms/bsd.md) ## Next steps Once `bear --version` works, follow [Getting started with Bear](tutorials/getting-started.md) for the first real run, or jump straight to a [recipe](guides/recipes/index.md) for your build system. [repology]: https://repology.org/project/bear-clang/versions [install]: https://github.com/rizsotto/Bear/blob/master/INSTALL.md [latest-release]: https://github.com/rizsotto/Bear/releases/latest ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/platforms/�����������������������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0020355�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/platforms/bsd.md�����������������������������������������������������0000664�0000000�0000000�00000002417�15271777203�0021453�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: how-to --> # Bear on BSD Bear runs on FreeBSD, OpenBSD, NetBSD, and DragonFly BSD with the same defaults as Linux: preload is the default interception method (see [how Bear works](../understanding/how-it-works.md) and [Configure Bear](../reference/configuration.md#intercept)), and it cannot see into a statically linked build tool. What differs is packaging and the default compiler and linker names. ## `cc` and `c++` mean Clang here On FreeBSD, OpenBSD, NetBSD, and DragonFly BSD (and on macOS), the base system's `cc`/`c++` is Clang, not GCC as on most Linux distributions. Bear resolves this by probing `--version`; no configuration is needed for the common case. See [Supported compilers](../reference/supported-compilers.md#ambiguous-names) to override when the probe cannot classify a compiler. ## Installing FreeBSD packages Bear (`pkg install bear`). On OpenBSD, NetBSD, and DragonFly BSD, build from source per [`INSTALL.md`](https://github.com/rizsotto/Bear/blob/master/INSTALL.md); check your ports/pkgsrc tree first. Related: [how Bear works](../understanding/how-it-works.md) for the preload mechanism, [Troubleshooting](../guides/troubleshooting.md) for output that comes out wrong, and the [Recipes](../guides/recipes/index.md) index for other tasks. �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/platforms/linux.md���������������������������������������������������0000664�0000000�0000000�00000002530�15271777203�0022036�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: how-to --> # Bear on Linux, WSL2, and Docker Preload is the default interception method on Linux (see [how Bear works](../understanding/how-it-works.md) for the mechanism, and [Configure Bear](../reference/configuration.md#intercept) for the setting). It cannot see into a statically linked build tool; force [`intercept.mode: wrapper`](../reference/configuration.md#intercept) instead. ## WSL2 If `.wslconfig` sets `networkingMode=mirrored`, the loopback connection intercepted processes use to report back to `bear-driver` breaks, producing an empty or short `compile_commands.json` with no other error. Remove the setting and restart WSL2 (`wsl --shutdown`). ## Docker Bear must run inside the container, as part of the build it observes; `bear -- docker exec ...` from the host does not work. See [Run Bear inside a Docker container](../guides/recipes/docker.md). ## Installing the preload library to a non-standard lib directory ```sh INTERCEPT_LIBDIR=lib64 cargo build --release INTERCEPT_LIBDIR=lib64 ./scripts/install.sh ``` A mismatch here fails silently: preload never loads and the database comes out empty or short. See [Installing Bear](../installation.md) for why. Related: [Troubleshooting](../guides/troubleshooting.md) for output that comes out wrong, and the [Recipes](../guides/recipes/index.md) index for other tasks. ������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/platforms/macos.md���������������������������������������������������0000664�0000000�0000000�00000002306�15271777203�0022002�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: how-to --> # Bear on macOS Wrapper is the default interception method on macOS: Apple-signed Xcode compilers block preload, because System Integrity Protection strips `DYLD_INSERT_LIBRARIES` before it can take effect (see [how Bear works](../understanding/how-it-works.md)). Preload can be forced with [`intercept.mode: preload`](../reference/configuration.md#intercept), but only works with SIP disabled. ## Building through Xcode or `xcodebuild` Xcode passes many flags through an `@file` response file, and Bear's default recording keeps that argument literal. Turn on [`format.arguments.from_response_files`](../reference/configuration.md#formatarguments) so an entry built through `xcodebuild` carries the actual flags. ## Homebrew-installed compilers Homebrew's GCC formula installs only versioned binaries (`gcc-14`, not `gcc`); Bear recognizes these by filename automatically (see [Supported compilers](../reference/supported-compilers.md)). Related: [how Bear works](../understanding/how-it-works.md) for the preload/wrapper mechanism, [Troubleshooting](../guides/troubleshooting.md) for output that comes out wrong, and the [Recipes](../guides/recipes/index.md) index for other tasks. ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/platforms/windows.md�������������������������������������������������0000664�0000000�0000000�00000002021�15271777203�0022364�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: how-to --> # Bear on Windows Wrapper is the only interception method on Windows: there is no `LD_PRELOAD`/`DYLD_INSERT_LIBRARIES` equivalent, so forcing preload is a startup error before anything runs (see [how Bear works](../understanding/how-it-works.md) and [Configure Bear](../reference/configuration.md#intercept)). ## MSYS2 and MinGW64 environments Bear builds under MSYS2's Unix-like environments (MINGW64, UCRT64, CLANG64, CLANGARM64). `$MINGW_PREFIX` points at whichever one you launched (`/mingw64`, `/ucrt64`, `/clang64`, and so on), the natural `PREFIX` for installing into it: ```sh DESTDIR="$pkgdir" PREFIX="$MINGW_PREFIX" ./scripts/install.sh ``` Only `bear-driver` and `bear-wrapper` are installed in this configuration; there is no preload library to package. Related: [how Bear works](../understanding/how-it-works.md) for the wrapper mechanism, [Troubleshooting](../guides/troubleshooting.md) for output that comes out wrong, and the [Recipes](../guides/recipes/index.md) index for other tasks. ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/reference/�����������������������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0020304�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/reference/command-line.md��������������������������������������������0000664�0000000�0000000�00000010105�15271777203�0023166�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: reference --> # Command-line options `bear` runs in four modes, selected by the subcommand or its absence: combined (`bear`, no subcommand, the default), `bear intercept`, `bear semantic`, and `bear parse-sh`. Options given before `--` belong to Bear; everything after `--` is the build command, passed through untouched. - **Combined** runs the build, captures it, and writes the compilation database in one step. It is the default and the recommended way to run Bear. - **`bear intercept`** runs the build and only captures it, writing the raw event stream to a file for later analysis. - **`bear semantic`** reads an event stream and writes the compilation database, without running a build. - **`bear parse-sh`** writes the compilation database from shell command text, such as a build's dry-run output or a saved build log, without running anything. See [How Bear works](../understanding/how-it-works.md) for the mechanism behind capture and analysis, [Configure Bear](configuration.md) for every key the `-c`/`--config` file accepts, and [Exit status](exit-status.md) for what Bear's own exit code means in each mode. <!-- BEGIN GENERATED: scripts/generate-command-line-reference.py -- DO NOT EDIT. Edits inside this block are lost the next time the script runs. --> ## Global usage ``` bear [OPTIONS] -- <BUILD_COMMAND>... bear [OPTIONS] <COMMAND> ``` Bear is a tool that generates a compilation database for clang tooling. ### Subcommands | Command | Description | |---|---| | `intercept` | intercepts command execution | | `semantic` | detect semantics of command executions | | `parse-sh` | produces the compilation database from build-system dry-run text (e.g. `make -n` output) | | `help` | Print this message or the help of the given subcommand(s) | ### Arguments | Argument | Description | |---|---| | `<BUILD_COMMAND>...` | Build command | ### Options | Flag | Description | Default | |---|---|---| | `-c, --config <FILE>` | Path of the config file | - | | `-o, --output <FILE>` | Path of the result file | `compile_commands.json` | | `-a, --append` | Append result to an existing output file | - | | `--overwrite` | Overwrite an existing output file (the default) | - | | `-h, --help` | Print help | - | | `-V, --version` | Print version | - | ## bear intercept intercepts command execution ``` bear intercept --output <FILE> -- <BUILD_COMMAND>... ``` ### Arguments | Argument | Description | |---|---| | `<BUILD_COMMAND>...` | Build command | ### Options | Flag | Description | Default | |---|---|---| | `-o, --output <FILE>` | Path of the event file to write | - | | `-h, --help` | Print help | - | ## bear semantic detect semantics of command executions ``` bear semantic [OPTIONS] ``` ### Options | Flag | Description | Default | |---|---|---| | `-i, --input <FILE>` | Path of the event file to read | `-` | | `-o, --output <FILE>` | Path of the result file | `compile_commands.json` | | `-a, --append` | Append result to an existing output file | - | | `--overwrite` | Overwrite an existing output file (the default) | - | | `--print-compilers` | Print the compilers Bear recognizes and exit | - | | `-h, --help` | Print help | - | Reads the event stream that `bear intercept` (or a third-party producer) writes and produces the compilation database, e.g.: ```sh bear semantic --input events.json ``` ## bear parse-sh produces the compilation database from build-system dry-run text (e.g. `make -n` output) ``` bear parse-sh [OPTIONS] ``` ### Options | Flag | Description | Default | |---|---|---| | `-i, --input <FILE>` | Path of the shell text to parse | `-` | | `-o, --output <FILE>` | Path of the result file | `compile_commands.json` | | `-a, --append` | Append result to an existing output file | - | | `--overwrite` | Overwrite an existing output file (the default) | - | | `-C, --directory <DIR>` | Initial working directory for the parsed commands | - | | `-h, --help` | Print help | - | Parses the text a build system prints in dry-run mode and writes the compilation database, without running a build, e.g.: ```sh make -n | bear parse-sh ``` <!-- END GENERATED --> �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/reference/configuration.md�������������������������������������������0000664�0000000�0000000�00000013555�15271777203�0023506�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: reference --> # Configure Bear Bear reads an optional `bear.yml`. Every key is optional except `schema`, which is required once you write a file at all; every section below states its default, so a key you do not write keeps that value. The canonical source for every key, accepted value, and default is the [`bear(1)` man page][manpage] CONFIGURATION section. ## Where the file is found Without `--config`, Bear searches for `bear.yml` in the current working directory first, then the platform's per-user configuration directory (the XDG locations on Linux and the BSDs, `%LOCALAPPDATA%` / `%APPDATA%` on Windows). The first file found is loaded; the rest are not consulted. The exhaustive, ordered list of paths is the man page's FILES section. Pass `-c`/`--config FILE` to load a specific file instead of searching. ## schema ```yaml schema: "4.2" ``` Names the configuration format version this file is written for. It is the one key with no default: a file that omits it, or names a version this Bear release does not support, is rejected rather than partially applied. ## intercept ```yaml intercept: mode: wrapper ``` - **`intercept.mode`**: `preload` or `wrapper`. Default: `preload` on Linux and the BSDs, `wrapper` on macOS and Windows. ## compilers ```yaml compilers: - path: /usr/bin/cc as: gcc - path: /usr/local/bin/gcc ignore: true ``` Default: `[]` (no overrides; every compiler is recognized automatically). - **`compilers.path`**: Path of the compiler executable. Required. A `path` on its own, with no other key, is already enough to make an otherwise unrecognized executable be treated as a compiler. - **`compilers.as`**: Compiler type hint. Optional. The accepted values are the family ids listed on [Supported compilers](supported-compilers.md), which is generated from Bear's own compiler definitions, plus `wrapper` for a compiler launcher (see [Use Bear with ccache, distcc, or icecc](../guides/recipes/ccache-distcc-icecc.md)). `bear semantic --print-compilers` prints the same set for the version you have installed. When `as` is omitted, Bear guesses the family from the executable's filename using its normal recognition patterns, falling back to `gcc` when no pattern matches; an MSVC-style or otherwise unusual compiler whose name matches nothing is then parsed with GCC's flag rules unless `as` is set explicitly. - **`compilers.ignore`**: Exclude this executable's invocations from the database. Optional. Default: `false`. In wrapper mode this section also decides which executables get wrapped. With no entry other than `ignore` ones, Bear scans PATH at startup and wraps every compiler it recognizes. As soon as one entry is not ignored, Bear wraps exactly the listed paths and skips the scan, so listing a single unusual compiler stops `gcc` and `clang` from being intercepted unless you list them as well. Compilers named in the compiler environment variables (`CC`, `CXX`, and similar) are wrapped either way. Preload mode is unaffected, because it intercepts every executed program regardless of this section. ## sources ```yaml sources: directories: - path: /project/tests action: exclude files: - pattern: "moc_*.cpp" action: exclude ``` Default: `{}` (both lists empty; nothing is filtered out). - **`sources.directories.path`**, **`sources.directories.action`**: list of directory rules; `action` is `include` or `exclude`. - **`sources.files.pattern`**, **`sources.files.action`**: list of filename-glob rules; `action` is `include` or `exclude`. ## duplicates ```yaml duplicates: match_on: - file - arguments ``` - **`duplicates.match_on`**: list of entry fields to compare: `file`, `arguments`, `directory`, `command`, `output`. Two entries are duplicates when all configured fields match; the first occurrence is kept. Default: `[directory, file]`. ## format ```yaml format: paths: directory: canonical file: canonical entries: use_array_format: true include_output_field: true arguments: from_response_files: false from_environment: true ``` ### format.paths - **`format.paths.directory`**, **`format.paths.file`**: `as-is`, `canonical`, `relative`, or `absolute`. Default: `as-is` for both. ### format.entries - **`format.entries.use_array_format`**: write the `arguments` array instead of the `command` string. Default: `true`. - **`format.entries.include_output_field`**: include the `output` field. Default: `true`. ### format.arguments - **`format.arguments.from_response_files`**: expand `@file` response-file references into their tokenized contents. Default: `false`. - **`format.arguments.from_environment`**: fold compiler environment variables (`CPATH`, `C_INCLUDE_PATH`, `CPLUS_INCLUDE_PATH`, `OBJC_INCLUDE_PATH`, MSVC's `CL` / `_CL_`) into the recorded arguments. Default: `true`. ## headers ```yaml headers: enabled: true strategy: dependency-files ``` - **`headers.enabled`**: turn header-entry synthesis on. Default: `false`. - **`headers.strategy`**: `siblings` or `dependency-files`. Default: `siblings`. ## Checking what is actually in effect Bear logs its fully resolved configuration as YAML whenever `RUST_LOG` is set to `info` or a more verbose level: ```sh RUST_LOG=info bear -- true ``` This is the same line whether the values came from a `bear.yml` or from built-in defaults, so it is the way to check what a given file actually changed on this machine, rather than working it out from the sections above. See also: [How Bear works](../understanding/how-it-works.md) for the interception and semantic-analysis mechanism this configuration shapes, [Supported compilers](supported-compilers.md) for compiler recognition, and the [Recipes](../guides/recipes/index.md) for task-oriented uses of these sections (for example excluding generated sources). [manpage]: https://github.com/rizsotto/Bear/blob/master/man/bear.1.md ���������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/reference/exit-status.md���������������������������������������������0000664�0000000�0000000�00000006244�15271777203�0023126�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: reference --> # Exit status Modes that run a build report the build's own outcome; modes that only transform data report whether Bear understood its input. | Mode | Exit status | |---|---| | `bear -- <build>` (combined) | The build's exit code, byte for byte: `0` on success, the same non-zero code otherwise. | | `bear intercept -- <build>` | Same as combined: the build's exit code. | | `bear semantic` | `0` when at least one input event was understood, or the input was empty. Non-zero when the input was non-empty and every event was rejected. | | `bear parse-sh` | `0` when at least one line parsed as a shell command, even when none of them named a recognized compiler, or when the input was empty. Non-zero when the input was non-empty and every line was skipped. | | `bear semantic --print-compilers` | Always `0`. | | `bear --help` (or a subcommand's `--help`) | Always `0`. | | A malformed invocation (unknown flag, missing build command) | Non-zero (`2`), reported before anything runs. | See [command-line options](command-line.md) for what each mode does. ## Notes **A build killed by a signal never exits `0`, but the signal number is not encoded.** `bear -- sh -c 'kill -TERM $$'` exits `1`, and so does a build killed with `SIGKILL`. Bear does not follow the `128 + signal` shell convention: a caller that only inspects Bear's own exit code cannot tell a signalled build from an ordinary failing one. The signal itself still reaches the build unchanged. **Empty input is not an error.** An empty `semantic` or `parse-sh` run writes an empty database, exits `0`, and prints a notice on standard error so an accidentally empty pipeline does not pass unnoticed. The same holds for `parse-sh` input that parses cleanly but names no compiler invocation, such as a lone `mv` command: no line is skipped, no diagnostic is printed, and the empty database is a success. **Bear can fail on its own, independent of the build or the input.** A configuration file that fails to parse, an invalid combination of flags (for example directing the compilation database to standard output while also running a build), or a named input file that does not exist are all reported before any build runs or any input is read, and all exit non-zero. An output path that cannot be written (a directory sitting where the database file should go) is also non-zero, and this one can surface after the build already succeeded: the build's own outcome does not change, but Bear's exit code still reports non-zero because it could not deliver its result. A missing preload library is not one of these failures, and the consequence is worth knowing: the dynamic linker ignores a preload target it cannot open, prints its own warning on standard error, and lets the build run. The build's own commands are still observed, but the processes it spawns are not, so a build whose compiler runs under `make` yields an empty database and an exit status of 0. Success here means the build succeeded, never that the database is complete. See [Bear produces an empty compile_commands.json](../guides/recipes/empty-compilation-database.md). For what to do about a non-zero exit, see [troubleshooting](../guides/troubleshooting.md). ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/reference/supported-compilers.md�������������������������������������0000664�0000000�0000000�00000043472�15271777203�0024660�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: reference --> # Supported compilers Bear decides whether an intercepted command is a compilation by matching the name of the executable the build ran against a table of known compiler names. A name it does not know is not a compiler, and Bear does not run the program to guess otherwise. The one exception is a short list of names that are genuinely ambiguous across platforms, and [Ambiguous names](#ambiguous-names) below says which and why. Once a name matches, the command line is parsed with that family's own flag rules, since GCC, MSVC, and Swift (to pick three) do not agree on how to spell an include path or an output file. The configuration keys that let you extend or correct that matching are in the [`bear(1)` man page][manpage] and explained in [Configure Bear](configuration.md). ## Cross-compiler prefixes and version suffixes A handful of core families are recognized under more than their plain name. GCC and Clang (and, following the same pattern, CUDA's `nvcc`, Flang, and Vala's `valac`) are also recognized with a cross-compilation target prefix (`arm-linux-gnueabihf-gcc`, `aarch64-linux-gnu-clang`), a version suffix (`gcc-12`, `clang-15`), or both together (`arm-linux-gnueabi-gcc-12`). Every such spelling is parsed with its base name's flag rules. Support for this varies by family: it is not a blanket rule applied to every entry in the table below. ## Compiler launchers ccache, sccache, distcc, and icecc wrap a real compiler invocation rather than compiling anything themselves. Bear recognizes each launcher by name, finds the real compiler among its arguments, and records the compilation as if the launcher were not there: `ccache gcc -c main.c` is recorded as a `gcc` invocation, not a `ccache` one. A launcher invocation whose argument is not a recognized compiler (`ccache make all`), or that wraps another launcher (`ccache distcc gcc -c main.c`), produces no entry; Bear does not chase a chain of launchers. distcc's own options (`-j`, `--jobs`, `-v`, and similar) are skipped while looking for the compiler, so they never get mistaken for one. ## MPI compiler wrappers `mpicc`, `mpicxx`, `mpifort`, and the vendor-specific MPI wrappers (Intel's `mpiicc`, `mpiifort`, and similar) are recorded as invoked, not expanded to the compiler they wrap. Clang tooling that needs a wrapper's baked-in include paths can point at the wrapper directly, for example with clangd's `--query-driver`. An information-only invocation (`mpicc -showme`, `-show`, `-compile_info`, `-link_info`) produces no entry, and a wrapper option that carries a value (MPICH's `-cc=gcc`) never swallows a following source file. ## Ambiguous names `cc`, `c++`, and the HPE Cray PrgEnv wrapper `CC` are not in the name table at all, because the same basename is a different compiler depending on the platform or the loaded environment module (GCC on most Linux distributions, Clang on the BSDs and macOS, whatever the loaded programming environment selects on a Cray system). Bear classifies these by running the executable once with `--version` and caching the result; a `compilers:` entry in the configuration (see below) skips the probe and forces a classification when its output does not match a known signature. ## `as` and `ignore` hints A `compilers:` entry in the configuration names a path and either an `as` value (the family to parse it with) or `ignore: true` (drop its invocations entirely). Reach for it when a compiler sits at a path or under a name the table below does not cover, or when a generic name is misclassified. The generic names `cc` and `c++` are the usual reason to add an entry, since not every custom build of GCC or Clang answers `--version` in a way the probe recognizes. An `as` value is a family identifier, not a display name: it is the short, lower-case id given as each family's "Configuration `as:` value" below, spelled verbatim. There are no aliases, so `as: gcc` is accepted while `as: GCC` is not, and several families share one id (`icx` and `icc` are both `intel_cc`). The accepted ids are the ones in the family tables below, which are generated from the compiler definitions, plus `wrapper` for a compiler launcher. This page does not repeat them here as a list: the tables are the copy that cannot drift. An unaccepted value is rejected when the configuration loads, with an error listing every value that would have worked, and `bear semantic --print-compilers` prints the same mapping for the version you have installed. ## Recognized families Internal jobs that a driver spawns for itself are recognized only so Bear can filter them back out: they are never user-facing invocations and never produce a database entry. The tables below label each one as internal, separately from the family's real, user-facing names. The compiler launchers share one kind rather than a compiler family, so `wrapper`, or any launcher's own name from the launcher table below, selects it. <!-- BEGIN GENERATED: scripts/generate-supported-compilers.py -- DO NOT EDIT. Edits inside this block are lost the next time the script runs. --> ### Compiler families #### `arm_flang` Configuration `as:` value: `arm_flang`. | Executable names | Recognized as | Version suffix | Cross-compilation prefix | Documentation | |---|---|---|---|---| | `armflang` | Arm Compiler for Linux Fortran | not recognized | not recognized | [learn.arm.com](https://learn.arm.com/install-guides/acfl/), [support.arm.com](https://support.arm.com/documentation/101380/latest/) | #### `armclang` Configuration `as:` value: `armclang`. | Executable names | Recognized as | Version suffix | Cross-compilation prefix | Documentation | |---|---|---|---|---| | `armclang`, `armclang++` | Arm Compiler 6 | `armclang-12` | not recognized | [developer.arm.com](https://developer.arm.com/documentation/100748/latest/) | #### `clang` Configuration `as:` value: `clang`. | Executable names | Recognized as | Version suffix | Cross-compilation prefix | Documentation | |---|---|---|---|---| | `clang`, `clang++` | Clang | `clang-12` | `arm-linux-gnueabihf-clang` | [clang.llvm.org](https://clang.llvm.org/docs/ClangCommandLineReference.html) | | `amdclang`, `amdclang++`, `hipcc` | AMD ROCm Clang/HIP | not recognized | not recognized | [rocm.docs.amd.com](https://rocm.docs.amd.com/en/latest/) | | `emcc`, `em++`, `emcc.py`, `em++.py` | Emscripten | not recognized | not recognized | [emscripten.org](https://emscripten.org/docs/tools_reference/emcc.html) | | `tiarmclang` | Texas Instruments Arm Clang | not recognized | not recognized | [ti.com](https://www.ti.com/tool/ARM-CGT) | An invocation is also ignored when its arguments include `-cc1`: that is an internal frontend or codegen call, not a user-facing compile. #### `clang_cl` Configuration `as:` value: `clang_cl`. | Executable names | Recognized as | Version suffix | Cross-compilation prefix | Documentation | |---|---|---|---|---| | `clang-cl` | Clang (MSVC mode) | `clang-cl-12` | not recognized | [clang.llvm.org](https://clang.llvm.org/docs/UsersManual.html#clang-cl) | #### `cray_cc` Configuration `as:` value: `cray_cc`. | Executable names | Recognized as | Version suffix | Cross-compilation prefix | Documentation | |---|---|---|---|---| | `craycc`, `crayCC`, `craycxx` | Cray C/C++ | `craycc-12` | not recognized | [support.hpe.com](https://support.hpe.com/hpesc/public/docDisplay?docId=a00113893en_us&page=index.html) | #### `cray_fortran` Configuration `as:` value: `cray_fortran`. | Executable names | Recognized as | Version suffix | Cross-compilation prefix | Documentation | |---|---|---|---|---| | `crayftn`, `ftn` | Cray Fortran | `crayftn-12` | not recognized | [support.hpe.com](https://support.hpe.com/hpesc/public/docDisplay?docId=a00115296en_us&page=Fortran_Command-line_Options.html) | #### `cuda` Configuration `as:` value: `cuda`. | Executable names | Recognized as | Version suffix | Cross-compilation prefix | Documentation | |---|---|---|---|---| | `nvcc` | NVIDIA CUDA | `nvcc-12` | `arm-linux-gnueabihf-nvcc` | [docs.nvidia.com](https://docs.nvidia.com/cuda/cuda-compiler-driver-nvcc/index.html) | #### `embarcadero` Configuration `as:` value: `embarcadero`. | Executable names | Recognized as | Version suffix | Cross-compilation prefix | Documentation | |---|---|---|---|---| | `bcc64x`, `bcc64`, `bcc32x` | Embarcadero C++Builder (Clang-based) | not recognized | not recognized | [docwiki.embarcadero.com](https://docwiki.embarcadero.com/RADStudio/Athens/en/Command-Line_Interfaces_of_C++_Compilers), [docwiki.embarcadero.com](https://docwiki.embarcadero.com/RADStudio/Athens/en/BCC64X) | #### `fasm` Configuration `as:` value: `fasm`. | Executable names | Recognized as | Version suffix | Cross-compilation prefix | Documentation | |---|---|---|---|---| | `fasm` | flat assembler | not recognized | not recognized | [flatassembler.net](https://flatassembler.net/docs.php) | #### `flang` Configuration `as:` value: `flang`. | Executable names | Recognized as | Version suffix | Cross-compilation prefix | Documentation | |---|---|---|---|---| | `flang`, `flang-new` | Flang | `flang-12` | `arm-linux-gnueabihf-flang` | [flang.llvm.org](https://flang.llvm.org/docs/FlangDriver.html) | | `amdflang` | AMD ROCm Flang | not recognized | not recognized | [rocm.docs.amd.com](https://rocm.docs.amd.com/en/latest/) | #### `gcc` Configuration `as:` value: `gcc`. | Executable names | Recognized as | Version suffix | Cross-compilation prefix | Documentation | |---|---|---|---|---| | `gcc`, `g++`, `gfortran`, `egfortran`, `f95` | GCC | `gcc-12` | `arm-linux-gnueabihf-gcc` | [gcc.gnu.org](https://gcc.gnu.org/onlinedocs/gcc/Option-Summary.html) | | `xc8-cc`, `xc8` | Microchip XC8 | not recognized | not recognized | [microchip.com](https://www.microchip.com/en-us/tools-resources/develop/mplab-xc-compilers) | Internal, not user-facing: `cc1`, `cc1plus`, `cc1obj`, `cc1objplus`, `f951`, `collect2`, `lto1`. Bear recognizes these only so it can filter them back out; they never produce a database entry. #### `ibm_xl` Configuration `as:` value: `ibm_xl`. | Executable names | Recognized as | Version suffix | Cross-compilation prefix | Documentation | |---|---|---|---|---| | `ibm-clang`, `ibm-clang++` | IBM Open XL C/C++ | `ibm-clang-12` | not recognized | [ibm.com](https://www.ibm.com/docs/en/openxl-c-and-cpp-aix) | | `xlclang`, `xlclang++` | IBM XL C/C++ | `xlclang-12` | not recognized | [ibm.com](https://www.ibm.com/docs/en/xl-c-and-cpp-aix) | #### `intel_cc` Configuration `as:` value: `intel_cc`. | Executable names | Recognized as | Version suffix | Cross-compilation prefix | Documentation | |---|---|---|---|---| | `icx`, `icpx` | Intel oneAPI C/C++ | `icx-12` | not recognized | [intel.com](https://www.intel.com/content/www/us/en/developer/tools/oneapi/dpc-compiler.html) | | `icc`, `icpc` | Intel C++ Compiler Classic | `icc-12` | not recognized | [intel.com](https://www.intel.com/content/www/us/en/developer/tools/oneapi/dpc-compiler.html) | | `mpiicc`, `mpiicpc`, `mpiicx`, `mpiicpx` | Intel MPI C/C++ wrapper | not recognized | not recognized | [intel.com](https://www.intel.com/content/www/us/en/developer/tools/oneapi/mpi-library.html) | #### `intel_cl` Configuration `as:` value: `intel_cl`. | Executable names | Recognized as | Version suffix | Cross-compilation prefix | Documentation | |---|---|---|---|---| | `icx-cl` | Intel oneAPI DPC++/C++ (MSVC mode) | not recognized | not recognized | [intel.com](https://www.intel.com/content/www/us/en/docs/oneapi/programming-guide/current/invoke-the-compiler.html), [intel.com](https://www.intel.com/content/www/us/en/docs/dpcpp-cpp-compiler/developer-guide-reference/2026-0/invoke-the-compiler.html) | #### `intel_fortran` Configuration `as:` value: `intel_fortran`. | Executable names | Recognized as | Version suffix | Cross-compilation prefix | Documentation | |---|---|---|---|---| | `ifort`, `ifx` | Intel Fortran | `ifort-12` | not recognized | [intel.com](https://www.intel.com/content/www/us/en/developer/tools/oneapi/fortran-compiler.html) | | `mpiifort`, `mpiifx` | Intel Fortran MPI wrapper | not recognized | not recognized | [intel.com](https://www.intel.com/content/www/us/en/developer/tools/oneapi/mpi-library.html) | #### `mpi` Configuration `as:` value: `mpi`. | Executable names | Recognized as | Version suffix | Cross-compilation prefix | Documentation | |---|---|---|---|---| | `mpicc`, `mpicxx`, `mpic++`, `mpiCC`, `mpifort`, `mpif77`, `mpif90` | MPI compiler wrapper | `mpicc-12` | not recognized | [open-mpi.org](https://www.open-mpi.org/doc/), [mpich.org](https://www.mpich.org/documentation/) | #### `msvc` Configuration `as:` value: `msvc`. | Executable names | Recognized as | Version suffix | Cross-compilation prefix | Documentation | |---|---|---|---|---| | `cl` | Microsoft Visual C++ | not recognized | not recognized | [learn.microsoft.com](https://learn.microsoft.com/en-us/cpp/build/reference/compiler-options) | Internal, not user-facing: `c1`, `c1xx`, `c2`. Bear recognizes these only so it can filter them back out; they never produce a database entry. #### `nasm` Configuration `as:` value: `nasm`. | Executable names | Recognized as | Version suffix | Cross-compilation prefix | Documentation | |---|---|---|---|---| | `nasm`, `yasm` | NASM / YASM assembler | not recognized | not recognized | [nasm.us](https://www.nasm.us/doc/nasmdoc2.html), [github.com](https://github.com/yasm/yasm) | #### `nvidia_hpc` Configuration `as:` value: `nvidia_hpc`. | Executable names | Recognized as | Version suffix | Cross-compilation prefix | Documentation | |---|---|---|---|---| | `nvc`, `nvc++`, `nvfortran` | NVIDIA HPC SDK | `nvc-12` | not recognized | [docs.nvidia.com](https://docs.nvidia.com/hpc-sdk/compilers/hpc-compilers-user-guide/index.html) | | `pgcc`, `pgc++`, `pgfortran` | PGI (legacy NVIDIA HPC) | `pgcc-12` | not recognized | [docs.nvidia.com](https://docs.nvidia.com/hpc-sdk/compilers/hpc-compilers-user-guide/index.html) | #### `qnx` Configuration `as:` value: `qnx`. | Executable names | Recognized as | Version suffix | Cross-compilation prefix | Documentation | |---|---|---|---|---| | `qcc`, `q++` | QNX qcc | not recognized | not recognized | [qnx.com](https://www.qnx.com/developers/docs/) | #### `swift` Configuration `as:` value: `swift`. | Executable names | Recognized as | Version suffix | Cross-compilation prefix | Documentation | |---|---|---|---|---| | `swiftc` | Swift | not recognized | not recognized | [swift.org](https://www.swift.org/documentation/) | Internal, not user-facing: `swift-frontend`. Bear recognizes these only so it can filter them back out; they never produce a database entry. An invocation is also ignored when its arguments include `-frontend`: that is an internal frontend or codegen call, not a user-facing compile. #### `ti_cgt` Configuration `as:` value: `ti_cgt`. | Executable names | Recognized as | Version suffix | Cross-compilation prefix | Documentation | |---|---|---|---|---| | `armcl` | TI Code Generation Tools (Arm) | not recognized | not recognized | [ti.com](https://www.ti.com/lit/ug/spnu151v/spnu151v.pdf) | | `cl6x` | TI Code Generation Tools (C6000) | not recognized | not recognized | [ti.com](https://www.ti.com/lit/ug/spru187/spru187.pdf) | | `cl7x` | TI Code Generation Tools (C7000) | not recognized | not recognized | [ti.com](https://www.ti.com/lit/ug/spruig8/spruig8.pdf) | | `cl2000` | TI Code Generation Tools (C2000) | not recognized | not recognized | [ti.com](https://www.ti.com/lit/ug/spru514/spru514.pdf) | | `cl430` | TI Code Generation Tools (MSP430) | not recognized | not recognized | [ti.com](https://www.ti.com/lit/ug/slau132/slau132.pdf) | | `clpru` | TI Code Generation Tools (PRU) | not recognized | not recognized | [ti.com](https://www.ti.com/lit/ug/spruhv7/spruhv7.pdf) | #### `vala` Configuration `as:` value: `vala`. | Executable names | Recognized as | Version suffix | Cross-compilation prefix | Documentation | |---|---|---|---|---| | `valac` | Vala | `valac-12` | `arm-linux-gnueabihf-valac` | [vala.dev](https://vala.dev/) | ### Compiler launchers | Executable name | Recognized as | Configuration `as:` value | Documentation | |---|---|---|---| | `ccache` | Compiler cache | `ccache` | [ccache.dev](https://ccache.dev/manual/latest.html) | | `distcc` | Distributed compiler | `distcc` | [distcc.org](https://www.distcc.org/man/distcc_1.html) | | `icecc` | Distributed compiler | `icecc` | [github.com](https://github.com/icecc/icecream/blob/master/README.md) | | `sccache` | Compiler cache | `sccache` | [github.com](https://github.com/mozilla/sccache/blob/main/README.md) | <!-- END GENERATED --> This list is kept in step with Bear's own compiler definitions, and grows by request as users bring toolchains that are not covered yet; it is not a fixed set. ## What is not recognized A few names are excluded on purpose, not by oversight: - **`as`** (the GNU assembler): GCC and Clang already spawn it on a temporary file for every ordinary compile, so recognizing it would add a throwaway entry per compilation, keyed on a name that differs every run. - **`mpirun`, `mpiexec`**: these launch programs, they do not compile anything. - **`swift`**: this is the Swift subcommand dispatcher (`swift build`, `swift run`); the actual compiler invocation is `swiftc`. - **`amdgpu-arch`**: reports target GPU architectures; despite the shared prefix, it is not a compiler driver. - **`ml`, `ml64`** (MASM): not currently supported. None of these can be recovered with a `compilers:` override, because `as` only redirects a path to one of the families already in the tables above; it cannot teach Bear a new one. If your build uses a toolchain that genuinely is not covered anywhere in this page, that is a gap in Bear's compiler definitions, not a configuration problem: it is worth filing as an issue against the project. See also: [Configure Bear](configuration.md) for the `compilers:` section in full, and [How Bear works](../understanding/how-it-works.md) for where recognition fits between interception and writing the database. [manpage]: https://github.com/rizsotto/Bear/blob/master/man/bear.1.md ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/tutorials/�����������������������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0020374�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/tutorials/autotools-tutorial.md��������������������������������������0000664�0000000�0000000�00000017364�15271777203�0024623�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: tutorial --> # Build an autotools project under Bear By the end of this page you will have built libtasn1 4.20.0 twice: once the ordinary way, once under Bear, and compared what each run leaves behind. This page assumes Bear is already installed; see [Getting started with Bear](getting-started.md) for a first run against a plain Makefile project, or [Install Bear](../installation.md) for platform packages. ## Get a real project libtasn1 is a small GNU library built with automake and autoconf, a common shape for the C and C++ projects Bear targets. Its release tarball ships a generated `configure` script, so building it needs nothing beyond a C compiler and `make`, no autoconf or automake installed. Download and extract it: ```sh curl -fsSL -O https://ftp.gnu.org/gnu/libtasn1/libtasn1-4.20.0.tar.gz tar xzf libtasn1-4.20.0.tar.gz cd libtasn1-4.20.0 ``` GNU's release tarballs do not change after release, so this download always matches this checksum: ```shell $ sha256sum libtasn1-4.20.0.tar.gz 92e0e3bd4c02d4aeee76036b2ddd83f0c732ba4cda5cb71d583272b23587a76c libtasn1-4.20.0.tar.gz ``` ## Build it the ordinary way ```sh ./configure make ``` This builds `libtasn1.so` under `lib/.libs/`, the same as it would for anyone packaging or installing this library. Look for a compilation database and there is none: ```shell $ ls compile_commands.json ls: cannot access 'compile_commands.json': No such file or directory ``` Nothing here was written to produce one; `configure` and `make` have no notion of a compilation database. That is the gap Bear fills. ## Build it under Bear Start from a clean tree and repeat the same two commands, this time running `make` under Bear: ```sh make distclean ./configure bear -- make ``` `./configure` is exactly the command from the previous section, run the same way, outside Bear. Only `make` runs under Bear. Preload interception, the default on Linux, watches every process the build spawns, so it does not matter that `configure` ran unsupervised earlier: Bear only needs to be watching for the commands it must record, and those are the compiler invocations `make` issues. "Build it again, in wrapper mode" below builds this same project a second time under the other interception method, where the configure step does need to run under Bear. ## Look at the result ```shell $ grep -c '"file":' compile_commands.json 34 ``` libtasn1's `Makefile.am` recurses into six subdirectories, and each one contributes its own entries: `lib` (9), `lib/gl` (3), `src` (4), `src/gl` (14), `examples` (3), `fuzz` (1). If you get fewer, check whether `make` stopped early: this release also builds its Texinfo manual, which needs `makeinfo` from the `texinfo` package, and a failure there ends the recursion before the last directories are reached. That is worth seeing once, because it is the general case with interception: Bear records what the build got to, so a build that stops early gives you a database that stops with it. A recursive automake build like this compiles in several directories at once, and gnulib's bundled replacement sources under the two `gl` directories are compiled with their own flags, separate from the library's. Here is one entry from `lib`: ```json { "file": "ASN1.c", "arguments": [ "/usr/bin/gcc", "-DHAVE_CONFIG_H", "-I.", "-I..", "-I./gl", "-I./includes", "-DASN1_BUILDING", "-fanalyzer", "-Wall", "-Wextra", "-Wshadow", "-Wwrite-strings", "...", "-g", "-O2", "-c", "ASN1.c", "-fPIC", "-o", ".libs/ASN1.o" ], "directory": "/home/you/libtasn1-4.20.0/lib", "output": ".libs/ASN1.o" } ``` (the real entry lists several dozen more `-W` flags; they are elided above with `...`) And one from `src/gl`: ```json { "file": "cloexec.c", "arguments": [ "/usr/bin/gcc", "-DHAVE_CONFIG_H", "-I.", "-I../..", "-Wno-cast-qual", "-Wno-conversion", "-Wno-sign-compare", "-Wno-unused-function", "-Wno-unused-parameter", "-g", "-O2", "-c", "cloexec.c", "-fPIC", "-o", ".libs/libsgl_la-cloexec.o" ], "directory": "/home/you/libtasn1-4.20.0/src/gl", "output": ".libs/libsgl_la-cloexec.o" } ``` `lib` turns on a wall of analyzer and style warnings for libtasn1's own code; `src/gl` turns most of them back off, because that directory holds gnulib's imported replacement sources, not code the project wants held to its own warning level. One database, one `directory` per entry, describes the whole recursive build: a tool reading it gets the flags that actually compiled each file, instead of one guessed command applied everywhere. ## Use the database: clangd ```shell $ clangd --check=lib/ASN1.c I[...] Compile command from CDB is: [/home/you/libtasn1-4.20.0/lib] /usr/bin/gcc -DHAVE_CONFIG_H -I. -I.. -I./gl -I./includes -DASN1_BUILDING -fanalyzer -Wall -Wextra ... -g -O2 -c -fPIC -o .libs/ASN1.o -resource-dir=... -- /home/you/libtasn1-4.20.0/lib/ASN1.c ``` clangd pulled the same flags straight out of `compile_commands.json`, directory and all. See [Set up clangd for a project without CMake](../guides/recipes/clangd-setup.md) for the editor side of this hand-off. ## Build it again, in wrapper mode Preload interception, used above, watches every process the build spawns, so it never mattered that `./configure` ran outside Bear: the commands Bear needed to see were `make`'s. Wrapper mode works differently: it puts wrapper executables on `PATH` in place of the real compilers, so a step that discovers and records the compiler has to see the wrapper while it does so, or the build later invokes the real compiler directly and Bear observes nothing. For libtasn1, that step is `./configure`. See [How Bear works](../understanding/how-it-works.md) for the mechanism behind both interception methods. That makes libtasn1's `configure` step a case where running under Bear is required, not just harmless. Write a `bear.yml` next to the source tree: ```yaml schema: "4.2" intercept: mode: wrapper ``` Bear finds `bear.yml` in the current working directory on its own, so no flag is needed; naming it explicitly with `--config bear.yml` works the same way. This same file selects wrapper mode as the interception method on every platform, including Linux and the BSDs, where preload is otherwise the default. [Configure Bear](../reference/configuration.md) covers the rest of the file's keys and the other places Bear looks for it. From a clean tree, run `configure` and `make` as two separate Bear invocations, in order: ```sh make distclean bear --config bear.yml -- ./configure bear --config bear.yml -- make ``` The first command writes a database of its own: six entries, all of them `configure`'s own throwaway compiler probes (`conftest.c` and similar, plus a libtool probe under a temporary directory), not libtasn1's sources. The second command writes the same 34 entries, across the same six directories, as the preload build above, with no `conftest` entry among them. Bear overwrites the output file on every run by default, so the second command's database simply replaces the first's; the probes are not merged in and do not need to be filtered out. Do not add `--append` to either command here, since that flag places new entries alongside the old ones instead of replacing them, which would let the probes survive into the final database. ## Next steps - [Generate compile_commands.json for a Makefile project](../guides/recipes/compile-commands-for-makefile.md) for the autotools variations this page skipped: the general `conftest.c` probe caveat, incremental and parallel builds. - [Recover compile_commands.json from a build log](compile-commands-from-a-build-log.md) if you cannot run the build yourself, only read its log. - [How Bear works](../understanding/how-it-works.md) for the interception mechanism. ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/tutorials/compile-commands-from-a-build-log.md�����������������������0000664�0000000�0000000�00000016636�15271777203�0027214�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: tutorial --> # Recover compile_commands.json from a build log By the end of this page you will have turned a plain text build log, with no live build behind it, into a working `compile_commands.json`, using `bear parse-sh` instead of running anything. This is for the build you cannot run again: a CI job that already finished and left its log in an artifact, a machine you no longer have access to, or a build that takes an hour when you only need the compiler flags out of it. [Getting started with Bear](getting-started.md) covers the other path, running a build under Bear as it happens; this page covers reconstructing a database from text alone, after the fact. ## Get a build to reconstruct Download Lua 5.4.8, a small, dependency-free C project, and extract it: ```sh curl -LO https://www.lua.org/ftp/lua-5.4.8.tar.gz tar xzf lua-5.4.8.tar.gz cd lua-5.4.8 ``` ## Capture a dry run Ask `make` to print the commands it would run, without running them, and save that text to a file: ```sh make -nw > build.log ``` `-n` is the dry run: `make` prints each recipe line instead of executing it. `-w` makes `make` also print `Entering directory` and `Leaving directory` lines around every directory it works in. Lua's top-level `Makefile` hands the real build off to a `make` invocation in `src/`; that inner `make` already announces its own directory by default, and `-w` additionally names the top-level one, so the log names every directory the build touched, not just the ones recursion would have announced anyway. ## Turn the log into a database ```shell $ bear parse-sh -i build.log bear: warning: line 4: skipped (command substitution) bear: warning: line 5: skipped (command substitution) bear: warning: parse-sh: 40 command(s) parsed, 2 line(s) skipped ``` Those two warnings point at Lua's own platform-detection recipe, which runs `` echo Guessing `uname` `` and `` make `uname` `` to pick a default target: the backticks are shell command substitution, outside the subset `parse-sh` understands, so it skips both lines and says why. Neither line is a compiler invocation, so nothing is missing from the result; a skip only costs you an entry when the skipped line was one. The run still exits 0, because most of the log did parse. `compile_commands.json` now holds one entry per source file `make` would have compiled: ```shell $ grep -c '"file"' compile_commands.json 34 ``` and each entry looks like this: ```json { "directory": "/home/you/lua-5.4.8/src", "file": "lapi.c", "arguments": [ "gcc", "-std=gnu99", "-O2", "-Wall", "-Wextra", "-DLUA_COMPAT_5_3", "-DLUA_USE_LINUX", "-c", "-o", "lapi.o", "lapi.c" ], "output": "lapi.o" } ``` `directory` is `src`, not the checkout root: `parse-sh` followed the `cd src` in the top-level recipe and the `Entering directory` lines `-w` added, the same way it would for a real recursive build. ## The same log, read somewhere else The scenario this page is really for is a log you did not just produce: someone hands you `build.log` from a build that ran elsewhere, or ran an hour ago on a machine that is gone now. Copy it somewhere that has never seen the Lua checkout, and parse it from there: ```shell $ mkdir ~/handed-a-log $ cp lua-5.4.8/build.log ~/handed-a-log/ $ cd ~/handed-a-log $ bear parse-sh -i build.log $ grep -m1 '"directory"' compile_commands.json "directory": "/home/you/lua-5.4.8/src", ``` Same 34 entries, same directory, even though `~/handed-a-log` has no `lua-5.4.8` anywhere near it. `parse-sh` gets `directory` from the `cd` and `Entering directory` text inside the log itself, never from where you happen to run it; that is what makes a saved log portable. Capture with `-nw` as a habit: it costs nothing and guarantees the log names its own build root, including for a Makefile whose top level compiles directly instead of only delegating, which the [Makefile recipe](../guides/recipes/compile-commands-for-makefile.md) covers in more detail. ## Limits of reconstructing from text `parse-sh` reconstructs commands from what a build system chose to print; it never observes anything running. That makes it lower fidelity than intercepting a real build, in specific, permanent ways: - A dry run can omit commands entirely. Recursive `make` does not always propagate `-n` to sub-makes, and a command that compiles a not-yet-generated source never gets printed in the first place. The sharpest case of this is a build that produces a tool and then consults that tool to decide what to compile, for example `SRCS := $(shell ./tool)` in a makefile, or an equivalent generated-flags step. `make -n` only prints the recipe that builds the tool; it never runs it, so `$(shell ./tool)` has nothing to run and the compiles that depend on its output never reach the log at all. On a project shaped this way, `bear -- make` records every compile, the tool's own included; `make -n` piped into `bear parse-sh` records only the tool's compile - silently, with no warning, and exit status 0. A generated makefile that is `include`d, by contrast, is not affected: GNU make remakes an included makefile for real even under `-n`, so a source list coming from an included `.mk` file survives the dry run. It is specifically the `$(shell ...)`-at-parse-time shape that goes missing. Nothing signals the gap, so notice it by comparing the entry count against what you expect for the project; when you find one, run the actual build under Bear (`bear -- make`) if it can be run at all. - A bare executable name in the log, like `gcc` above, resolves against the environment and `PATH` you run `parse-sh` in, not the original build's. If the build used a different compiler on `PATH`, the database will point at yours instead. - Only a documented subset of POSIX shell is understood: word splitting and quoting, the `;`, `&&`, `||`, `&`, and `|` separators, comments, redirections, `cd`, brace groups, and make's directory markers. Subshells, command substitution (what tripped the two lines above), parameter expansion, globs in the executable position, here-documents, and shell keywords such as `if` or `for` are all outside it and get skipped, loudly, rather than guessed at. See `bear parse-sh` in the [`bear(1)` man page][manpage] for the exact list. Prefer `bear -- make` (see [Getting started with Bear](getting-started.md) and the [Makefile recipe](../guides/recipes/compile-commands-for-makefile.md)) whenever you can actually run the build: it observes the real `exec()` calls, so it cannot miss a command or resolve a compiler name wrong. Reach for `parse-sh` only when running the build again is not an option. ## Related pages - [Getting started with Bear](getting-started.md) for the tutorial that builds a project under Bear directly, instead of from a log. - [Generate compile_commands.json for a Makefile project](../guides/recipes/compile-commands-for-makefile.md) for recursive builds, incremental builds, and the rest of the Make-specific detail. - [Capture a complete dry run for `bear parse-sh`](../guides/recipes/dry-run-capture.md) for the capture flags that decide how much of the build reaches the log in the first place. - [Bear produces an empty compile_commands.json](../guides/recipes/empty-compilation-database.md) if a database, from either path, comes out shorter than expected. - [Recipes](../guides/recipes/index.md) for the other tasks. [manpage]: https://github.com/rizsotto/Bear/blob/master/man/bear.1.md ��������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/tutorials/getting-started.md�����������������������������������������0000664�0000000�0000000�00000011430�15271777203�0024022�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: tutorial --> # Getting started with Bear By the end of this page you will have built Lua 5.4 under Bear, looked at the `compile_commands.json` it wrote, and used that file to run clangd and clang-tidy against the Lua source, the same way an editor or a CI lint job would. ## Install Bear Install Bear with your platform's package manager; see [Install Bear](../installation.md) for the commands. Then confirm it is on your `PATH`: ```sh bear --version ``` ## Get a real project Lua 5.4 is a good first target: a plain Makefile, no configure step, no dependencies beyond a C compiler and `make`. Download and extract it: ```sh curl -fsSL -O https://www.lua.org/ftp/lua-5.4.8.tar.gz tar xzf lua-5.4.8.tar.gz cd lua-5.4.8 ``` `lua.org`'s ftp directory does not change existing releases, so this download always matches this checksum: ```shell $ sha256sum lua-5.4.8.tar.gz 4f18ddae154e793e46eeab727c59ef1c0c0c2b744e7b94219710d76f530629ae lua-5.4.8.tar.gz ``` ## Build it under Bear ```sh bear -- make ``` Bear runs the build exactly as `make` would on its own, forwarding its output, and adds one thing: a `compile_commands.json` file in the current directory, one entry per compiled source file. Lua's Makefile recurses into `src`, so this produces 34 entries, one for each `.c` file that went into `lua` and `luac`. ## Look at the result ```sh cat compile_commands.json ``` Each entry looks like this: ```json { "file": "lapi.c", "arguments": [ "/usr/bin/gcc", "-std=gnu99", "-O2", "-Wall", "-Wextra", "-DLUA_COMPAT_5_3", "-DLUA_USE_LINUX", "-c", "-o", "lapi.o", "lapi.c" ], "directory": "/home/you/lua-5.4.8/src", "output": "lapi.o" } ``` `directory` is where the compiler ran, `file` is the source it compiled (relative to `directory`), `arguments` is the exact command as an array, and `output` is the object file it produced. Your own output will show your own resolved compiler path in `arguments[0]`; on a distribution that routes compilers through `ccache`, that path may be a ccache symlink instead of the compiler itself. There is one entry per compiled source and none for the headers Lua's sources include, such as `lapi.h`, because Bear only records commands the build actually ran, and the build never invokes the compiler on a header directly. For what Bear does and does not capture, and why, see [How Bear works](../understanding/how-it-works.md). ## Use the database: clangd `compile_commands.json` exists for tools like clangd to read, not to be read by people. Point clangd at one Lua source file and it logs the exact command it pulled from the database: ```shell $ clangd --check=src/lapi.c I[...] Compile command from CDB is: [/home/you/lua-5.4.8/src] /usr/bin/gcc -std=gnu99 -O2 -Wall -Wextra -DLUA_COMPAT_5_3 -DLUA_USE_LINUX -c -o lapi.o -resource-dir=... -- /home/you/lua-5.4.8/src/lapi.c ``` That is the whole hand-off: an editor with clangd support opened on this directory gets working "go to definition", completion, and diagnostics with no other setup. See [Set up clangd for a project without CMake](../guides/recipes/clangd-setup.md) for the editor side. ## Use the database: clang-tidy clang-tidy reads the same file to know how to parse each source it lints. Run it against one Lua source file, restricted to a single check so the output stays short: ```shell $ clang-tidy -p . -checks='-*,clang-analyzer-deadcode.DeadStores' src/lfunc.c 1 warning generated. lfunc.c:196:42: warning: Although the value stored to 'upl' is used in the enclosing expression, the value is never actually read from 'upl' [clang-analyzer-deadcode.DeadStores] 196 | while ((uv = L->openupval) != NULL && (upl = uplevel(uv)) >= level) { | ^ ``` That is a real finding in Lua's own source, produced by clang-tidy, not by Bear: Bear's job ends at writing `compile_commands.json`, and every tool built on Clang's tooling API, from editors to linters to refactoring tools, takes it from there. ## Next steps - [Generate compile_commands.json for a Makefile project](../guides/recipes/compile-commands-for-makefile.md) for variations on what you just did: autotools, incremental and parallel builds, recursive Makefiles. - [Set up clangd for a project without CMake](../guides/recipes/clangd-setup.md) if the editor side needs more than the default lookup. - [Recover compile_commands.json from a build log](compile-commands-from-a-build-log.md) if you cannot run the build yourself, only read its log. - [Bear produces an empty `compile_commands.json`](../guides/recipes/empty-compilation-database.md) if a real project's database comes out empty, and [Troubleshooting](../guides/troubleshooting.md) if it comes out wrong. - [How Bear works](../understanding/how-it-works.md) for the interception mechanism. ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/understanding/�������������������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0021213�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/src/understanding/how-it-works.md����������������������������������������0000664�0000000�0000000�00000016641�15271777203�0024117�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Diataxis type: explanation --> # How Bear works Producing a compilation database is two separate problems: capturing which commands a build runs, and recognizing which of those commands are compilations worth recording. Bear keeps the two apart. A capture stage produces a stream of execution events (program, arguments, working directory, environment); a semantic analysis stage turns the events it recognizes as compiler invocations into compilation-database entries. Combined mode runs both in one process; `bear intercept` and `bear semantic` run them as two separate steps over a file in between, and `bear parse-sh` replaces capture entirely by reconstructing events from text instead of running anything. For the commands themselves see [Getting started with Bear](../tutorials/getting-started.md) and the [Recipes](../guides/recipes/index.md). ## Two ways to capture a real build Interception observes the build as it runs. Bear has two mechanisms for it, and uses exactly one per invocation, chosen before the build starts. Which one is the default on your system is on its platform page - [Linux](../platforms/linux.md), [macOS](../platforms/macos.md), [Windows](../platforms/windows.md), [BSD](../platforms/bsd.md) - and [Configure Bear](../reference/configuration.md) explains how to force the other one. **Preload** injects a small shared library into every process the build starts, using the dynamic linker's library-preloading facility (`LD_PRELOAD` on Linux and the BSDs, `DYLD_INSERT_LIBRARIES` on macOS). The library intercepts the `exec` family of calls (and `posix_spawn`, `popen`, `system`) before they reach the kernel, reports the call, then lets it proceed unchanged. Because it hooks the mechanism every process uses to start another one, it sees every compiler invocation regardless of how the build found it, and the build itself is unaware anything is watching. It cannot see into a statically linked executable, because such a binary never goes through the dynamic linker in the first place. It cannot run at all on Windows, which has no preloading facility to hook and for which the library is not even built, nor on a macOS host with System Integrity Protection enabled, since SIP strips `DYLD_INSERT_LIBRARIES` from protected executables before it can take effect. **Wrapper** takes the opposite approach: instead of watching every process, it puts a reporting executable where the compiler is expected to be. Bear creates a `.bear/` directory holding one wrapper for each compiler it recognizes on `PATH` at startup, puts that directory first on `PATH`, and points compiler-selection variables (`CC`, `CXX`, and the like) at it. When the build launches what it thinks is the compiler, it launches the wrapper instead; the wrapper reports the call, then execs the real compiler found at its resolved, absolute path so the build sees no difference. This works on every platform, including Windows, but only for compilers the build actually reaches through `PATH` or a variable Bear resolved at startup: a build step that discovers the compiler on its own (a `./configure` probing for `cc`) needs to run under Bear too, so the wrapper is in place by the time the real build starts. Neither mechanism is a fallback for the other. Where preload is unavailable, Bear does not quietly switch to wrapper mode; forcing preload there is a startup error that names wrapper mode as the alternative, and the build does not run. ## From an intercepted call to the driver Both mechanisms report through the same channel: the process running the build (`bear-driver`, from the `bear-driver` binary) opens a TCP listener on the loopback interface, and every preload library instance or wrapper process connects to it and writes one length-prefixed JSON event per execution. This keeps the reporting side dependency-free and lets interception and semantic analysis live in separate processes when running as `bear intercept` and `bear semantic`, with the event stream as the file in between. The shared pieces of this runtime (the execution type, the wire encoding, environment-variable filtering) live in the `intercept` crate; the driver-side half that supervises the build, runs the TCP collector, and prepares the wrapper directory or preload environment lives in `intercept-supervisor`; the injected library itself is `intercept-preload`, and the wrapper executable's own code is `bear-wrapper`. ## Reconstructing events without running anything `bear parse-sh` takes a third path that skips capture entirely: instead of observing real execution, it parses shell command text, typically a build system's dry-run output (`make -n`) or a saved build log, and reconstructs the executions that text describes. It understands a documented subset of `sh` syntax and make's recursive-directory markers, and feeds what it reconstructs through the same semantic analysis as a real build. This is inherently lower fidelity: a dry run can omit commands a real build would issue, and the environment used to resolve bare executable names is the parsing environment, not the build's. It exists for builds that cannot be run at all, such as recovering a database from a CI log after the fact; interception remains the higher-fidelity default whenever the build can run. ## Semantic analysis Whatever produced the event stream, from here the processing is the same. Each event's executable name is checked against Bear's compiler definitions, including cross-compiler prefixes, version suffixes, and compiler launchers such as ccache and distcc that carry the real compiler in their own arguments; see [Supported compilers](../reference/supported-compilers.md) for how that recognition works. An event that names no recognized compiler produces nothing. A recognized invocation is then turned into zero, one, or several entries, depending on the compiler and what the invocation asked it to do: - most compilers treat each source file as its own translation unit, so an invocation naming several sources yields one entry per source, each stripped down to look like a command that compiled only that file; - a compiler that always compiles a whole target as one unit (Vala's `valac`) collapses to a single entry keyed on the first source, with every source kept in its arguments; - a compiler whose sources depend on each other within one invocation (Swift's `swiftc` in whole-module mode) produces one entry per source, but every entry keeps the complete invocation's arguments; - an invocation that only links, or only asks for information (`--version`, preprocessing, `--help`), produces no entry at all. From there, the entries pass through whatever the configuration requests: dropping generated sources by directory or filename, collapsing duplicate entries, resolving or leaving paths as they are, choosing between an arguments array and a command string, and optionally synthesizing entries for header files. [Configure Bear](../reference/configuration.md) explains each of these. ## Output The result is written as a JSON array of entries conforming to the [Clang JSON Compilation Database specification][jsoncdb], the format clangd, clang-tidy, and similar tools read to know how each source file is compiled. See also: [Getting started with Bear](../tutorials/getting-started.md) for the first run, the [Recipes](../guides/recipes/index.md) for specific tasks, and [Troubleshooting](../guides/troubleshooting.md) for a database that came out empty or wrong. [jsoncdb]: https://clang.llvm.org/docs/JSONCompilationDatabase.html �����������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/theme/�������������������������������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0016661�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/site/theme/head.hbs�����������������������������������������������������������0000664�0000000�0000000�00000001304�15271777203�0020256�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������{{!-- mdBook injects this file into every page's <head>. It exists for one reason: the Google Search Console ownership token for the https://rizsotto.github.io/Bear/ property. Verification breaks if the tag stops being served, so this stays for as long as the property does. Only site-wide, static <head> content belongs here; anything that varies per page (canonical link, meta description, JSON-LD) is written by scripts/postprocess-docs-site.sh, because mdBook exposes only the source .md path to this template and it cannot be rewritten to the rendered .html URL. --}} <meta name="google-site-verification" content="2labVImYflOjLm3EBVp6_qQqda7h_DcIdZNZJV2iiyo" /> ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/������������������������������������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0015755�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/�������������������������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0020074�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/README.md����������������������������������������������������0000664�0000000�0000000�00000021665�15271777203�0021365�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Bear dogfooding harness A non-automated, release-time test harness. It runs Bear's *installed release* binaries against a real open-source project, pinned to a known revision, inside a throwaway container, then validates the `compile_commands.json` Bear captured. It proves the end-to-end interception loop works on a real build and catches regressions and correctness divergences in Bear's output before a release. Nothing here touches your repo working tree, your devcontainer, or any cargo cache: the sources and toolchain live only inside the per-run container. This file is for running the harness. The design and per-check rationale (why each check works the way it does, and how to extend one) live in `SPEC.md`. ## Targets and checks The harness ships six targets. Each has a default way of validating a capture: | Target | Build system | Default validation | |---|---|---| | `zlib` | autotools | Golden regression (compare against a committed baseline) | | `curl` | CMake | Oracle (compare against the database CMake itself emits) | | `ffmpeg` | custom `configure` | None -- run with an on-demand check below | | `kernel` | Kbuild | None -- run with an on-demand check below | | `nasm` | x264, custom `configure` | None -- run with an on-demand check below | | `openmpi` | OSU benchmarks, autotools | None -- run with an on-demand check below | On top of the default, every target supports these on-demand checks, each selected by a flag. They need no baseline, so they run against any target: | Flag | Checks that... | |---|---| | `--determinism` | two identical builds produce equivalent captures | | `--invariants` | the capture is structurally well-formed (no empty/duplicate entries; entry count matches objects built) | | `--replay[=N]` | the compiler re-accepts a sample of recorded commands | | `--consumer[=N]` | a real clang tool (clang-tidy) can parse a sample of entries | `ffmpeg`, `kernel`, `nasm`, and `openmpi` have no default gate (no golden, no CMake oracle), so they are run *only* with the on-demand checks; a plain run against them is rejected with a pointer to those flags. `ffmpeg` and `kernel` are scale targets. The other two exercise specific toolchains on a real build: - `nasm` builds x264 (nasm-heavy, mixing nasm and gcc) to confirm Bear records a well-formed entry for each nasm assembly compile alongside the C ones. x264 compiles every bit-depth-dependent source twice (8-bit and 10-bit), so this target runs Bear with a config (`targets/nasm/bear.yaml`) that adds `arguments` to the duplicate match key, keeping both compiles; the `--invariants` entry count then matches the on-disk object count exactly. - `openmpi` builds the OSU Micro-Benchmarks with the Open MPI `mpicc`/`mpicxx` wrappers to confirm, in a real LD_PRELOAD build, that Bear records the wrapper command verbatim and collapses the wrapper+gcc-child pair to a single entry per translation unit (`--invariants`' no-true-duplicates and entry count). ## Prerequisites - **Rootless Podman** (developed against podman 5.8.3). The build container runs with `--systemd=always` so Bear's cgroup-based process-tree teardown works; this needs the host's delegated cgroup controllers (`/etc/systemd/system/user@.service.d/delegate.conf` with `Delegate=cpu cpuset io memory pids`). - **Free disk** on the podman graphroot (zlib needs ~2 GiB, curl ~4 GiB for the images plus the build tree). The harness preflight checks this against each target's `MIN_FREE_KIB`. The first run builds two cached images (`bear-dogfood-base:<sha>` and `bear-dogfood-<target>:<sha>`, tagged by the Bear commit under test); later runs reuse them. The base build compiles Bear from `git archive HEAD`, so the first run takes a few minutes. ## How to run All commands are run from the repo root. ```sh # Default validation for a target (golden for zlib, oracle for curl). tests/dogfooding/run.sh # zlib is the default target tests/dogfooding/run.sh curl # On-demand checks (any target; these skip the golden/oracle gate). tests/dogfooding/run.sh --determinism zlib # build twice, compare tests/dogfooding/run.sh --invariants curl # structural well-formedness tests/dogfooding/run.sh --replay zlib # replay 20 recorded commands tests/dogfooding/run.sh --replay=30 curl # replay a sample of 30 tests/dogfooding/run.sh --consumer curl # feed 20 entries to clang-tidy tests/dogfooding/run.sh --invariants kernel # scale target, checks only # Modifiers. tests/dogfooding/run.sh --label rc1 curl # name the run (results/curl/rc1/) tests/dogfooding/run.sh --keep # keep the container for inspection tests/dogfooding/run.sh --metrics ffmpeg # also profile bear-driver (see below) # Prove the checks actually catch faults, without a container (fast). # This one runs the comparator on the host, so build it first: # cargo build --release -p bear-test-tools --bin cdb-compare tests/dogfooding/internal/selftest.sh ``` Run artifacts land under `results/<target>/<label>/` (git-ignored). Goldens live under `goldens/<target>/` and are tracked. ## Outcomes and exit codes Every run prints one final `OUTCOME:` line and exits with: | Outcome | Exit | Meaning | |---|---|---| | PASS | 0 | The check passed. No regression. | | FAIL | 1 | The check failed -- a real behavioral change or defect in Bear's output. Read the artifact for that check (below) and either fix Bear or, for the golden, rebless. | | INCONCLUSIVE | 2 | The target build failed for its own reasons (source fetch, sha, network, configure/make, OOM), or a sampling check verified nothing (every sampled entry was inconclusive). Not a Bear regression. | | ERROR | 3 | Harness or Bear-infrastructure failure: podman missing, disk/digest preflight, base or target image build, an empty capture (a `libexec` / `INTERCEPT_LIBDIR` mismatch), or an oracle that matched 0 units. | ## Understanding a FAIL Each check writes a human-readable artifact under `results/<target>/<label>/`. On a FAIL, start there: | Check | What a FAIL means | Read | |---|---|---| | Golden | The capture drifted from the committed baseline. | `git diff` on the golden; if the change is intentional, rebless (below) | | Oracle | Matched units diverge from CMake's own database. | the `matched but differing` section of `oracle-report.txt` | | Determinism | Two identical builds produced different captures -- real non-determinism or a race in Bear. | `determinism-diff.txt` | | Invariants | Bear produced a malformed database (empty/duplicate entry, or the entry count is off from objects built). | `invariants-report.txt` | | Replay | A recorded command would not replay -- a malformed entry (wrong cwd or mangled flag). | `replay_result` | | Consumer | clang-tidy rejected a well-formed entry -- a semantically broken database (e.g. a wrong or missing `-I`). | `consumer_result` | ## Reblessing the golden The golden is a frozen, normalized capture -- a change-detector, not a proof of correctness. When a behavior change is *intentional* (Bear deliberately changed the flags it records, or the pinned zlib/base moved), regenerate it on purpose: ```sh tests/dogfooding/run.sh --rebless zlib ``` This runs the full pipeline and then, instead of gating, overwrites `goldens/zlib/compile_commands.json` with the fresh normalized capture and reports "reblessed" (exit 0). The new golden is left in the working tree for you to: 1. Inspect the diff (`git diff tests/dogfooding/goldens/zlib/`) and confirm the change is the one you intended. 2. Commit it with a message explaining *why* the recorded behavior changed. Reblessing is never automatic -- a normal run only ever reads the golden and fails on mismatch, so an unintended change cannot silently overwrite it. Oracle targets (curl) have no golden and reject `--rebless`; the CMake reference renews itself when curl updates. ## Metrics: profiling bear-driver `--metrics` additionally profiles `bear-driver`'s CPU and memory with [rprof](https://github.com/rizsotto/rprof) while it builds the target, and keeps the full JSONL at `results/<target>/<label>/metrics.jsonl` (determinism yields `metrics.run1.jsonl` / `metrics.run2.jsonl`). It layers on any run: ```sh tests/dogfooding/run.sh --metrics ffmpeg # profiled build, metrics only tests/dogfooding/run.sh --metrics --invariants curl # invariants plus a profile tests/dogfooding/run.sh --metrics --determinism zlib # two builds, two profiles ``` The harness only *collects* the file; render and compare runs yourself with `rprof view`. It is harmless when `--metrics` is not given. ## What the harness does NOT do - It does not modify the repo working tree, the devcontainer image, or any cargo cache. Sources and toolchain live only in the throwaway container. - It does not leave its per-run container behind (unless `--keep`). It does keep the two cached images so reruns are fast; remove them when done with `podman rmi bear-dogfood-<target>:<sha> bear-dogfood-base:<sha>`. - It does not parse or summarize the metrics profile -- that is `rprof view`'s job. ���������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/SPEC.md������������������������������������������������������0000664�0000000�0000000�00000102162�15271777203�0021152�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Dogfooding harness specification This is the maintainer's reference for the harness under `tests/dogfooding/`: the `dogfood-*` contracts it satisfies, the rationale behind each check, and how to extend it. For running the harness, see `README.md`. These are contracts on the TEST HARNESS, not on Bear, so they live here and NOT under `docs/requirements/` (which is reserved for Bear's own contracts). Scope: non-automated, run by the maintainer at release time. Bear's installed release binaries are run against a real project inside a throwaway container, and the captured compilation database is validated. Sources and toolchain live only in the container, never in the repo or the devcontainer image (the host-orchestrated Podman model: the orchestrator is POSIX `sh` on the host, each target runs in a per-project throwaway container). ## How the contracts are organized - **Run model** -- how a run is built and how outcomes are classified: `dogfood-run-containerized`, `dogfood-fixed-paths`, `dogfood-pinned-target`, `dogfood-preflight`, `dogfood-build-failure-taxonomy`. - **Per-target default validation** -- chosen by a `VALIDATION` selector in the target's `config.env`: `golden` (zlib) or `oracle` (curl). `none` (ffmpeg, kernel) means no default gate; the target is run only with an on-demand check. - **On-demand checks** -- selected by flag, need no maintained baseline, run against any target: `dogfood-determinism`, `dogfood-invariants`, `dogfood-replay`, `dogfood-clang-consumer`. Each builds+captures once (twice for determinism) and skips the golden/oracle gate. - **Fault demonstrations** -- each check must demonstrably catch a fault: `dogfood-injected-fault` (host-side `selftest.sh`) and the in-container consumer fault demo. - **Metrics** -- `dogfood-metrics-collect`, an additive profiling modifier. All comparison, normalization, and gating is done by `cdb-compare` (package `bear-test-tools`), so the harness needs no `jq`. `run.sh` runs the copy that ships inside the target image (dogfood-run-containerized), so it needs no host binary and there is no glibc-mismatch risk from copying the image's dynamically linked comparator onto an arbitrary host. The one exception is `selftest.sh`, which never builds an image and so runs a locally built `target/release/cdb-compare` on the host -- built and run in the same environment, so no cross-libc concern. ## dogfood-run-containerized The suite builds a real project inside a throwaway container using the installed, release Bear binaries, leaving the repo working tree and the devcontainer image unmodified. A release/install build is required (not a debug tree) so `libexec.so` sits at the path the binary resolves via `INTERCEPT_LIBDIR`; a mismatch makes Bear run but capture nothing, so the harness verifies interception actually occurred (non-empty capture) before trusting a run. Implementation: Bear is built inside the base image from `git archive HEAD` (committed files only) and installed with `scripts/install.sh` to `PREFIX=/opt/bear` (default `INTERCEPT_LIBDIR=lib`), giving the layout `/opt/bear/bin/bear` -> `/opt/bear/libexec/bear/bin/bear-driver` + `../lib/libexec.so`. The base carries no Rust toolchain (multi-stage build). The comparator stays in the container model too. The base image also installs `/opt/bear/bin/cdb-compare`, and every host-side gate (golden, oracle, determinism, invariants, rebless) runs it there rather than on the host, via the `cdb_compare` helper in `lib.sh`: it `podman run`s the target image with the run directory bind-mounted read-only at `/run`, passing `/run/<file>` paths, and the report/exit code propagate back to the host. So `run.sh` needs no host `cdb-compare` (removing the copy-out fragility of a dynamically linked binary), and the comparison is the same version that shipped with the Bear under test. The golden compare stages the committed golden into the run directory first, so one read-only mount covers both sides; rebless captures `normalize`'s stdout on the host and installs it atomically. The in-container `sample` for replay/consumer already used this image binary. ## dogfood-fixed-paths The build runs at fixed, well-known container paths so absolute paths in `directory` (and in arguments) do not vary between runs, making command lines reproducible and letting the golden check pass with little or no normalization. Implementation: zlib builds in-tree at the fixed path `/src` (its `./configure` has no usable out-of-tree mode). The config pins `SRC_DIR=/src`. ## dogfood-pinned-target A dogfooding configuration is (base image digest, target source revision, target build type); all are pinned so the golden stays stable. An unpinned base would let a package update change the compiler under a fixed golden; an unpinned build type would change the recorded flags. Release is the default build type. Implementation: `targets/zlib/config.env` pins the fedora:44 base by digest, the zlib 1.3.1 tarball by URL + sha256, and `BUILD_TYPE=Release` (`CFLAGS="-O3 -DNDEBUG"`). ## dogfood-preflight Before launching any container, the suite verifies free disk against a per-target minimum, that the pinned image digest is present/pullable, and that the image carries a working Bear (the non-empty-capture check). It exits non-zero with a clear diagnostic on any failure, so a run never starts only to leave a torn scratch behind. Implementation: `run.sh` step 1 checks free disk on the podman graphroot (`podman info` + `df -Pk`) against `MIN_FREE_KIB` and resolves/pulls the pinned digest; step 4 runs a trivial `bear -- gcc -c` and asserts the capture is non-empty before the real build. ## dogfood-build-failure-taxonomy The harness distinguishes a target build failure (network, source fetch, OOM, missing target dep, configure/make error) from a Bear/harness failure, and reports them as distinct outcomes. A target build that fails for its own reasons is "inconclusive", not a Bear regression. Implementation: four outcomes with distinct exit codes - `PASS=0`, `FAIL=1` (golden regression, or oracle matched-TU divergence), `INCONCLUSIVE=2` (target build failed for its own reasons: target image build, configure/make), `ERROR=3` (harness or Bear-infra: podman missing, disk/digest preflight, base or target image build, empty capture, an oracle that matched 0 TUs). `run.sh` prints one final `OUTCOME:` status line. ## dogfood-golden-regression For the pinned target, the suite compares Bear's output against a tracked golden using `cdb-compare compare` (order-independent multiset equivalence) and flags any change. The golden is a frozen, full normalized compilation database emitted with `cdb-compare normalize --sort`, committed under `tests/dogfooding/goldens/` (NOT under the git-ignored `results/`). The comparison binary is reused as-is; no hash-manifest tooling is added. The golden is a change-detector, not an independent proof of correctness. Implementation: `goldens/zlib/compile_commands.json` is the frozen golden; the gate is `cdb-compare compare <golden> <fresh>` run in the target image (via `cdb_compare`, dogfood-run-containerized), with the golden staged into the run directory so one read-only mount covers both sides. No normalization flags are used (same pinned image + fixed `/src` => raw multiset matches); if a benign compiler-path diff ever appears, add `--substitute-compiler cc` to both the bless and the check, consistently. ## dogfood-golden-rebless There is a documented, deliberate procedure to regenerate the golden when a behavior change is intentional, so re-blessing is explicit and reviewable rather than automatic. Implementation: `run.sh --rebless <target>` runs the full pipeline, then instead of gating runs `cdb-compare normalize --sort <fresh>` in the target image (via `cdb_compare`), captures its stdout on the host, and installs it atomically at the golden path, reporting "reblessed" for the maintainer to review and commit. See README.md for the maintainer-facing procedure. ## dogfood-oracle-cmake For a CMake-native target, the suite compares Bear's output against the database CMake itself emits (`CMAKE_EXPORT_COMPILE_COMMANDS=ON`), scoped to the intersection of translation units. The check passes when matched TUs have equivalent flags under normalization. Entries present in only one database are logged as "extras", never failures: CMake lists configured TUs with configure-time flags, while Bear records the actual make-time command, so a whole-database equality would be pure noise. The oracle renews itself when the target updates; no hand-maintained baseline. The oracle target is curl (CMake- native, small); zlib stays the golden target (autotools). Implementation: `targets/curl/config.env` sets `VALIDATION=oracle` and pins the fedora:44 base by digest, curl 8.11.1 by URL + sha256, and `BUILD_TYPE=Release`. The in-container command (dogfood-fixed-paths) configures out-of-tree (source `/src`, build `/build`) with `-DCMAKE_EXPORT_COMPILE_COMMANDS=ON` and the optional dependencies turned off (no TLS/nghttp2/zlib/zstd/brotli/psl/idn/ssh/ ldap), so curl builds with just cmake+gcc+make. The CMake *configure* step is NOT wrapped by Bear (it is not a compile); only `cmake --build` is, so Bear's capture lands at `/out/compile_commands.json` and CMake's reference database is copied to `/out/oracle.json`. Both are pulled out with `podman cp`. The comparison is done entirely by the image's `cdb-compare` (via `cdb_compare`) - no allow-list file, no shell JSON surgery: ```sh cdb-compare compare --intersection --substitute-compiler cc \ --output-from-o --drop-dependency-flags <bear.json> <cmake.json> ``` `tests/tools/SPEC.md` documents what each flag does; the reason each is needed *here* is: - `--output-from-o` - Bear encodes `output` relative to `directory` and CMake relative to the build root, so TUs must match by source plus the absolute object path rather than the raw `output` string; it also keeps a source compiled into several targets (shared lib, tool) distinct. - `--drop-dependency-flags` - the make-time compile carries the `-M*` depfile flags and CMake's configure-time export omits them (see dogfood-divergence-report); they touch only the `.d` side-file. - `--substitute-compiler cc` - absorbs any compiler-driver path difference between the two producers. - `--intersection` - gates on the `differing` set alone, and its non-vacuity clause turns a comparison that matched 0 TUs into a failure rather than a green no-op. Exit 0 = matched TUs equivalent (PASS); exit 1 = a real matched-TU divergence (FAIL). ## dogfood-divergence-report The Bear-only and CMake-only "extras" are reported for review, never a failure. On the pinned build there are 0 Bear-only and ~156 CMake-only extras (CMake lists every configured TU, including ones a given build target does not actually compile). The one known-benign argument difference on matched TUs - the dependency-file generation flags (`-MD`/`-MMD`/`-MP` and the argument-consuming `-MF`/`-MT`/`-MQ`/`-MJ`) - is removed by `cdb-compare --drop-dependency-flags` before the gate. These flags only control the build system's `.d` side-file, never the object or how a Clang tool parses the TU, so dropping them is a tested, documented operation of the comparator rather than a hand-maintained shell allow-list. On the pinned curl build this group is the ENTIRE matched-but- differing set (221/221 TUs); with it dropped, `differing` is empty and the gate passes. `run.sh` writes the comparator's full report (its extras lists plus a `summary:` line) to `results/<target>/<label>/oracle-report.txt`, with a machine-readable `oracle-compare.json` alongside. There is no rebless for oracle targets - the oracle is self-renewing, so `--rebless` is rejected for them. If a future oracle target exhibits a different benign argument difference that `--drop-dependency-flags` does not cover, extend the comparator with a tested rule rather than reintroducing a shell allow-list - the comparison logic stays in one unit-tested place (see "Extending cdb-compare" below). ## dogfood-determinism The suite can run the same target twice and assert Bear's two outputs are equivalent under `cdb-compare`. Any difference indicates non-determinism or a race in Bear itself; the build is its own reference, no golden and no oracle required. This is target-agnostic: it runs for any target, golden or oracle. Implementation: `run.sh --determinism <target>` runs the shared preflight, base + target image builds, and the non-empty-capture smoke (exactly as a normal run), then runs the target build TWICE in two distinct fresh throwaway containers off the same pinned image (`build_and_capture` in `lib.sh`, invoked once per container - the same body the normal single-run path uses, so the build-failure taxonomy is identical), and compares the two captures with `cdb-compare compare <run1> <run2>` (via `cdb_compare`, in the target image). NO normalization flags are used: the fixed build paths (`/src`, `/build`) make the two captures multiset-equivalent at the source, and `cdb-compare` is order-independent, so build parallelism does not matter. The golden/oracle gate is SKIPPED entirely (determinism is its own check). `--determinism` with `--rebless` is rejected (ERROR: no golden is involved); `--inject-fault` outside `--determinism` is likewise an ERROR. Outcome (reusing the existing taxonomy): both captures equivalent => PASS; captures differ => FAIL (real Bear non-determinism / a race), with the `cdb-compare compare` diff saved to `results/<target>/<label>/determinism-diff.txt` (and `.json`); either build failing for its own reasons => INCONCLUSIVE; podman/infra/empty-capture => ERROR. Both captures are written as `compile_commands.run1.json` and `compile_commands.run2.json`. Determinism is verified on both zlib and curl. Self-test (the check must demonstrably catch a fault): `run.sh --determinism --inject-fault <target>` perturbs the SECOND build with an extra compiler flag so the two builds legitimately diverge, and the check is shown to FAIL. The fault is injected as a real, different second build, NOT by editing captured JSON by hand: `run.sh` passes a non-empty `INJECT_CFLAGS` (an extra `-D...` macro) into the second container only, and each target's `config.env` threads `${INJECT_CFLAGS:-}` into that build's compiler flags (`CFLAGS` for zlib's configure, `CMAKE_C_FLAGS` for curl's cmake). On a normal run and on determinism run 1 the value is empty, a no-op. The FAIL run's `determinism-diff.txt` shows the injected flag present in run 2's arguments and absent in run 1's. Both directions are verified for zlib and curl. Scope boundary: the DROPPED, DUPLICATED, and CORRUPTED-ENTRY faults are NOT determinism's territory - they belong to `dogfood-invariants` (dropped/duplicated/empty-arguments) and `dogfood-replay` (corrupted `directory`), both specified below. Determinism's own fault model is a divergent second build, which is exactly what `--inject-fault` exercises. ## dogfood-invariants For any target, the suite asserts structural invariants on Bear's single capture: every entry has non-empty `arguments`, every entry has a non-empty `directory` (a blank `directory` is non-replayable - a `cd ""` consumer lands in the wrong tree), there are no TRUE duplicates (identical `file` + `output` + normalized `arguments` - a source compiled into different outputs with different flags is legitimate, not a duplicate), and the entry count is within a configured tolerance of the number of object files the build produced. Each invariant is independently reported. No golden, no oracle, no maintained baseline. Implementation: `run.sh --invariants <target>` runs the shared preflight, image builds, and smoke, builds+captures once (`build_and_capture`), then gates on the exit code of one in-image `cdb-compare invariants --drop-dependency-flags --expected-objects <N> --tolerance <PCT> --format human <capture>` (via `cdb_compare`). The whole structural check - non-empty-arguments, non-empty-directory, no-true-duplicates, and the entry-count band - lives in the unit-tested comparator (defined in `tests/tools/SPEC.md`); the harness only supplies `<N>` and `<PCT>` and gates the exit code (no JSON parsed in shell). Exit 0 => PASS; exit 1 => FAIL (Bear produced a malformed CDB); a non-1 error or a build/infra failure maps to ERROR/INCONCLUSIVE via `build_and_capture`. The human report is saved to `results/<target>/<label>/invariants-report.txt`. The object count `<N>` is taken IN the container before teardown, by a per-target `OBJECT_COUNT_CMD` (config.env) written to `/out/object_count` and pulled out. The instrument is per-target because "objects produced" is not always "*.o files still on disk": - curl / ffmpeg leave every object on disk, so the default `find $OBJECTS_DIR -name '*.o' | wc -l` is exact; - zlib's in-tree `make` compiles each source twice (static + PIC) and DELETES the PIC objects at link time (19 survive, 34 produced), so it counts make's OWN dependency graph (`make -Bn`); - the kernel aggregates objects into built-in.a / linked .o (a `find` overcounts) AND a `make -Bn` dry-run does not enumerate its recursive compiles (it counts ~0), so it counts Kbuild's per-compile `.foo.o.cmd` files instead. All are build-system-native, cleanup-independent, Bear-independent. `<PCT>` is the per-target `OBJECT_TOLERANCE_PCT`. Graceful degradation: the entry-count cross-check is opt-in and must never block the always-on structural checks. If `OBJECT_COUNT_CMD` cannot produce a number (0 / empty), `run.sh` warns and runs `cdb-compare invariants` WITHOUT `--expected-objects` (entry-count skipped), still gating on non-empty-arguments, non-empty-directory, and no-true-duplicates. A target whose independent object count is genuinely hard still gets the structural invariants. Scale: validated on two larger gate-less (`VALIDATION=none`) targets - ffmpeg (~1945 TUs, exact: entries == objects == 1945) and the Linux kernel (x86_64 defconfig, ~3000 TUs: Kbuild `.cmd` count 2991 vs entries 3004, a 0.4% match). The entry-count band is thus a real coverage cross-check at scale (did Bear capture ~every compile?), not just a small-target nicety; entries == objects exactly on zlib (34) and curl (221). The kernel run also proves Bear stays deterministic and its process-tree teardown holds under a high-`-j` build. ## dogfood-replay The suite takes a sample of Bear's entries and replays each recorded command in its recorded `directory` with `-fsyntax-only` appended, asserting the compiler accepts the recorded arguments. A command that fails to replay indicates a malformed entry (wrong cwd, missing or mangled flag). Sampling size is configurable. In a throwaway container a generated header may no longer exist, so a replay failure for a TU that depends on a missing input is *inconclusive*, not a Bear failure; the sampler preferentially selects TUs whose flags do not reference build-dir includes to keep replay meaningful. Implementation: `run.sh --replay[=N] <target>` (default N=20; also `--replay N`) runs the shared pipeline, builds+captures once, and replays INSIDE the build container as part of the same `podman run` - the recorded sources and generated headers exist there only before teardown. The replay loop is one POSIX function (`replay-loop.sh`) read into the in-container script; the in-image `/opt/bear/bin/cdb-compare sample <capture> --count N --build-dir <BUILD_DIR>` selects up to N replayable entries (build-dir-aware ranking is defined in `tests/tools/SPEC.md`) and emits one shell-quoted replay line per entry (`directory` then argv). Each is replayed as `( cd "$dir" && "$@" -fsyntax-only )` and tallied: - **OK** - the compiler accepted the recorded arguments. - **INCONCLUSIVE** - the failure stderr matches a missing-file diagnostic ("No such file" / "file not found" / "not found") - a build-time input gone after teardown. - **FAIL** - any other failure, including a `directory` that does not exist (a corrupted-directory fault, caught before the compile by a `-d` test). The function writes its tally and any failing commands to `/out/replay_result` and its return code to `/out/replay_rc`, both pulled out and gated on without parsing JSON. `BUILD_DIR` comes from config.env (curl `/build`; zlib has no out-of-tree dir, so its `BUILD_DIR` is `/src`). Gate (non-vacuity, mirroring the oracle): any real FAIL => FAIL (a malformed entry); all OK with inconclusive allowed => PASS; EVERY sampled entry inconclusive (nothing actually verified) => INCONCLUSIVE (replay must not pass vacuously). On the pinned builds all 20 sampled entries replay OK for both zlib and curl (0 FAIL, 0 INCONCLUSIVE). ## dogfood-clang-consumer Feed a sample of the captured database to a REAL clang-tooling consumer over the recorded entries, IN-container, and assert the tool accepts each. This is the only check that validates the database WORKS in the tool it exists for - catching a structurally-valid but semantically-broken DB (a wrong/missing `-I`, a dropped `-x`, a missing sysroot) that the structural invariants and the replay check both miss. Same shape as dogfood-replay: reuse `cdb-compare sample`, categorize OK / FAIL / INCONCLUSIVE, non-vacuous (an all-inconclusive or empty sample is INCONCLUSIVE, never a vacuous PASS). Implementation: `run.sh --consumer[=N] <target>` (default N=20; also `--consumer N`) runs the shared pipeline, builds+captures once, and runs the consumer INSIDE the build container as part of the same `podman run` - the recorded sources and generated headers exist there only before teardown, AND the clang tooling lives in the image (the host has no clang). The consumer loop is one POSIX function (`consumer-loop.sh`) read into the in-container script, mirroring how `replay-loop.sh` is wired. It is additive/standalone like `--replay`: mutually exclusive with the other check modes, allowed on `VALIDATION=none` targets, and gated on an in-container return code mapped to PASS/FAIL/INCONCLUSIVE/ERROR. The consumer (chosen empirically, not guessed - the targets build with GCC, so clang tooling must consume a gcc-built database) is: ```sh clang-tidy --checks='-*,bugprone-assert-side-effect' --allow-no-checks \ -p <cdb-dir> <file> ``` clang-tidy reads the gcc-recorded command from the CDB at `-p <cdb-dir>` and mangles the driver-incompatible flags itself (dropping GCC-only flags clang does not know) - that cross-compiler consumption is precisely what the check validates. A single real check forces a full front-end parse (with NO check enabled clang-tidy short-circuits and never parses, so the gate would be vacuous); `--allow-no-checks` keeps the run from erroring should that check name disappear from a future clang-tools. `clangd --check` was rejected as the consumer: it runs clangd's INTERNAL tweak/refactoring self-tests over the TU and reports their failures as "N errors" with a non-zero exit even when the AST built perfectly (every curl TU "built the AST" yet reported up to 60 such bogus errors), making its exit code useless as a database-validity signal. clang-tidy with no lint check does a pure parse, so the only `error:` diagnostics are genuine front-end errors. The consumer also passes `--extra-arg=-Wno-error`: a recorded `-Werror` (the kernel's `CONFIG_WERROR=y`) would promote clang-only warnings on a gcc-clean TU to errors, which says nothing about the database. Categorization (keyed on clang-tidy's `error:` diagnostics): - **OK** - zero `error:` diagnostics: clang-tidy parsed the TU and built the AST from Bear's recorded command. Warnings are expected on a gcc TU under clang and are NOT a defect. - **FAIL** - one or more `error:` diagnostics, INCLUDING a not-found `#include`. This relies on the loop's premise: the consumer runs in the LIVE post-build container, so every header the build saw (source-tree AND generated) is still on disk. A "file not found" therefore is NOT a generated-header-gone artifact (that only happens after teardown); it means Bear's recorded include paths do not let the tool find a header the build found - the wrong/missing-`-I` defect the check exists for. - **INCONCLUSIVE** - the TU source itself is no longer on disk (the one genuine missing-input case, since a TU source, unlike a header, can be a generated file). Kept narrow so the not-found-header FAIL signal is not diluted. Gate (mirroring the oracle / replay non-vacuity): any FAIL => FAIL; all OK (inconclusive allowed) => PASS; EVERY sampled entry inconclusive (nothing consumed) => INCONCLUSIVE. The tally and any rejected entries are written to `results/<target>/<label>/consumer_result`, the return code to `consumer_rc`, both pulled out and gated on without parsing JSON. Signal quality on a gcc-built target: meaningful, not noise-dominated. On the pinned curl build all 20 sampled entries come back OK (`ok=20 fail=0 inconclusive=0`): clang-tidy consumes the gcc-recorded database cleanly, parses every TU without error, and only the deliberate fault flips a verdict. The build-dir-aware sampler (`--build-dir <BUILD_DIR>`) avoids generated-header-heavy TUs, which keeps good entries from tripping the not-found FAIL. Boundary to scrutinize (the OK<->FAIL line): because a not-found header is FAIL, a future target whose sampled TUs legitimately depend on a generated header the build CLEANS before this loop runs would false-FAIL. That does not happen for curl/zlib (the build leaves its tree intact and the sampler prefers source-tree TUs); it is the honest trade - keeping not-found as INCONCLUSIVE would have masked the very wrong-`-I` defect the check is for. If such a target appears, narrow the FAIL rule to "not-found header NOT reachable from a build-dir include", rather than blanket-INCONCLUSIVE-ing not-found. Image requirement: the base image installs `clang clang-tools-extra` in its final stage (a CONSUMER tool, like `cdb-compare`, NOT a toolchain - so it goes in the final stage, not the toolchain-only builder), with a `clang-tidy --version` sanity in the final stage. Every per-target image layers on this base, so the check needs no per-target change; `BUILD_DIR` from config.env (reused as the sampler's `--build-dir`) is the only per-target input. ### Consumer fault demonstration The check must catch a deliberately broken entry: a stripped `-I` the tool then rejects. Unlike the replay bad-directory fault (a host-side fixture in `selftest.sh`), this demo MUST run where clang exists - IN the build container - because the host has no compiler. It is a small, clearly-labeled in-container one-off, `consumer-fault-demo.sh`, run by the maintainer against the curl target image: ```sh podman run --rm --systemd=always \ -v tests/dogfooding/internal/consumer-loop.sh:/consumer-loop.sh:ro,Z \ -v tests/dogfooding/internal/consumer-fault-demo.sh:/demo.sh:ro,Z \ bear-dogfood-curl:<sha> sh /demo.sh ``` It builds curl (so the capture, sources, and headers exist), takes a KNOWN-GOOD entry the consumer accepts (`altsvc.c` => OK), strips the `-I/src*` flags that TU needs to find `curl/system.h`, and shows the SAME shipped `consumer_cdb` function now reports FAIL for that entry. No JSON is hand-edited beyond removing the one include group; the fault is a real, broken database entry. The demo exits 0 iff the good entry was OK (rc 0) and the broken entry was caught (rc 1): ``` good: rc=0 (ok=1 fail=0 inconclusive=0) bad: rc=1 (ok=0 fail=1 inconclusive=0) diag: /src/lib/curl_setup.h:330:10: error: 'curl/system.h' file not found DEMO PASSED ``` ## dogfood-injected-fault The invariants and replay checks must demonstrably catch injected faults - a dropped entry, a duplicated entry, an empty/blank field, and a corrupted `directory`. This is demonstrated WITHOUT a container by `selftest.sh` against tiny, committed, hand-written fault fixtures under `faults/` (so the fault is unambiguous and not the product of fragile in-shell JSON surgery on a real capture): - `faults/duplicate.json` - two byte-identical entries => `cdb-compare invariants` FAILS its no-true-duplicates check. - `faults/empty-arguments.json` - an entry with `"arguments": []` => invariants FAILS non-empty-arguments. - `faults/blank-directory.json` - an entry with `"directory": ""` => invariants FAILS non-empty-directory (a `cd ""` consumer lands in the wrong tree, so the entry is non-replayable). Distinct from `bad-directory.json`, which holds a nonexistent path to exercise REPLAY. - `faults/undercount.json` - 2 entries asserted against `--expected-objects 3 --tolerance 0` => invariants FAILS entry-count (the "dropped entry" fault). - `faults/bad-directory.json` - a valid entry whose `directory` does not exist => the replay loop FAILS (the recorded compile can never run from where Bear claims it did - the "corrupted directory" fault). `faults/tu/hello.c` is the trivial TU it nominally compiles. `selftest.sh` runs the host `cdb-compare invariants` against the invariants fixtures and the SAME `replay-loop.sh` function (host-side) against the bad-directory fixture, asserts each check exits non-zero (the fault was caught), adds a control that an honest CDB passes (no false positive), and prints a clear pass/fail. It needs no container, so it is fast. It is invoked as `tests/dogfooding/internal/selftest.sh`. Unlike the containerized gates, this is the one path that never builds an image, so it runs a locally built `target/release/cdb-compare` on the host (built with `cargo build --release -p bear-test-tools --bin cdb-compare`) - the sole host- binary user in the harness. Because it is built and run in the same environment, there is no cross-libc concern; if the binary is missing it exits with that build hint. ## dogfood-metrics-collect While Bear builds a target, the suite can profile `bear-driver`'s CPU and memory with rprof (github.com/rizsotto/rprof) and keep the full capture as an artifact. The profiled subject is `bear-driver` SPECIFICALLY - the component that accumulates every intercepted execution and serializes the compilation database, so its memory is the load-bearing scaling signal; the short-lived per-exec preload and `bear-wrapper` are not profiled. This is intentionally NOT automated: the harness only COLLECTS the rprof JSONL - it never parses or summarizes it. The maintainer runs `rprof view` afterward to render and compare runs. Implementation: rprof (pinned to its `v1.0.0` release) is baked into the base image via the existing multi-stage build - compiled in the toolchain-carrying builder, the ~1 MB static binary copied into the toolchain-free final on PATH. The `--metrics` flag turns the build into `rprof run -o /out/metrics.jsonl -- bear -- <build>`. Because the `bear` entry script execs `bear-driver` (same PID) and rprof measures the single launched process WITHOUT following descendants, this profiles exactly `bear-driver` and excludes the build's compiler processes - no PID gymnastics needed. The whole JSONL is copied to `results/<target>/<label>/metrics.jsonl` (determinism's two builds yield `metrics.run1.jsonl` / `metrics.run2.jsonl`). `--metrics` is an additive modifier: it layers on any mode (golden / oracle / determinism / invariants / replay), and on a gate-less (`VALIDATION=none`) target with no check mode it stands alone as a profiled build whose deliverable is the artifact. The metrics file is advisory - a missing file warns, never fails - and the wrapping is runner-controlled (rprof in the base is harmless when unused). ## Adding a new target A target is a directory under `targets/<name>/` with a `config.env` and whatever in-container build glue it needs. `config.env` fields (see the existing targets for working examples): | Field | Purpose | |---|---| | `VALIDATION` | `golden`, `oracle`, or `none`. Omit to default to `golden`. `none` means no default gate; the target runs only with an on-demand check. | | `BASE_IMAGE` | The base image pinned by digest (dogfood-pinned-target). | | `<NAME>_VERSION` / `<NAME>_URL` | The source revision, pinned by URL + sha256. | | `BUILD_TYPE` | Release by default; pins the recorded flags. | | `SRC_DIR` / `BUILD_DIR` / `OBJECTS_DIR` | Fixed container paths (dogfood-fixed-paths). `BUILD_DIR` is also the sampler's `--build-dir` for replay/consumer. | | `OBJECT_COUNT_CMD` | Build-system-native object count for the invariants entry-count cross-check. Omit to use the default (`find` for `*.o` under `OBJECTS_DIR`). See dogfood-invariants for why this is per-target. | | `OBJECT_TOLERANCE_PCT` | Tolerance band for entry-count. | | `MIN_FREE_KIB` | Preflight free-disk minimum on the podman graphroot. | | `TARGET_BUILD_CMD` | The in-container build command; thread `${INJECT_CFLAGS:-}` into its compiler flags so `--inject-fault` works (dogfood-determinism). | For a golden target, add `goldens/<name>/compile_commands.json` by running `run.sh --rebless <name>` once and committing the result. For an oracle target, the build must emit its own reference database and the harness compares against it -- no committed baseline. A `none` target needs neither; it is exercised only by `--invariants` / `--determinism` / `--replay` / `--consumer`. ## Extending cdb-compare Every comparison, normalization, and structural check lives in the `cdb-compare` binary (package `bear-test-tools`, `crates`/`tests/tools`), not in shell. That is deliberate: the logic is unit-tested in one place. When a target surfaces a new benign difference (e.g. an oracle argument mismatch that `--drop-dependency-flags` does not cover, or a new structural invariant), add a tested rule to `cdb-compare` and select it with a flag from `run.sh` - do NOT add a shell allow-list or a `jq` heuristic. See `tests/tools/SPEC.md` for the comparator's normalization and matching model. ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/base/��������������������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0021006�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/base/Containerfile�������������������������������������������0000664�0000000�0000000�00000010103�15271777203�0023506�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SPDX-License-Identifier: GPL-3.0-or-later # # Base image for the dogfooding harness (Stage 2). Carries the Bear-under-test # installed as a real release install (correct INTERCEPT_LIBDIR layout) plus # the cdb-compare binary. Built from a 'git archive HEAD' context so only # committed files reach the image (dogfood-run-containerized: the repo working # tree and the devcontainer image stay unmodified). # # Multi-stage: the builder carries the Rust toolchain; the final stage does # not, so the shipped base stays free of a build toolchain. # ARG must precede the first FROM so it can parameterize the base image; both # stages reference the same pinned digest. ARG BASE_IMAGE FROM ${BASE_IMAGE} AS builder ENV RUSTUP_HOME=/root/.rustup \ CARGO_HOME=/root/.cargo \ PATH=/root/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin # Build deps: c-development for the cc toolchain the cc-crate/preload link # needs, lld because the ELF version script uses multiple version tags that # GNU ld rejects (lld is required on Linux per the repo host requirements), # curl for the rustup bootstrap. rustup gives a known >= 1.85 toolchain for # the Rust 2024 edition regardless of what fedora's packaged rust happens to # be (mirrors the devcontainer, which also bootstraps via rustup). RUN dnf -y install curl ca-certificates lld git \ && dnf -y group install c-development \ && dnf -y clean all \ && rm -rf /var/cache/dnf \ && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ | sh -s -- -y --profile minimal --default-toolchain stable WORKDIR /work COPY . /work # Build the workspace release binaries (Bear) and the comparator, then install # Bear with the default INTERCEPT_LIBDIR=lib so bear-driver resolves # ../lib/libexec.so. PREFIX=/opt/bear gives the fixed layout # /opt/bear/bin/bear -> /opt/bear/libexec/bear/bin/bear-driver. RUN cargo build --release \ && cargo build --release -p bear-test-tools --bin cdb-compare \ && PREFIX=/opt/bear sh scripts/install.sh \ && install -m 755 target/release/cdb-compare /opt/bear/bin/cdb-compare # rprof (github.com/rizsotto/rprof): a single-process resource profiler, pinned # by release tag, baked into the base so the harness can profile bear-driver's # CPU/memory during a build (dogfood-metrics-collect). Built here in the # toolchain-carrying builder and installed into /opt/bear/bin so the final stage # inherits it on PATH with no Rust toolchain (multi-stage: copy just the # binary). rprof measures the single launched process and ignores its # descendants, so `rprof run -- bear -- <build>` profiles exactly bear-driver # (the `bear` entry script execs the driver; the compilers are descendants). ARG RPROF_REF=v1.0.0 RUN git clone https://github.com/rizsotto/rprof /tmp/rprof \ && git -C /tmp/rprof checkout "${RPROF_REF}" \ && cargo build --release --manifest-path /tmp/rprof/Cargo.toml \ && install -m 755 /tmp/rprof/target/release/rprof /opt/bear/bin/rprof \ && rm -rf /tmp/rprof FROM ${BASE_IMAGE} AS final # Copy the installed Bear tree and the comparator; no Rust toolchain here. COPY --from=builder /opt/bear /opt/bear ENV PATH=/opt/bear/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin # Clang tooling for the clang-consumer check (dogfood-clang-consumer): clang + # clang-tools-extra give clangd, clang-tidy, and clang itself. These are CONSUMER # tools (like cdb-compare) - they read a captured compilation database and try to # build the AST for each entry - so they belong in the final stage, NOT the # toolchain-only builder. Every per-target image layers on this base, so the # clang-consumer check needs no per-target change. The base grows by the clang # install (accepted: it is the tool the database exists to feed). RUN dnf -y install clang clang-tools-extra \ && dnf -y clean all \ && rm -rf /var/cache/dnf # Sanity: the entry script and driver must be present and runnable, rprof (the # metrics capability) must be on PATH, and the clang consumer (clang-tidy, for # the clang-consumer check) must run. RUN bear --version && rprof --version && clang-tidy --version �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/faults/������������������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0021372�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/faults/bad-directory.json������������������������������������0000664�0000000�0000000�00000000270�15271777203�0025014�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������[ { "directory": "/nonexistent/bear/dogfood/corrupted-directory", "file": "hello.c", "arguments": ["cc", "-c", "-o", "hello.o", "hello.c"], "output": "hello.o" } ] ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/faults/blank-directory.json����������������������������������0000664�0000000�0000000�00000000437�15271777203�0025362�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������[ { "directory": "/src", "file": "adler32.c", "arguments": ["cc", "-c", "-o", "adler32.o", "adler32.c"], "output": "adler32.o" }, { "directory": "", "file": "crc32.c", "arguments": ["cc", "-c", "-o", "crc32.o", "crc32.c"], "output": "crc32.o" } ] ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/faults/duplicate.json����������������������������������������0000664�0000000�0000000�00000000453�15271777203�0024241�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������[ { "directory": "/src", "file": "adler32.c", "arguments": ["cc", "-c", "-o", "adler32.o", "adler32.c"], "output": "adler32.o" }, { "directory": "/src", "file": "adler32.c", "arguments": ["cc", "-c", "-o", "adler32.o", "adler32.c"], "output": "adler32.o" } ] ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/faults/empty-arguments.json����������������������������������0000664�0000000�0000000�00000000375�15271777203�0025433�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������[ { "directory": "/src", "file": "adler32.c", "arguments": ["cc", "-c", "-o", "adler32.o", "adler32.c"], "output": "adler32.o" }, { "directory": "/src", "file": "crc32.c", "arguments": [], "output": "crc32.o" } ] �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/faults/tu/���������������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0022022�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/faults/tu/hello.c��������������������������������������������0000664�0000000�0000000�00000000636�15271777203�0023276�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* SPDX-License-Identifier: GPL-3.0-or-later */ /* A trivial, self-contained translation unit used by the dogfooding * self-test (selftest.sh). The bad-directory fault fixture records a * compile of this file but with a wrong `directory`, so replaying the * recorded command fails - demonstrating that the replay check catches a * corrupted `directory` field. */ int dogfood_selftest_hello(void) { return 0; } ��������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/faults/undercount.json���������������������������������������0000664�0000000�0000000�00000000443�15271777203�0024454�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������[ { "directory": "/src", "file": "adler32.c", "arguments": ["cc", "-c", "-o", "adler32.o", "adler32.c"], "output": "adler32.o" }, { "directory": "/src", "file": "crc32.c", "arguments": ["cc", "-c", "-o", "crc32.o", "crc32.c"], "output": "crc32.o" } ] �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/goldens/�����������������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0021527�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/goldens/zlib/������������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0022467�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/goldens/zlib/compile_commands.json���������������������������0000664�0000000�0000000�00000011323�15271777203�0026673�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������[ { "directory": "/src", "file": "adler32.c", "arguments": [ "/usr/sbin/gcc", "-O3", "-DNDEBUG", "-D_LARGEFILE64_SOURCE=1", "-DHAVE_HIDDEN", "-c", "-o", "adler32.o", "adler32.c" ], "output": "adler32.o" }, { "directory": "/src", "file": "compress.c", "arguments": [ "/usr/sbin/gcc", "-O3", "-DNDEBUG", "-D_LARGEFILE64_SOURCE=1", "-DHAVE_HIDDEN", "-c", "-o", "compress.o", "compress.c" ], "output": "compress.o" }, { "directory": "/src", "file": "crc32.c", "arguments": [ "/usr/sbin/gcc", "-O3", "-DNDEBUG", "-D_LARGEFILE64_SOURCE=1", "-DHAVE_HIDDEN", "-c", "-o", "crc32.o", "crc32.c" ], "output": "crc32.o" }, { "directory": "/src", "file": "deflate.c", "arguments": [ "/usr/sbin/gcc", "-O3", "-DNDEBUG", "-D_LARGEFILE64_SOURCE=1", "-DHAVE_HIDDEN", "-c", "-o", "deflate.o", "deflate.c" ], "output": "deflate.o" }, { "directory": "/src", "file": "gzclose.c", "arguments": [ "/usr/sbin/gcc", "-O3", "-DNDEBUG", "-D_LARGEFILE64_SOURCE=1", "-DHAVE_HIDDEN", "-c", "-o", "gzclose.o", "gzclose.c" ], "output": "gzclose.o" }, { "directory": "/src", "file": "gzlib.c", "arguments": [ "/usr/sbin/gcc", "-O3", "-DNDEBUG", "-D_LARGEFILE64_SOURCE=1", "-DHAVE_HIDDEN", "-c", "-o", "gzlib.o", "gzlib.c" ], "output": "gzlib.o" }, { "directory": "/src", "file": "gzread.c", "arguments": [ "/usr/sbin/gcc", "-O3", "-DNDEBUG", "-D_LARGEFILE64_SOURCE=1", "-DHAVE_HIDDEN", "-c", "-o", "gzread.o", "gzread.c" ], "output": "gzread.o" }, { "directory": "/src", "file": "gzwrite.c", "arguments": [ "/usr/sbin/gcc", "-O3", "-DNDEBUG", "-D_LARGEFILE64_SOURCE=1", "-DHAVE_HIDDEN", "-c", "-o", "gzwrite.o", "gzwrite.c" ], "output": "gzwrite.o" }, { "directory": "/src", "file": "infback.c", "arguments": [ "/usr/sbin/gcc", "-O3", "-DNDEBUG", "-D_LARGEFILE64_SOURCE=1", "-DHAVE_HIDDEN", "-c", "-o", "infback.o", "infback.c" ], "output": "infback.o" }, { "directory": "/src", "file": "inffast.c", "arguments": [ "/usr/sbin/gcc", "-O3", "-DNDEBUG", "-D_LARGEFILE64_SOURCE=1", "-DHAVE_HIDDEN", "-c", "-o", "inffast.o", "inffast.c" ], "output": "inffast.o" }, { "directory": "/src", "file": "inflate.c", "arguments": [ "/usr/sbin/gcc", "-O3", "-DNDEBUG", "-D_LARGEFILE64_SOURCE=1", "-DHAVE_HIDDEN", "-c", "-o", "inflate.o", "inflate.c" ], "output": "inflate.o" }, { "directory": "/src", "file": "inftrees.c", "arguments": [ "/usr/sbin/gcc", "-O3", "-DNDEBUG", "-D_LARGEFILE64_SOURCE=1", "-DHAVE_HIDDEN", "-c", "-o", "inftrees.o", "inftrees.c" ], "output": "inftrees.o" }, { "directory": "/src", "file": "test/example.c", "arguments": [ "/usr/sbin/gcc", "-O3", "-DNDEBUG", "-D_LARGEFILE64_SOURCE=1", "-DHAVE_HIDDEN", "-I.", "-c", "-o", "example.o", "test/example.c" ], "output": "example.o" }, { "directory": "/src", "file": "test/minigzip.c", "arguments": [ "/usr/sbin/gcc", "-O3", "-DNDEBUG", "-D_LARGEFILE64_SOURCE=1", "-DHAVE_HIDDEN", "-I.", "-c", "-o", "minigzip.o", "test/minigzip.c" ], "output": "minigzip.o" }, { "directory": "/src", "file": "trees.c", "arguments": [ "/usr/sbin/gcc", "-O3", "-DNDEBUG", "-D_LARGEFILE64_SOURCE=1", "-DHAVE_HIDDEN", "-c", "-o", "trees.o", "trees.c" ], "output": "trees.o" }, { "directory": "/src", "file": "uncompr.c", "arguments": [ "/usr/sbin/gcc", "-O3", "-DNDEBUG", "-D_LARGEFILE64_SOURCE=1", "-DHAVE_HIDDEN", "-c", "-o", "uncompr.o", "uncompr.c" ], "output": "uncompr.o" }, { "directory": "/src", "file": "zutil.c", "arguments": [ "/usr/sbin/gcc", "-O3", "-DNDEBUG", "-D_LARGEFILE64_SOURCE=1", "-DHAVE_HIDDEN", "-c", "-o", "zutil.o", "zutil.c" ], "output": "zutil.o" } ] �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/internal/����������������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0021710�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/internal/consumer-fault-demo.sh������������������������������0000775�0000000�0000000�00000013201�15271777203�0026132�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/sh # SPDX-License-Identifier: GPL-3.0-or-later # # Fault demonstration for the clang-consumer check (dogfood-clang-consumer exit # criterion: "catches a deliberately broken entry"). Unlike the replay # bad-directory fault - a host-side fixture in selftest.sh - this demo MUST run # where clang exists, i.e. IN the build container, because the host has no # compiler. It is therefore a small, clearly-labeled in-container one-off, run by # the maintainer against the curl target image: # # podman run --rm \ # -v tests/dogfooding/internal/consumer-loop.sh:/consumer-loop.sh:ro,Z \ # -v tests/dogfooding/internal/consumer-fault-demo.sh:/demo.sh:ro,Z \ # bear-dogfood-curl:<tag> sh /demo.sh # # It builds curl (so the captured CDB, sources, and headers exist), takes a # KNOWN-GOOD entry that the consumer accepts (OK), strips an `-I` that the TU # needs to find a header, and shows the SAME consumer (consumer_cdb from # consumer-loop.sh) now reports FAIL for that entry. No JSON is edited by hand # beyond removing the one include flag; the fault is a real, broken database # entry. Exit 0 iff the good entry was OK and the broken entry was caught (FAIL); # non-zero otherwise (the demo itself failed to demonstrate the fault). # # It deliberately reuses consumer_cdb's exact categorization so the demo proves # the SHIPPED check (not a bespoke probe) catches the fault. To feed # consumer_cdb a single chosen entry, it writes a one-entry compile_commands.json # into a scratch directory and points the loop at it. set -eu # shellcheck source=tests/dogfooding/internal/consumer-loop.sh . /consumer-loop.sh CDB_COMPARE="${CDB_COMPARE:-/opt/bear/bin/cdb-compare}" OUT_DIR="/out" CDB="$OUT_DIR/compile_commands.json" # Build curl with Bear if the capture is not already present (so the demo is # runnable standalone). The TARGET_BUILD_CMD is intentionally NOT reproduced # here; the caller is expected to have built, or we do a minimal build below. if [ ! -f "$CDB" ]; then printf '== building curl so the capture, sources, and headers exist ==\n' >&2 mkdir -p "$OUT_DIR" cmake -S /src -B /build -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ -DCURL_USE_OPENSSL=OFF -DCURL_USE_LIBSSH2=OFF -DCURL_USE_LIBSSH=OFF \ -DCURL_USE_LIBPSL=OFF -DCURL_USE_GSSAPI=OFF -DUSE_NGHTTP2=OFF \ -DUSE_LIBIDN2=OFF -DCURL_BROTLI=OFF -DCURL_ZSTD=OFF -DCURL_ZLIB=OFF \ -DUSE_LIBRTMP=OFF -DCURL_DISABLE_LDAP=ON -DCURL_DISABLE_LDAPS=ON \ -DBUILD_CURL_EXE=ON -DBUILD_SHARED_LIBS=ON >&2 bear --output "$CDB" -- cmake --build /build >&2 fi GOOD_DIR="$(mktemp -d)" BAD_DIR="$(mktemp -d)" RESULT_GOOD="$(mktemp)" RESULT_BAD="$(mktemp)" cleanup() { rm -rf "$GOOD_DIR" "$BAD_DIR" "$RESULT_GOOD" "$RESULT_BAD"; } trap cleanup EXIT INT TERM # Pick a known-good entry and write two one-entry databases: the original (good) # and a copy with one needed `-I` stripped (bad). python3 is present in the # fedora base; it edits JSON robustly without a shell here-string. printf '== picking a known-good entry and stripping an -I it needs ==\n' >&2 python3 - "$CDB" "$GOOD_DIR/compile_commands.json" "$BAD_DIR/compile_commands.json" <<'PY' >&2 import json, sys, copy cdb_path, good_out, bad_out = sys.argv[1], sys.argv[2], sys.argv[3] db = json.load(open(cdb_path)) def args_of(e): return e.get("arguments") # Choose an entry that (a) has a source-tree -I we can strip and (b) is a plain # library TU. altsvc.c is a stable, dependency-light curl TU that needs the # curl/*.h headers reached via -I/src/include (and -I/src/lib). cand = None for e in db: a = args_of(e) if not a: continue if e["file"].endswith("altsvc.c") and any(x.startswith("-I/src") for x in a): cand = e break if cand is None: # Fallback: any entry with a /src include path. for e in db: a = args_of(e) if a and any(x.startswith("-I/src") for x in a): cand = e break if cand is None: sys.stderr.write("no entry with a /src include path to strip; cannot demo\n") sys.exit(2) good = copy.deepcopy(cand) bad = copy.deepcopy(cand) stripped = [x for x in args_of(bad) if x.startswith("-I/src")] bad["arguments"] = [x for x in args_of(bad) if not x.startswith("-I/src")] json.dump([good], open(good_out, "w")) json.dump([bad], open(bad_out, "w")) sys.stderr.write("entry: %s\n" % cand["file"]) sys.stderr.write("stripped -I flags: %s\n" % " ".join(stripped)) PY # Run the SHIPPED consumer over each one-entry database (count 1). printf '\n== consumer verdict on the GOOD entry (expect OK / PASS) ==\n' >&2 set +e consumer_cdb "$CDB_COMPARE" "$GOOD_DIR/compile_commands.json" 1 /build "$RESULT_GOOD" GOOD_RC=$? set -e sed 's/^/ /' "$RESULT_GOOD" >&2 GOOD_TALLY="$(sed -n 's/^consumer: //p' "$RESULT_GOOD")" printf '\n== consumer verdict on the BROKEN entry (-I stripped; expect FAIL) ==\n' >&2 set +e consumer_cdb "$CDB_COMPARE" "$BAD_DIR/compile_commands.json" 1 /build "$RESULT_BAD" BAD_RC=$? set -e sed 's/^/ /' "$RESULT_BAD" >&2 BAD_TALLY="$(sed -n 's/^consumer: //p' "$RESULT_BAD")" printf '\n== fault-demo result ==\n' >&2 printf 'good: rc=%s (%s)\n' "$GOOD_RC" "$GOOD_TALLY" >&2 printf 'bad: rc=%s (%s)\n' "$BAD_RC" "$BAD_TALLY" >&2 # The demo succeeds iff the good entry passed (rc 0) AND the broken entry was # caught as a FAIL (rc 1). Any other combination means the consumer did not # distinguish a well-formed entry from a deliberately broken one. if [ "$GOOD_RC" -eq 0 ] && [ "$BAD_RC" -eq 1 ]; then printf 'DEMO PASSED: the consumer accepts the good entry and FAILS the stripped-I entry\n' >&2 exit 0 fi printf 'DEMO FAILED: the consumer did not flip OK->FAIL on the stripped-I fault\n' >&2 exit 1 �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/internal/consumer-loop.sh������������������������������������0000664�0000000�0000000�00000024460�15271777203�0025054�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SPDX-License-Identifier: GPL-3.0-or-later # # The clang-consumer loop (dogfood-clang-consumer), factored into a single POSIX # sh function. It is the SAME SHAPE as replay-loop.sh's replay_cdb, but instead # of re-running the recorded compiler it feeds each sampled entry to a real # clang-tooling consumer and asks "can the tool build the AST from Bear's # database?". That is the one check that validates the database WORKS in the tool # it exists for, not merely that it looks structurally right. # # It runs INSIDE the build container only (concatenated into run.sh's `podman run # sh -c` script): the consumer reads the CAPTURED compile_commands.json and needs # the recorded sources and generated headers, which exist there only before # teardown. Unlike replay's bad-directory fault (a host-side selftest.sh # fixture), the clang-consumer fault demo also runs in-container, because the # host has no clang (see consumer-fault-demo.sh). # # consumer_cdb <cdb-compare> <CDB> <count> <build-dir> <summary-out> # # <cdb-compare> path to the cdb-compare binary to use for `sample` # <CDB> captured compilation database to feed the consumer (its # containing directory is passed to the consumer as `-p`) # <count> max entries to sample # <build-dir> passed to `sample --build-dir` so the sampler prefers TUs # whose flags do not reference build-dir includes (the same # build-dir-aware selection replay uses) # <summary-out> file to write the OK/FAIL/INCONCLUSIVE tally and any rejected # entries to # # THE CONSUMER (chosen empirically, not guessed). The targets build with GCC, so # clang tooling must consume a gcc-built compile_commands.json. The consumer is: # # clang-tidy --checks='-*,bugprone-assert-side-effect' --allow-no-checks \ # -p <cdb-dir> <file> # # clang-tidy reads the gcc-recorded command from the CDB at `-p <cdb-dir>` and # mangles the driver-incompatible flags itself (it drops GCC-only flags clang # does not know) - this cross-compiler consumption is exactly what the check # validates. A single real check (`bugprone-assert-side-effect`, a cheap, always- # present check) is enabled to FORCE a full front-end parse of the TU; with no # check enabled clang-tidy short-circuits and never parses, so the gate would be # vacuous. `--allow-no-checks` keeps the run from erroring out should that check # name ever disappear from a future clang-tools. # # Why clang-tidy and not `clangd --check`: `clangd --check` runs clangd's # INTERNAL tweak/refactoring self-tests over the TU and reports their failures as # "N errors" with a non-zero exit even when the AST built perfectly (observed: # every curl TU "built the AST" yet reported up to 60 such bogus errors). That # makes clangd's exit code and error count useless as a database-validity signal. # clang-tidy with no lint check enabled does a PURE parse: the only `error:` # diagnostics it emits are genuine front-end errors (a header not found from a # wrong/missing -I, a bad -x, a missing sysroot) - exactly the database defects # this check exists to catch. # # Categorization (keyed on clang-tidy's `error:` diagnostics, tuned on the real # curl output - 20/20 OK, the stripped-`-I` fault FAILs): # OK zero `error:` diagnostics: clang-tidy parsed the TU and built # the AST from Bear's recorded command. (Warnings are expected on # a gcc TU under clang and are NOT a defect.) # FAIL one or more `error:` diagnostics, INCLUDING a not-found # #include. This is the load-bearing decision and it relies on a # premise this loop is built around: the consumer runs in the # LIVE post-build container (same `podman run` as the build), so # every header the build saw - source-tree AND generated - is # still on disk. A "file not found" here therefore is NOT a # generated-header-gone artifact (that only happens after # teardown); it means Bear's recorded include paths do not # actually let the tool find a header the build found - exactly # the wrong/missing-`-I` defect this check exists to catch. The # fault demo (consumer-fault-demo.sh) confirms this: stripping an # `-I` from a good entry turns its OK into a not-found `error:` => # FAIL. On the pinned curl build no GOOD entry trips this (the # build-dir-aware sampler avoids generated-header-heavy TUs). # INCONCLUSIVE the source TU is no longer on disk (a build-time input gone - # the one genuine missing-input case, since the TU source, unlike # a header, can be a generated/temporary file). Reserved narrowly # so the not-found-header signal is not diluted into noise. # # NOTE on the OK<->FAIL boundary (the thing to scrutinize): because a not-found # header is FAIL, a future target whose sampled TUs legitimately depend on a # generated header the build cleans BEFORE this loop runs would false-FAIL. That # does not happen for curl/zlib (the build leaves its tree intact and the sampler # prefers source-tree TUs), and it is the honest trade: keeping not-found as # INCONCLUSIVE would have masked the very wrong-`-I` defect the check is for. If # such a target appears, narrow the FAIL rule to "not-found header NOT reachable # from a build-dir include" rather than blanket-INCONCLUSIVE-ing not-found. # # Exit status mirrors replay_cdb so run.sh reuses the same gate mapping: # 0 PASS no FAIL and at least one OK (non-vacuous: an all-inconclusive # or empty sample validated nothing and must NOT pass) # 1 FAIL at least one entry indicts the database # 2 INCONCLUSIVE no FAIL but nothing was actually consumed (all inconclusive # or empty sample) # 3 ERROR `cdb-compare sample` itself failed (infra) consumer_cdb() { _cc_compare="$1" _cc_cdb="$2" _cc_count="$3" _cc_builddir="$4" _cc_out="$5" _cc_ok=0 _cc_fail=0 _cc_incon=0 : >"$_cc_out" # clang-tidy reads the database from the directory given to `-p`; that is the # directory holding the captured compile_commands.json. _cc_cdbdir="$(dirname "$_cc_cdb")" "$_cc_compare" sample "$_cc_cdb" --count "$_cc_count" --build-dir "$_cc_builddir" \ >"$_cc_out.lines" 2>>"$_cc_out" _cc_sample_rc=$? # A non-zero sample is an infra failure (a broken cdb-compare), NOT a vacuous # "nothing matched": signal it distinctly (3 => ERROR). if [ "$_cc_sample_rc" -ne 0 ]; then printf 'ERROR: cdb-compare sample failed (rc=%s)\n' "$_cc_sample_rc" >>"$_cc_out" rm -f "$_cc_out.lines" return 3 fi while IFS= read -r _cc_line; do [ -n "$_cc_line" ] || continue eval "set -- $_cc_line" # The first field is the recorded `directory`; the rest is the argv. We do # not re-run the compiler - we only need the source FILE to hand to # clang-tidy, which then reads the recorded command from the CDB itself. _cc_dir="$1" shift # Find the source file in the recorded argv: the argument naming an # existing C/C++ TU. Resolve it relative to the recorded directory when it # is not absolute, so the path matches the CDB's `file`. The `-o` output # is never matched here (it is a .o, not a source extension), so it needs # no special-casing on the build systems we target. _cc_file="" for _cc_arg in "$@"; do case "$_cc_arg" in *.c|*.cc|*.cpp|*.cxx|*.c++|*.C|*.m|*.mm) case "$_cc_arg" in /*) _cc_cand="$_cc_arg" ;; *) _cc_cand="$_cc_dir/$_cc_arg" ;; esac if [ -f "$_cc_cand" ]; then _cc_file="$_cc_cand" break fi ;; esac done if [ -z "$_cc_file" ]; then # The TU source is no longer on disk: a build-time input gone, not a # database defect. Inconclusive. _cc_incon=$((_cc_incon + 1)) printf 'INCONCLUSIVE (source not on disk): %s\n' "$*" >>"$_cc_out" continue fi # Feed the entry to clang-tidy. A single real check forces a full parse; # `-p <cdb-dir>` makes it read Bear's recorded command for this file. # `-Wno-error` undoes a recorded `-Werror`: the target is only warning-free # under GCC, so a clang warning promoted to an error says nothing about # the database (seen on the kernel, built with CONFIG_WERROR=y). _cc_diag="$(clang-tidy --checks='-*,bugprone-assert-side-effect' \ --allow-no-checks --extra-arg=-Wno-error \ -p "$_cc_cdbdir" "$_cc_file" 2>&1)" # The verdict is the presence of front-end `error:` diagnostics. None => # the AST built (warnings are expected on a gcc TU and are ignored). Any # error - including a not-found #include, which in the live post-build # container is a wrong/missing-`-I` defect, not a generated-header artifact # - indicts the database (FAIL). The only missing-INPUT inconclusive is the # TU source itself being gone, handled above before the consumer runs. # Match only the diagnostic SEVERITY field ': error:' (clang prints # `<path>:<line>:<col>: error: ...`, the severity always preceded by # ': '), so a warning whose message BODY merely contains the word # "error:" cannot false-FAIL a healthy entry. if printf '%s\n' "$_cc_diag" | grep -q ': error:'; then _cc_fail=$((_cc_fail + 1)) printf 'FAIL (clang-tidy rejected the entry): %s\n' "$_cc_file" >>"$_cc_out" printf '%s\n' "$_cc_diag" | grep ': error:' | head -3 | sed 's/^/ diag: /' >>"$_cc_out" else _cc_ok=$((_cc_ok + 1)) fi done <"$_cc_out.lines" rm -f "$_cc_out.lines" printf 'consumer: ok=%s fail=%s inconclusive=%s\n' \ "$_cc_ok" "$_cc_fail" "$_cc_incon" >>"$_cc_out" if [ "$_cc_fail" -gt 0 ]; then return 1 fi if [ "$_cc_ok" -eq 0 ]; then # Nothing was actually consumed (all inconclusive, or empty sample). return 2 fi return 0 } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/internal/lib.sh����������������������������������������������0000664�0000000�0000000�00000024114�15271777203�0023014�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SPDX-License-Identifier: GPL-3.0-or-later # # Sourced helpers for the dogfooding harness: logging, outcome/exit-code # constants, preflight checks, and podman wrappers. Keeping these here keeps # run.sh readable. POSIX sh only; no bashisms. # --- outcome / exit codes (dogfood-build-failure-taxonomy) ------------------- # # PASS = 0 golden regression check passed # FAIL = 1 regression: golden mismatch (a real behavioral change) # INCONCLUSIVE = 2 the target build failed for its own reasons # (configure/make/source-fetch/network/sha/OOM) # ERROR = 3 harness or Bear-infra failure (podman missing, disk/digest # preflight, base build, empty capture) EXIT_PASS=0 EXIT_FAIL=1 EXIT_INCONCLUSIVE=2 EXIT_ERROR=3 # --- logging (everything to stderr; stdout is reserved for data) ------------- log() { printf '%s\n' "$*" >&2; } info() { printf '[dogfood] %s\n' "$*" >&2; } warn() { printf '[dogfood] WARN: %s\n' "$*" >&2; } err() { printf '[dogfood] ERROR: %s\n' "$*" >&2; } # Print the single final status line and exit with the matching code. Takes # the outcome name and a one-line reason. finish() { _outcome="$1" _reason="$2" case "$_outcome" in PASS) _code="$EXIT_PASS" ;; FAIL) _code="$EXIT_FAIL" ;; INCONCLUSIVE) _code="$EXIT_INCONCLUSIVE" ;; ERROR) _code="$EXIT_ERROR" ;; *) _code="$EXIT_ERROR"; _outcome="ERROR" ;; esac printf '\n[dogfood] OUTCOME: %s (exit %s) - %s\n' "$_outcome" "$_code" "$_reason" >&2 exit "$_code" } # --- podman wrappers --------------------------------------------------------- require_podman() { if ! command -v podman >/dev/null 2>&1; then err "podman not found on PATH" finish ERROR "podman is required but not installed" fi } # Remove a container if it exists; ignore errors (best-effort cleanup). rm_container() { podman rm -f "$1" >/dev/null 2>&1 || true } # --- preflight (dogfood-preflight) ------------------------------------------- # (a) Free-disk check on the podman graphroot against a per-target minimum. preflight_disk() { _min_kib="$1" _graphroot="$(podman info --format '{{.Store.GraphRoot}}' 2>/dev/null)" if [ -z "$_graphroot" ]; then err "could not determine podman graphroot" return 1 fi # df -Pk: POSIX format, 1024-byte blocks. Field 4 is available blocks. _avail_kib="$(df -Pk "$_graphroot" 2>/dev/null | awk 'NR==2 {print $4}')" if [ -z "$_avail_kib" ]; then err "could not read free disk for graphroot $_graphroot" return 1 fi info "graphroot $_graphroot has ${_avail_kib} KiB free (need ${_min_kib} KiB)" if [ "$_avail_kib" -lt "$_min_kib" ]; then err "insufficient free disk on $_graphroot: ${_avail_kib} KiB < ${_min_kib} KiB" return 1 fi return 0 } # (b) Resolve/verify the pinned base image is present or pullable. Pull by # digest is idempotent; a failure here means the pin is unreachable. preflight_image() { _image="$1" if podman image exists "$_image" 2>/dev/null; then info "pinned base image already present: $_image" return 0 fi info "pulling pinned base image: $_image" if ! podman pull "$_image" >&2; then err "could not pull pinned base image: $_image" return 1 fi return 0 } # --- cdb_compare (dogfood-run-containerized) ---------------------------------- # # Run the release cdb-compare that ships inside the target image, against files # in the run directory. The comparison stays in the container model the rest of # the harness uses: the host needs no cdb-compare binary, and there is no # glibc-mismatch risk from copying the image's dynamically linked binary out to # an arbitrary host. RESULTS_DIR is bind-mounted read-only at /run, so callers # pass in-container paths (`/run/<file>`, e.g. "/run/${FRESH##*/}"). The report # goes to stdout and the exit code propagates, so the existing `>"$REPORT"` # redirects and `if cdb_compare ...; then` gates are unchanged from the former # host-binary calls. All gates run after build_and_capture, so TARGET_TAG (which # carries /opt/bear/bin/cdb-compare) always exists by the time this is called. # # Reads from the caller's environment: TARGET_TAG, RESULTS_DIR. cdb_compare() { podman run --rm \ -v "$RESULTS_DIR":/run:ro,z \ "$TARGET_TAG" /opt/bear/bin/cdb-compare "$@" } # --- build-and-capture (dogfood-run-containerized + dogfood-fixed-paths) ------ # # Run the real target build wrapped by Bear in a fresh throwaway container and # copy the captured compilation database out. Factored out of run.sh STEP 5 so # the determinism check (dogfood-determinism) can invoke it twice, once per # fresh container, without duplicating the build-failure taxonomy. # # Arguments: # $1 container name (must be unique; the caller registers it for cleanup) # $2 destination path for the captured compile_commands.json on the host # $3 build log path on the host # $4 value for INJECT_CFLAGS passed into the container (empty for a normal # run; a perturbing flag for the --inject-fault determinism self-test) # $5 optional post-build snippet run INSIDE the same container AFTER the # build, with `set +e` (so its own failing commands - e.g. replay's # deliberate failing compiles - do not abort the script). Empty for a # normal / determinism run, so those paths are byte-for-byte unchanged. # Used by invariants mode (write /out/object_count) and replay mode (run # the replay loop and write /out/replay_result), which both need the # container's source/object tree still present before teardown. # $6 optional host path for the rprof metrics artifact (dogfood-metrics- # collect). When non-empty, the build is profiled with rprof (via the # RPROF_PREFIX env the target's TARGET_BUILD_CMD expands before `bear`) and # /out/metrics.jsonl is copied here. Empty => plain bear, no profiling. # # Reads from the caller's environment: TARGET_TAG, TARGET_BUILD_CMD. # # Outcome taxonomy (calls finish, which exits the whole run): # container never started -> ERROR (host/cgroup/image infra) # build exited non-zero -> INCONCLUSIVE (target's own configure/make) # capture missing / empty -> ERROR (interception produced nothing) # On success it returns 0 and leaves the capture at $2. build_and_capture() { _bac_container="$1" _bac_dest="$2" _bac_log="$3" _bac_inject="$4" _bac_post="${5:-}" _bac_metrics="${6:-}" # When a metrics dest is given, profile bear-driver with rprof: the target's # TARGET_BUILD_CMD expands ${RPROF_PREFIX:-} immediately before `bear`, so a # non-empty value turns `bear -- <build>` into `rprof run -o ... -- bear -- # <build>`. rprof profiles the single bear/bear-driver process and ignores # the compiler descendants. Empty => plain bear. if [ -n "$_bac_metrics" ]; then _bac_rprof="rprof run -o /out/metrics.jsonl --" else _bac_rprof="" fi info "running real build in container $_bac_container" set +e podman run --systemd=always --name "$_bac_container" \ --env "INJECT_CFLAGS=$_bac_inject" \ --env "RPROF_PREFIX=$_bac_rprof" \ "$TARGET_TAG" sh -c " set -e mkdir -p /out $TARGET_BUILD_CMD # Post-build step (invariants/replay) runs without set -e so its own # expected failures do not abort the container; it writes result files # under /out that the caller pulls out and gates on. set +e $_bac_post : " >"$_bac_log" 2>&1 _bac_rc=$? set -e if [ "$_bac_rc" -ne 0 ]; then # Distinguish a container that never started (host/cgroup/image infra => # ERROR) from one that ran but whose build failed for its own reasons # (=> INCONCLUSIVE). If the container does not exist, podman run never # launched it. if ! podman container exists "$_bac_container" 2>/dev/null; then err "podman run never started the container; see $_bac_log" finish ERROR "podman run failed to start container (systemd/cgroup/image infra)" fi err "target build exited $_bac_rc; see $_bac_log" finish INCONCLUSIVE "target build failed (configure/make) - log at $_bac_log" fi # Copy the captured CDB out of the stopped container (sidesteps SELinux relabel). if ! podman cp "$_bac_container:/out/compile_commands.json" "$_bac_dest" >&2; then finish ERROR "could not copy compile_commands.json out of the container" fi # Empty / entry-less capture => ERROR (Bear ran but captured nothing). if ! grep -q '"file"' "$_bac_dest" 2>/dev/null; then err "captured CDB has no entries: $_bac_dest" finish ERROR "empty capture from real build (interception produced nothing)" fi info "captured CDB: $_bac_dest" # Pull the full rprof JSONL out when profiling was enabled. A missing file is # a warning, not a failure: the build + capture already succeeded and metrics # are an advisory by-product (dogfood-metrics-collect). The file is kept whole # for `rprof view` to render/compare later; the harness never parses it. if [ -n "$_bac_metrics" ]; then if podman cp "$_bac_container:/out/metrics.jsonl" "$_bac_metrics" >&2; then info "rprof metrics captured: $_bac_metrics" else warn "rprof metrics file not found in container (/out/metrics.jsonl)" fi fi return 0 } # --- oracle validation (dogfood-oracle-cmake, dogfood-divergence-report) ------ # # The oracle path compares Bear's capture against the database CMake emits, on # the intersection of translation units. The WHOLE comparison - matching TUs by # absolute object path (`--output-from-o`), dropping the benign `-M*` depfile # flags (`--drop-dependency-flags`), bucketing one-sided entries as advisory # extras and gating on matched-but-differing only (`--intersection`) - is done # by the image's cdb-compare (via `cdb_compare`, see run.sh STEP 6). No jq and no # allow-list file: the equivalence logic lives in the unit-tested tests/tools # crate, not in shell. ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/internal/replay-loop.sh��������������������������������������0000664�0000000�0000000�00000011242�15271777203�0024507�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SPDX-License-Identifier: GPL-3.0-or-later # # The replay loop (dogfood-replay), factored into a single POSIX sh function so # the exact same logic runs in two places: # - INSIDE the build container (run.sh's replay mode reads this file into the # `podman run sh -c` script, because the recorded sources and generated # headers only exist there before teardown), and # - on the HOST in selftest.sh, against the committed bad-directory fault # fixture, to demonstrate the check catches a corrupted `directory`. # # It is sourced (host) or concatenated into the in-container script (container); # either way it defines one function and runs nothing on its own. # # replay_cdb <cdb-compare> <CDB> <count> <build-dir> <summary-out> # # <cdb-compare> path to the cdb-compare binary to use for `sample` # <CDB> compilation database to replay from # <count> max entries to sample # <build-dir> passed to `sample --build-dir` so the sampler prefers TUs # whose flags do not reference build-dir includes # <summary-out> file to write the OK/FAIL/INCONCLUSIVE tally and any failing # commands to # # Each sampled entry is replayed in its recorded `directory` with `-fsyntax-only` # appended (parse the TU, produce no object). Categorization: # OK the compiler accepted the recorded arguments # INCONCLUSIVE the failure is a missing include / generated header (the TU # depends on a build-time input that no longer exists in a # throwaway container) - not a Bear fault # FAIL any other replay failure, including a `directory` that does # not exist (a corrupted-directory fault) - a malformed entry # # Exit status: 0 iff no real FAIL AND at least one entry was actually verified # (an all-INCONCLUSIVE sample validated nothing and must not pass vacuously, # mirroring the oracle's non-vacuity stance). Return codes: 0 PASS, 1 a real # FAIL, 2 all-inconclusive (INCONCLUSIVE), 3 `cdb-compare sample` itself failed # (an infra ERROR). The caller maps these to the harness outcome. replay_cdb() { _rc_compare="$1" _rc_cdb="$2" _rc_count="$3" _rc_builddir="$4" _rc_out="$5" _rc_ok=0 _rc_fail=0 _rc_incon=0 : >"$_rc_out" "$_rc_compare" sample "$_rc_cdb" --count "$_rc_count" --build-dir "$_rc_builddir" \ >"$_rc_out.lines" 2>>"$_rc_out" _rc_sample_rc=$? # A non-zero sample is an infra failure (a broken/malfunctioning cdb-compare), # NOT a vacuous "nothing matched": signal it distinctly (return 3 => ERROR) # rather than letting an empty line set fall through to INCONCLUSIVE. if [ "$_rc_sample_rc" -ne 0 ]; then printf 'ERROR: cdb-compare sample failed (rc=%s)\n' "$_rc_sample_rc" >>"$_rc_out" rm -f "$_rc_out.lines" return 3 fi while IFS= read -r _rc_line; do [ -n "$_rc_line" ] || continue eval "set -- $_rc_line" _rc_dir="$1" shift # A nonexistent recorded `directory` is itself a malformed entry: the # compile can never run from where Bear claims it did. Categorize that # as a real FAIL before attempting the compile. if [ ! -d "$_rc_dir" ]; then _rc_fail=$((_rc_fail + 1)) printf 'FAIL (bad directory "%s"): %s\n' "$_rc_dir" "$*" >>"$_rc_out" continue fi _rc_err="$( (cd "$_rc_dir" && "$@" -fsyntax-only) 2>&1 )" _rc_status=$? if [ "$_rc_status" -eq 0 ]; then _rc_ok=$((_rc_ok + 1)) continue fi # Missing include / generated header => inconclusive (a build-time input # gone after teardown), NOT a malformed-entry failure. The signal is a # missing-file diagnostic ("No such file", "file not found", "not # found"); a bare "fatal error:" without one is a real failure, so it # is NOT matched here. case "$_rc_err" in *"No such file"*|*"file not found"*|*"not found"*) _rc_incon=$((_rc_incon + 1)) printf 'INCONCLUSIVE (missing input): %s\n' "$*" >>"$_rc_out" ;; *) _rc_fail=$((_rc_fail + 1)) printf 'FAIL: %s\n' "$*" >>"$_rc_out" printf ' stderr: %s\n' "$_rc_err" >>"$_rc_out" ;; esac done <"$_rc_out.lines" rm -f "$_rc_out.lines" printf 'replay: ok=%s fail=%s inconclusive=%s\n' \ "$_rc_ok" "$_rc_fail" "$_rc_incon" >>"$_rc_out" if [ "$_rc_fail" -gt 0 ]; then return 1 fi if [ "$_rc_ok" -eq 0 ]; then # Nothing was actually verified (all inconclusive, or empty sample). return 2 fi return 0 } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/internal/selftest.sh�����������������������������������������0000775�0000000�0000000�00000010346�15271777203�0024104�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/sh # SPDX-License-Identifier: GPL-3.0-or-later # # Dogfooding self-test: demonstrate that the Stage 4 checks catch the plan's # injected faults - a dropped entry, a duplicated entry, an empty-arguments # entry, and a corrupted `directory`. This is the demonstrable-fault-catching # deliverable for the dogfood-invariants / dogfood-replay exit criterion. # # It needs NO container: it runs the HOST cdb-compare against tiny, committed # fault fixtures under faults/, and runs the replay loop (replay-loop.sh, the # very same function the in-container replay uses) against the bad-directory # fixture. Every fault MUST be caught (the check must exit non-zero); if any # fault slips through, the self-test fails. # # Usage: # tests/dogfooding/internal/selftest.sh # # Exit 0 = every fault was caught; exit 1 = a fault was missed (or infra error). set -eu HERE="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$HERE/../../.." && pwd)" FAULTS="$HERE/../faults" # shellcheck source=tests/dogfooding/internal/replay-loop.sh . "$HERE/replay-loop.sh" CDB_COMPARE="$REPO_ROOT/target/release/cdb-compare" if [ ! -x "$CDB_COMPARE" ]; then printf 'ERROR: host cdb-compare not found at %s\n' "$CDB_COMPARE" >&2 printf 'build it with: cargo build --release -p bear-test-tools --bin cdb-compare\n' >&2 exit 1 fi PASS_COUNT=0 FAIL_COUNT=0 # Assert that a command exits NON-zero (the fault was caught). $1 is a label; # the rest is the command to run. Its output is shown indented for the log. assert_caught() { _label="$1" shift if "$@" >/tmp/dogfood-selftest.$$ 2>&1; then printf 'MISS %s (the check exited 0 - the fault was NOT caught)\n' "$_label" >&2 sed 's/^/ /' /tmp/dogfood-selftest.$$ >&2 FAIL_COUNT=$((FAIL_COUNT + 1)) else printf 'CAUGHT %s\n' "$_label" >&2 sed 's/^/ /' /tmp/dogfood-selftest.$$ >&2 PASS_COUNT=$((PASS_COUNT + 1)) fi rm -f /tmp/dogfood-selftest.$$ } printf '== dogfooding self-test: faults must be caught ==\n\n' >&2 # 1. Duplicated entry => no-true-duplicates invariant FAILS. assert_caught "duplicate entry (no-true-duplicates)" \ "$CDB_COMPARE" invariants --format human "$FAULTS/duplicate.json" printf '\n' >&2 # 2. Empty arguments => non-empty-arguments invariant FAILS. assert_caught "empty arguments (non-empty-arguments)" \ "$CDB_COMPARE" invariants --format human "$FAULTS/empty-arguments.json" printf '\n' >&2 # 3. Dropped entry => entry-count invariant FAILS (fewer entries than expected). # undercount.json has 2 entries; we state 3 expected with 0 tolerance. assert_caught "dropped entry (entry-count, --expected-objects 3)" \ "$CDB_COMPARE" invariants --expected-objects 3 --tolerance 0 \ --format human "$FAULTS/undercount.json" printf '\n' >&2 # 4. Corrupted directory => replay FAILS (the recorded `directory` does not # exist, so the compile can never run from where Bear claims it did). # replay_cdb returns non-zero on a real FAIL; assert_caught requires that. REPLAY_OUT="$(mktemp)" assert_caught "corrupted directory (replay)" \ replay_cdb "$CDB_COMPARE" "$FAULTS/bad-directory.json" 1 /nonexistent "$REPLAY_OUT" rm -f "$REPLAY_OUT" printf '\n' >&2 # 5. Blank directory => non-empty-directory invariant FAILS (a `cd ""` consumer # silently lands in the wrong tree, so the entry is non-replayable). assert_caught "blank directory (non-empty-directory)" \ "$CDB_COMPARE" invariants --format human "$FAULTS/blank-directory.json" printf '\n' >&2 # 6. Control: an honest 2-entry CDB with NO expected-count must PASS invariants # (a sanity check that the invariants do not false-fire on a clean input). if "$CDB_COMPARE" invariants --format human "$FAULTS/undercount.json" >/dev/null 2>&1; then printf 'CONTROL OK honest CDB passes invariants (no false positive)\n' >&2 PASS_COUNT=$((PASS_COUNT + 1)) else printf 'CONTROL FAIL honest CDB unexpectedly failed invariants\n' >&2 FAIL_COUNT=$((FAIL_COUNT + 1)) fi printf '\n' >&2 printf '== self-test result: %s checks ok, %s failed ==\n' "$PASS_COUNT" "$FAIL_COUNT" >&2 if [ "$FAIL_COUNT" -gt 0 ]; then printf 'SELF-TEST FAILED: a fault was not caught (or a control misfired)\n' >&2 exit 1 fi printf 'SELF-TEST PASSED: every injected fault was caught\n' >&2 exit 0 ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/results/�����������������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0021575�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/results/.gitignore�������������������������������������������0000664�0000000�0000000�00000000031�15271777203�0023557�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������* !.gitignore !README.md �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/results/README.md��������������������������������������������0000664�0000000�0000000�00000001565�15271777203�0023063�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Dogfooding run results (git-ignored) This directory holds the output of dogfooding runs. Its contents are git-ignored on purpose: only this README and the `.gitignore` are tracked, so the directory stays visible in the source viewer while run artifacts never land in `git status`. Each run writes to its own subdirectory: ``` results/<target>/<label>/ compile_commands.json the CDB Bear captured this run build.log stdout+stderr of the in-container build golden-diff.txt human-readable golden comparison (on mismatch) golden-diff.json machine-readable golden comparison (on mismatch) ``` `<label>` is supplied with `--label` (default `local`), so reruns under different labels do not overwrite each other and can be compared after the fact. The durable, reviewable goldens live separately under `tests/dogfooding/goldens/` and ARE tracked. �������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/run.sh�������������������������������������������������������0000775�0000000�0000000�00000113134�15271777203�0021242�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/sh # SPDX-License-Identifier: GPL-3.0-or-later # # Dogfooding harness entry point (Stage 2). One command spins up a per-project # throwaway build container, runs the installed release Bear inside it against # a pinned target, and passes/fails the captured compilation database against a # committed golden (dogfood-golden-regression). # # Two validation modes, selected per-target by VALIDATION in config.env: # golden (zlib, Stage 2): gate the capture against a committed golden CDB. # oracle (curl, Stage 3): gate the capture against the database CMake itself # emits, on the intersection of translation units, via cdb-compare # (dogfood-oracle-cmake, dogfood-divergence-report). # # Three target-agnostic Stage 4 modes each build+capture once (twice, for # determinism) and SKIP the golden/oracle gate: # --determinism run the build twice and compare the two captures # (dogfood-determinism). PASS iff equivalent; FAIL = real # non-determinism / a race in Bear itself. # --invariants assert structural invariants on the single capture with # cdb-compare (dogfood-invariants): non-empty arguments, no # true duplicates, and entry count within tolerance of the # object files the build produced. PASS iff all hold; FAIL = # a malformed CDB. # --replay[=N] sample up to N entries (default 20) and replay each recorded # command in its directory with -fsyntax-only, INSIDE the build # container (dogfood-replay). PASS iff all sampled entries # replay (>=1 verified); FAIL = a malformed entry; all- # inconclusive = INCONCLUSIVE. # --consumer[=N] sample up to N entries (default 20) and feed each to a real # clang-tooling consumer (clang-tidy) reading the captured # compile_commands.json, INSIDE the build container # (dogfood-clang-consumer). PASS iff the tool built the AST for # every sampled entry (>=1 verified); FAIL = the tool rejects a # well-formed entry (a semantically broken DB, e.g. a wrong/ # missing -I); all-inconclusive = INCONCLUSIVE. # # Usage: # tests/dogfooding/run.sh [--label L] [--rebless] [--keep] [zlib|curl] # tests/dogfooding/run.sh --determinism [--inject-fault] [--label L] [--keep] T # tests/dogfooding/run.sh --invariants [--label L] [--keep] T # tests/dogfooding/run.sh --replay[=N] [--label L] [--keep] T # tests/dogfooding/run.sh --consumer[=N] [--label L] [--keep] T # tests/dogfooding/run.sh --metrics [--invariants] [--label L] T # + rprof profile # tests/dogfooding/internal/selftest.sh # no container: prove the checks catch faults # # --label L name the per-run results subdirectory (default: local) # --rebless regenerate the committed golden from this run instead of # gating against it (dogfood-golden-rebless; golden targets only) # --determinism run the build twice and compare the two captures # (dogfood-determinism); skips the golden/oracle gate # --invariants assert structural invariants on one capture (dogfood-invariants) # --replay[=N] replay up to N sampled entries in-container (dogfood-replay; # default N=20; also accepts `--replay N`) # --consumer[=N] feed up to N sampled entries to a clang-tooling consumer # (clang-tidy) in-container (dogfood-clang-consumer; # default N=20; also accepts `--consumer N`) # --metrics additionally profile bear-driver's CPU/memory with rprof and # keep the full JSONL at results/<target>/<label>/metrics.jsonl # (dogfood-metrics-collect); layers on any mode, or stands alone # on a gate-less target. View/compare later with `rprof view`. # --inject-fault with --determinism, perturb the SECOND build with an extra # compiler flag so the captures legitimately diverge - a # self-test that the determinism check catches a fault # --keep keep the throwaway container(s) and scratch instead of removing # zlib|curl target name (default: zlib) # # Outcomes / exit codes (dogfood-build-failure-taxonomy): # PASS=0 FAIL=1 INCONCLUSIVE=2 ERROR=3 # Runtime model: the orchestrator is POSIX sh on the host, each target runs in # its own per-project throwaway rootless Podman container. set -eu HERE="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$HERE/../.." && pwd)" # shellcheck source=tests/dogfooding/internal/lib.sh . "$HERE/internal/lib.sh" # --- argument parsing -------------------------------------------------------- LABEL="local" REBLESS=0 KEEP=0 DETERMINISM=0 INJECT_FAULT=0 INVARIANTS=0 REPLAY=0 REPLAY_COUNT=20 CONSUMER=0 CONSUMER_COUNT=20 METRICS=0 TARGET="" while [ $# -gt 0 ]; do case "$1" in --label) shift; [ $# -gt 0 ] || finish ERROR "--label needs a value"; LABEL="$1" ;; --label=*) LABEL="${1#--label=}" ;; --rebless) REBLESS=1 ;; --determinism) DETERMINISM=1 ;; --inject-fault) INJECT_FAULT=1 ;; --invariants) INVARIANTS=1 ;; --replay) REPLAY=1 ;; --replay=*) REPLAY=1; REPLAY_COUNT="${1#--replay=}" ;; --consumer) CONSUMER=1 ;; --consumer=*) CONSUMER=1; CONSUMER_COUNT="${1#--consumer=}" ;; --metrics) METRICS=1 ;; --keep) KEEP=1 ;; -h|--help) sed -n '4,60p' "$HERE/run.sh" >&2 exit 0 ;; --*) finish ERROR "unknown option: $1" ;; *) # A bare integer right after --replay is its count (--replay N form). if [ "$REPLAY" -eq 1 ] && [ "$REPLAY_COUNT" = "20" ]; then case "$1" in *[!0-9]*) ;; # not a number: fall through to target handling *) REPLAY_COUNT="$1"; shift; continue ;; esac fi # A bare integer right after --consumer is its count (--consumer N). if [ "$CONSUMER" -eq 1 ] && [ "$CONSUMER_COUNT" = "20" ]; then case "$1" in *[!0-9]*) ;; # not a number: fall through to target handling *) CONSUMER_COUNT="$1"; shift; continue ;; esac fi if [ -n "$TARGET" ]; then finish ERROR "only one target supported, got extra: $1"; fi TARGET="$1" ;; esac shift done [ -n "$TARGET" ] || TARGET="zlib" # --determinism, --invariants, --replay, and --consumer are each a standalone # Stage 4/6 check that builds+captures once (or twice, for determinism) and # SKIPS the golden/oracle gate. They are mutually exclusive with each other and # with --rebless (no golden is involved). --inject-fault is a self-test of # --determinism only. --consumer (dogfood-clang-consumer) is wired exactly like # --replay: additive/standalone, allowed on VALIDATION=none targets, gated on an # in-container return code mapped to PASS/FAIL/INCONCLUSIVE/ERROR. MODE_COUNT=$((DETERMINISM + INVARIANTS + REPLAY + CONSUMER)) if [ "$MODE_COUNT" -gt 1 ]; then finish ERROR "--determinism, --invariants, --replay, and --consumer are mutually exclusive" fi if [ "$MODE_COUNT" -ge 1 ] && [ "$REBLESS" -eq 1 ]; then finish ERROR "--rebless applies only to the default golden gate, not the Stage 4/6 checks" fi if [ "$INJECT_FAULT" -eq 1 ] && [ "$DETERMINISM" -eq 0 ]; then finish ERROR "--inject-fault is only meaningful with --determinism" fi case "$REPLAY_COUNT" in ''|*[!0-9]*) finish ERROR "--replay count must be a positive integer: '$REPLAY_COUNT'" ;; esac if [ "$REPLAY" -eq 1 ] && [ "$REPLAY_COUNT" -lt 1 ]; then finish ERROR "--replay count must be >= 1" fi case "$CONSUMER_COUNT" in ''|*[!0-9]*) finish ERROR "--consumer count must be a positive integer: '$CONSUMER_COUNT'" ;; esac if [ "$CONSUMER" -eq 1 ] && [ "$CONSUMER_COUNT" -lt 1 ]; then finish ERROR "--consumer count must be >= 1" fi # Both values become path segments and a container-name segment; reject anything # that could traverse out of the harness tree or break a podman name. case "$TARGET" in *[!A-Za-z0-9_-]*) finish ERROR "target must be [A-Za-z0-9_-]: '$TARGET'" ;; esac case "$LABEL" in *[!A-Za-z0-9_-]*) finish ERROR "label must be [A-Za-z0-9_-]: '$LABEL'" ;; esac TARGET_DIR="$HERE/targets/$TARGET" [ -d "$TARGET_DIR" ] || finish ERROR "unknown target '$TARGET' (no $TARGET_DIR)" # shellcheck source=tests/dogfooding/targets/zlib/config.env . "$TARGET_DIR/config.env" # Per-target validation selector. zlib's config predates this selector and omits # it, so golden is the default; curl sets oracle. Large scale-check targets (the # kernel, ffmpeg) set `none`: they have no committed golden and no CMake oracle # (unverifiable / non-CMake at scale), so they are only run with the # target-agnostic Stage 4 checks (--invariants/--determinism/--replay). VALIDATION="${VALIDATION:-golden}" case "$VALIDATION" in golden|oracle|none) ;; *) finish ERROR "VALIDATION must be golden, oracle, or none, got '$VALIDATION'" ;; esac # A VALIDATION=none target has no default gate; a no-mode run cannot do anything # meaningful, so direct the caller to a Stage 4 check rather than failing later # on a missing golden. if [ "$MODE_COUNT" -eq 0 ] && [ "$VALIDATION" = "none" ] && [ "$METRICS" -eq 0 ]; then finish ERROR "target '$TARGET' has no golden/oracle gate; run it with --invariants, --determinism, --replay, --consumer, or --metrics" fi GOLDEN="$HERE/goldens/$TARGET/compile_commands.json" RESULTS_DIR="$HERE/results/$TARGET/$LABEL" mkdir -p "$RESULTS_DIR" # rprof metrics artifact path, set only when --metrics is given (dogfood-metrics- # collect). Passed as the 6th arg to build_and_capture, which wraps the build # with rprof and copies the full JSONL here for `rprof view` later. --metrics is # an additive modifier: it layers on any mode, and on a gate-less target with no # check mode it stands alone as a profiled build (the metrics ARE the output). if [ "$METRICS" -eq 1 ]; then METRICS_DEST="$RESULTS_DIR/metrics.jsonl"; else METRICS_DEST=""; fi # Image / container names derived from the Bear commit under test, so a stale # cached image is never silently reused for a different Bear. BEAR_SHA="$(cd "$REPO_ROOT" && git rev-parse --short HEAD)" BASE_TAG="bear-dogfood-base:$BEAR_SHA" TARGET_TAG="bear-dogfood-$TARGET:$BEAR_SHA" CONTAINER="bear-dogfood-$TARGET-$LABEL-$$" # Determinism runs the build twice, in two distinct fresh containers. CONTAINER2="bear-dogfood-$TARGET-$LABEL-$$-2" info "target=$TARGET label=$LABEL bear=$BEAR_SHA rebless=$REBLESS determinism=$DETERMINISM invariants=$INVARIANTS replay=$REPLAY replay_count=$REPLAY_COUNT consumer=$CONSUMER consumer_count=$CONSUMER_COUNT inject_fault=$INJECT_FAULT keep=$KEEP" # Cleanup of the throwaway container(s) unless --keep. Cached images are left in # place (mentioned in the final report); the harness only removes what it spun # up per run. cleanup() { if [ "$KEEP" -eq 1 ]; then info "--keep: leaving container(s) $CONTAINER $CONTAINER2 in place" else rm_container "$CONTAINER" rm_container "$CONTAINER2" fi } trap cleanup EXIT INT TERM # === STEP 1: PREFLIGHT (dogfood-preflight) =================================== # Fail fast with a clear diagnostic BEFORE creating any scratch, so a run never # starts only to leave a torn container behind. require_podman preflight_disk "$MIN_FREE_KIB" || finish ERROR "disk preflight failed" preflight_image "$BASE_IMAGE" || finish ERROR "pinned base image unavailable: $BASE_IMAGE" # The comparator (cdb-compare) ships inside the target image and every gate runs # it there via cdb_compare (dogfood-run-containerized), so there is no host # binary to preflight - a missing/broken image is already caught by the base and # target image builds below. # --rebless has no meaning for an oracle target (there is no committed golden; # the reference is CMake's own database, regenerated each run). if [ "$VALIDATION" = "oracle" ] && [ "$REBLESS" -eq 1 ]; then finish ERROR "--rebless applies only to golden targets; $TARGET validates against the CMake oracle" fi # === STEP 2: BUILD BASE IMAGE (dogfood-run-containerized) ===================== # Build Bear-under-test inside the base image from a 'git archive HEAD' context # (committed files ONLY; the dirty working tree, uncommitted scratch, target/ # never reach the image). A failure here is ERROR (harness / Bear build). ARCHIVE_DIR="$(mktemp -d)" cleanup_archive() { rm -rf "$ARCHIVE_DIR"; } # Chain archive cleanup onto the container cleanup. trap 'cleanup; cleanup_archive' EXIT INT TERM info "exporting committed tree (git archive HEAD) to $ARCHIVE_DIR" (cd "$REPO_ROOT" && git archive HEAD | tar -x -C "$ARCHIVE_DIR") # 'git archive HEAD' inherently carries only committed files (never the dirty # working tree, target/, or uncommitted scratch files). Assert positively # that the harness itself made it in, rather than blocklisting scratch names that # could one day be legitimately committed. [ -f "$ARCHIVE_DIR/tests/dogfooding/run.sh" ] || \ finish ERROR "git archive is missing tests/dogfooding (commit the harness before building)" info "building base image $BASE_TAG" if ! podman build \ --build-arg "BASE_IMAGE=$BASE_IMAGE" \ -f "$ARCHIVE_DIR/tests/dogfooding/base/Containerfile" \ -t "$BASE_TAG" \ "$ARCHIVE_DIR" >&2; then finish ERROR "base image build failed (Bear build or install)" fi cleanup_archive trap cleanup EXIT INT TERM # === STEP 3: BUILD TARGET IMAGE ============================================== # FROM the locally-built base. dnf/curl/sha/network failures here are the # target's own infra -> INCONCLUSIVE. A failure resolving the base would be # ERROR, but step 2 just produced it, so a failure here is target infra. # Each target pins its source under its own URL/SHA256 variable names (ZLIB_*, # CURL_*); both Containerfiles consume the same SRC_DIR. Pass the variables the # present target defines, so a target only ever sees its own build-args. info "building target image $TARGET_TAG" set -- --build-arg "BASE_TAG=$BASE_TAG" --build-arg "SRC_DIR=$SRC_DIR" [ -n "${ZLIB_URL:-}" ] && set -- "$@" --build-arg "ZLIB_URL=$ZLIB_URL" [ -n "${ZLIB_SHA256:-}" ] && set -- "$@" --build-arg "ZLIB_SHA256=$ZLIB_SHA256" [ -n "${CURL_URL:-}" ] && set -- "$@" --build-arg "CURL_URL=$CURL_URL" [ -n "${CURL_SHA256:-}" ] && set -- "$@" --build-arg "CURL_SHA256=$CURL_SHA256" if ! podman build "$@" \ -f "$TARGET_DIR/Containerfile" \ -t "$TARGET_TAG" \ "$TARGET_DIR" >&2; then finish INCONCLUSIVE "target image build failed (source fetch / sha / network / deps)" fi # === STEP 4: NON-EMPTY-CAPTURE SMOKE ========================================= # (dogfood-preflight + dogfood-run-containerized) Before the real build, prove # interception actually works: a wrong libexec/INTERCEPT_LIBDIR layout makes # Bear run yet capture nothing. Empty capture => ERROR. info "smoke: verifying Bear captures a trivial compile" SMOKE_OUT="$(podman run --rm --systemd=always "$TARGET_TAG" sh -c ' set -e d="$(mktemp -d)" cd "$d" printf "int main(void){return 0;}\n" > smoke.c bear --output cc.json -- gcc -c smoke.c -o smoke.o >/dev/null 2>&1 cat cc.json ' 2>/dev/null)" || SMOKE_OUT="" case "$SMOKE_OUT" in *smoke.c*) info "smoke: capture OK" ;; *) err "smoke capture empty or missing smoke.c" err "diagnostic: libexec/INTERCEPT_LIBDIR mismatch: Bear ran but captured nothing" finish ERROR "non-empty-capture smoke failed (interception not working)" ;; esac # === STEP 5 (determinism): TWO REAL RUNS + COMPARE (dogfood-determinism) ====== # Stage 4 self-check, target-agnostic: run the SAME target's build twice in two # fresh throwaway containers off the same pinned image, capture Bear's database # from each, and compare the two captures. The fixed build paths (/src, /build) # make the two captures multiset-equivalent, and cdb-compare is order- # independent, so build parallelism is fine - NO normalization flags are needed. # The build is its own reference: no golden, no oracle. # # PASS iff the two captures are equivalent. FAIL means real non-determinism / a # race in Bear itself (the diff is saved). Either build failing for its own # reasons is INCONCLUSIVE; infra/empty-capture is ERROR. All of that taxonomy # lives in build_and_capture (lib.sh). # # --inject-fault is the self-test: it perturbs the SECOND build with an extra # compiler flag (INJECT_CFLAGS, threaded into the build by config.env) so the # two captures legitimately diverge and the check is shown to FAIL. We do NOT # edit captured JSON by hand; the fault is a real, different second build. if [ "$DETERMINISM" -eq 1 ]; then RUN1="$RESULTS_DIR/compile_commands.run1.json" RUN2="$RESULTS_DIR/compile_commands.run2.json" LOG1="$RESULTS_DIR/build.run1.log" LOG2="$RESULTS_DIR/build.run2.log" if [ "$METRICS" -eq 1 ]; then M1="$RESULTS_DIR/metrics.run1.jsonl"; M2="$RESULTS_DIR/metrics.run2.jsonl"; else M1=""; M2=""; fi INJECT2="" if [ "$INJECT_FAULT" -eq 1 ]; then # A perturbing extra macro definition. config.env threads INJECT_CFLAGS # into the second build's compiler flags, so this lands in the recorded # arguments and makes run2 genuinely differ from run1. INJECT2="-DBEAR_DOGFOOD_INJECTED_FAULT=1" warn "inject-fault: second build perturbed with INJECT_CFLAGS='$INJECT2'" fi info "determinism run 1/2 (container $CONTAINER)" build_and_capture "$CONTAINER" "$RUN1" "$LOG1" "" "" "$M1" info "determinism run 2/2 (container $CONTAINER2)" build_and_capture "$CONTAINER2" "$RUN2" "$LOG2" "$INJECT2" "" "$M2" DIFF_HUMAN="$RESULTS_DIR/determinism-diff.txt" DIFF_JSON="$RESULTS_DIR/determinism-diff.json" info "comparing the two captures (cdb-compare compare, no normalization)" if cdb_compare compare "/run/${RUN1##*/}" "/run/${RUN2##*/}" >"$DIFF_HUMAN" 2>&1; then cat "$DIFF_HUMAN" >&2 finish PASS "the two captures are equivalent (no non-determinism)" else cat "$DIFF_HUMAN" >&2 cdb_compare compare --format json "/run/${RUN1##*/}" "/run/${RUN2##*/}" >"$DIFF_JSON" 2>&1 || true err "the two captures differ; diffs saved to $DIFF_HUMAN and $DIFF_JSON" finish FAIL "captures differ across two identical builds (Bear non-determinism) - see $DIFF_HUMAN" fi fi # === STEP 5 (invariants): ONE BUILD + STRUCTURAL INVARIANTS (dogfood-invariants) # Build+capture once, then assert structural invariants on the single capture # with the image's cdb-compare (via cdb_compare), and gate on its exit code. No # golden, no oracle. The checks: non-empty-arguments and no-true-duplicates # (always), plus entry-count against the number of object files the build # actually produced (opt-in via --expected-objects). # # The object count is taken IN the container before teardown by a per-target # OBJECT_COUNT_CMD (config.env): curl uses the default `find $OBJECTS_DIR -name # '*.o' | wc -l` (its CMake objects persist), zlib counts make's dependency # graph (`make -Bn`) because its in-tree build deletes its PIC objects. The # count is written to /out/object_count, pulled out, and fed to # --expected-objects with a per-target --tolerance band (OBJECT_TOLERANCE_PCT) # so a stray build-system .o does not false-fail. # # PASS = invariants hold; FAIL = Bear produced a malformed CDB; build failure # is INCONCLUSIVE and infra is ERROR (both handled in build_and_capture). if [ "$INVARIANTS" -eq 1 ]; then FRESH="$RESULTS_DIR/compile_commands.json" BUILD_LOG="$RESULTS_DIR/build.log" OBJECTS_DIR="${OBJECTS_DIR:?config.env must set OBJECTS_DIR for invariants mode}" OBJ_TOL="${OBJECT_TOLERANCE_PCT:-10}" # Post-build step: count the object files the build PRODUCED and write the # number to /out/object_count. The instrument is per-target (OBJECT_COUNT_CMD # in config.env) because "objects produced" is not always "*.o files still on # disk": curl's CMake leaves every object under /build, so a `find` is exact # (the default below), but zlib's in-tree make deletes its PIC objects under # objs/ at link time, so a post-build find undercounts ~2x. zlib therefore # overrides OBJECT_COUNT_CMD to count make's own dependency graph # (`make -Bn`), a build-system-native, cleanup-independent, Bear-independent # count of produced objects. Default: count surviving *.o under OBJECTS_DIR. OBJECT_COUNT_CMD="${OBJECT_COUNT_CMD:-find \"$OBJECTS_DIR\" -name '*.o' 2>/dev/null | wc -l}" INV_POST="{ $OBJECT_COUNT_CMD ; } > /out/object_count 2>/dev/null" build_and_capture "$CONTAINER" "$FRESH" "$BUILD_LOG" "" "$INV_POST" "$METRICS_DEST" OBJ_COUNT_FILE="$RESULTS_DIR/object_count" if ! podman cp "$CONTAINER:/out/object_count" "$OBJ_COUNT_FILE" >&2; then finish ERROR "could not copy object_count out of the container" fi OBJ_COUNT="$(tr -d ' \n\r\t' < "$OBJ_COUNT_FILE")" ENTRY_COUNT="$(grep -c '"file"' "$FRESH" 2>/dev/null || echo 0)" # The entry-count cross-check is opt-in and DEGRADES GRACEFULLY: the # always-on structural invariants (non-empty-arguments, no-true-duplicates) # must not be blocked just because a per-target object-count instrument could # not produce a number (some large build systems make an independent compile # count genuinely hard - the kernel's recursive Kbuild, for one). When the # count is missing/zero, warn and skip only entry-count; still run the rest. case "$OBJ_COUNT" in ''|*[!0-9]*|0) warn "object count unavailable ('$OBJ_COUNT') from OBJECT_COUNT_CMD; skipping the entry-count cross-check (structural invariants still run)" COUNT_ARGS="" OBJ_COUNT="unavailable" ;; *) COUNT_ARGS="--expected-objects $OBJ_COUNT --tolerance $OBJ_TOL" ;; esac info "invariants: object_count=$OBJ_COUNT entry_count=$ENTRY_COUNT tolerance=${OBJ_TOL}%" REPORT="$RESULTS_DIR/invariants-report.txt" info "asserting structural invariants (cdb-compare invariants)" set +e # shellcheck disable=SC2086 # COUNT_ARGS is a deliberate word list (may be empty) cdb_compare invariants --drop-dependency-flags $COUNT_ARGS \ --format human "/run/${FRESH##*/}" >"$REPORT" 2>&1 INV_RC=$? set -e cat "$REPORT" >&2 case "$INV_RC" in 0) finish PASS "structural invariants hold (entries=$ENTRY_COUNT objects=$OBJ_COUNT); see $REPORT" ;; 1) err "an invariant failed; see $REPORT" finish FAIL "Bear produced a malformed CDB (invariant violated) - see $REPORT" ;; *) finish ERROR "cdb-compare invariants failed to run (exit $INV_RC); see $REPORT" ;; esac fi # === STEP 5 (replay): ONE BUILD + REPLAY A SAMPLE (dogfood-replay) ============ # Stage 4 self-check: build+capture once, then take a sample of Bear's entries # and replay each recorded command in its recorded directory with -fsyntax-only # appended, verifying the compiler accepts the recorded arguments. This runs # INSIDE the build container as part of the SAME podman run, because the # recorded sources and generated headers exist there only before teardown. The # replay loop (replay-loop.sh) is read into the in-container script; the # in-image cdb-compare at /opt/bear/bin/cdb-compare drives the sampling. # # Categorization (per replay-loop.sh): OK / INCONCLUSIVE (missing generated # header) / FAIL (malformed entry). Gate: any real FAIL => FAIL; all OK # (inconclusive allowed) => PASS; every sampled entry inconclusive (nothing # verified) => INCONCLUSIVE (do not pass vacuously). if [ "$REPLAY" -eq 1 ]; then FRESH="$RESULTS_DIR/compile_commands.json" BUILD_LOG="$RESULTS_DIR/build.log" REPLAY_BUILD_DIR="${BUILD_DIR:?config.env must set BUILD_DIR for replay mode}" # Verify the in-image cdb-compare exists before relying on it. if ! podman run --rm "$TARGET_TAG" test -x /opt/bear/bin/cdb-compare; then finish ERROR "in-image cdb-compare missing at /opt/bear/bin/cdb-compare" fi # Build the in-container post-build snippet: the replay function definition # (from replay-loop.sh) followed by one call against Bear's capture. The # function writes its OK/FAIL/INCONCLUSIVE tally and any failing commands to # /out/replay_result; its return code is recorded to /out/replay_rc so the # host can gate on it without parsing JSON. [ -f "$HERE/internal/replay-loop.sh" ] || finish ERROR "internal/replay-loop.sh missing (commit the harness)" REPLAY_FN="$(cat "$HERE/internal/replay-loop.sh")" REPLAY_POST="$REPLAY_FN replay_cdb /opt/bear/bin/cdb-compare /out/compile_commands.json $REPLAY_COUNT \"$REPLAY_BUILD_DIR\" /out/replay_result echo \$? > /out/replay_rc cat /out/replay_result" build_and_capture "$CONTAINER" "$FRESH" "$BUILD_LOG" "" "$REPLAY_POST" "$METRICS_DEST" REPLAY_RESULT="$RESULTS_DIR/replay_result" REPLAY_RC_FILE="$RESULTS_DIR/replay_rc" if ! podman cp "$CONTAINER:/out/replay_result" "$REPLAY_RESULT" >&2; then finish ERROR "could not copy replay_result out of the container" fi if ! podman cp "$CONTAINER:/out/replay_rc" "$REPLAY_RC_FILE" >&2; then finish ERROR "could not copy replay_rc out of the container" fi cat "$REPLAY_RESULT" >&2 REPLAY_RC="$(tr -d ' \n\r\t' < "$REPLAY_RC_FILE")" TALLY="$(sed -n 's/^replay: //p' "$REPLAY_RESULT")" info "replay: $TALLY (build-dir $REPLAY_BUILD_DIR, sampled up to $REPLAY_COUNT)" case "$REPLAY_RC" in 0) finish PASS "all sampled entries replayed (at least one verified): $TALLY" ;; 1) err "a recorded command failed to replay (malformed entry); see $REPLAY_RESULT" finish FAIL "replay failed - a malformed entry was found ($TALLY); see $REPLAY_RESULT" ;; 2) finish INCONCLUSIVE "every sampled entry was inconclusive (missing generated inputs); replay verified nothing ($TALLY)" ;; 3) finish ERROR "in-container cdb-compare sample failed; replay could not start - see $REPLAY_RESULT" ;; *) finish ERROR "replay loop did not run to completion (rc='$REPLAY_RC'); see $REPLAY_RESULT" ;; esac fi # === STEP 5 (consumer): ONE BUILD + CLANG CONSUMER OVER A SAMPLE ============== # (dogfood-clang-consumer) Stage 6's one real deliverable. Build+capture once, # then sample Bear's entries and feed each to a REAL clang-tooling consumer # (clang-tidy) reading the captured compile_commands.json, asserting the tool # can build the AST. This is the only check that validates the database WORKS in # the tool it exists for - catching a structurally-valid but semantically-broken # DB (a wrong/missing -I, a dropped -x or sysroot) that the other checks miss. # # Like replay, it runs INSIDE the build container as part of the SAME podman run, # because the consumer needs the recorded sources and generated headers, which # exist there only before teardown - and because the clang tooling lives in the # image (the host has no clang). The consumer loop (consumer-loop.sh) is read # into the in-container script; the in-image clang-tidy consumes the gcc-recorded # database, mangling driver-incompatible flags itself. # # Categorization (per consumer-loop.sh): OK (AST built) / INCONCLUSIVE (missing # generated header, or a benign gcc-vs-clang flag clang-tidy cannot consume - a # cross-compiler difference, NOT a DB defect) / FAIL (the tool rejects a # well-formed entry: the canonical case is a wrong/missing -I). Gate (mirroring # replay): any FAIL => FAIL; all OK (inconclusive allowed) => PASS; every sampled # entry inconclusive (nothing consumed) => INCONCLUSIVE (no vacuous PASS). if [ "$CONSUMER" -eq 1 ]; then FRESH="$RESULTS_DIR/compile_commands.json" BUILD_LOG="$RESULTS_DIR/build.log" CONSUMER_BUILD_DIR="${BUILD_DIR:?config.env must set BUILD_DIR for consumer mode}" # Verify the in-image cdb-compare and clang-tidy exist before relying on them. if ! podman run --rm "$TARGET_TAG" test -x /opt/bear/bin/cdb-compare; then finish ERROR "in-image cdb-compare missing at /opt/bear/bin/cdb-compare" fi if ! podman run --rm "$TARGET_TAG" sh -c 'command -v clang-tidy >/dev/null 2>&1'; then finish ERROR "in-image clang-tidy missing (base image must install clang-tools-extra)" fi # Build the in-container post-build snippet: the consumer function definition # (from consumer-loop.sh) followed by one call against Bear's capture. The # function writes its OK/FAIL/INCONCLUSIVE tally and any rejected entries to # /out/consumer_result; its return code is recorded to /out/consumer_rc so the # host gates on it without parsing JSON. The capture stays at /out alongside # consumer_result; clang-tidy reads it via the -p flag the consumer loop passes. [ -f "$HERE/internal/consumer-loop.sh" ] || finish ERROR "internal/consumer-loop.sh missing (commit the harness)" CONSUMER_FN="$(cat "$HERE/internal/consumer-loop.sh")" CONSUMER_POST="$CONSUMER_FN consumer_cdb /opt/bear/bin/cdb-compare /out/compile_commands.json $CONSUMER_COUNT \"$CONSUMER_BUILD_DIR\" /out/consumer_result echo \$? > /out/consumer_rc cat /out/consumer_result" build_and_capture "$CONTAINER" "$FRESH" "$BUILD_LOG" "" "$CONSUMER_POST" "$METRICS_DEST" CONSUMER_RESULT="$RESULTS_DIR/consumer_result" CONSUMER_RC_FILE="$RESULTS_DIR/consumer_rc" if ! podman cp "$CONTAINER:/out/consumer_result" "$CONSUMER_RESULT" >&2; then finish ERROR "could not copy consumer_result out of the container" fi if ! podman cp "$CONTAINER:/out/consumer_rc" "$CONSUMER_RC_FILE" >&2; then finish ERROR "could not copy consumer_rc out of the container" fi cat "$CONSUMER_RESULT" >&2 CONSUMER_RC="$(tr -d ' \n\r\t' < "$CONSUMER_RC_FILE")" TALLY="$(sed -n 's/^consumer: //p' "$CONSUMER_RESULT")" info "consumer: $TALLY (build-dir $CONSUMER_BUILD_DIR, sampled up to $CONSUMER_COUNT)" case "$CONSUMER_RC" in 0) finish PASS "clang-tidy built the AST for all sampled entries (at least one verified): $TALLY" ;; 1) err "clang-tidy rejected a well-formed entry (semantically broken DB); see $CONSUMER_RESULT" finish FAIL "clang consumer failed - the database does not work in the tool ($TALLY); see $CONSUMER_RESULT" ;; 2) finish INCONCLUSIVE "every sampled entry was inconclusive (missing inputs / gcc-vs-clang flags); the consumer verified nothing ($TALLY)" ;; 3) finish ERROR "in-container cdb-compare sample failed; the consumer could not start - see $CONSUMER_RESULT" ;; *) finish ERROR "consumer loop did not run to completion (rc='$CONSUMER_RC'); see $CONSUMER_RESULT" ;; esac fi # === STEP 5 (metrics-only): PROFILED BUILD ON A GATE-LESS TARGET ============== # --metrics with no check mode on a VALIDATION=none target: a profiled # build+capture whose deliverable is the rprof artifact (kept whole for # `rprof view`). Reachable only with --metrics, since the no-mode guard above # rejects a gate-less target otherwise. if [ "$MODE_COUNT" -eq 0 ] && [ "$VALIDATION" = "none" ]; then FRESH="$RESULTS_DIR/compile_commands.json" build_and_capture "$CONTAINER" "$FRESH" "$RESULTS_DIR/build.log" "" "" "$METRICS_DEST" ENTRY_COUNT="$(grep -c '"file"' "$FRESH" 2>/dev/null || echo 0)" finish PASS "metrics-only run: $ENTRY_COUNT entries captured; rprof metrics at $METRICS_DEST" fi # === STEP 5: REAL RUN (dogfood-run-containerized + dogfood-fixed-paths) ======= # Run the real build wrapped by Bear at fixed path /src. Configure/make failure # => INCONCLUSIVE (target's own reasons). Empty captured CDB => ERROR. The # build-and-capture body is shared with the determinism path (lib.sh). FRESH="$RESULTS_DIR/compile_commands.json" BUILD_LOG="$RESULTS_DIR/build.log" build_and_capture "$CONTAINER" "$FRESH" "$BUILD_LOG" "" "" "$METRICS_DEST" # For oracle targets, also pull CMake's own database (the reference oracle the # in-container build wrote to /out/oracle.json). ORACLE="$RESULTS_DIR/oracle.json" if [ "$VALIDATION" = "oracle" ]; then if ! podman cp "$CONTAINER:/out/oracle.json" "$ORACLE" >&2; then finish ERROR "could not copy oracle.json (CMake's database) out of the container" fi if ! grep -q '"file"' "$ORACLE" 2>/dev/null; then err "CMake oracle DB has no entries: $ORACLE" finish ERROR "empty CMake oracle database (configure did not export compile_commands)" fi info "captured oracle CDB: $ORACLE" fi # === STEP 6: GATE ============================================================ # Dispatch on the per-target validation selector: oracle (curl, Stage 3) or # golden (zlib, Stage 2). if [ "$VALIDATION" = "oracle" ]; then # --- oracle gate (dogfood-oracle-cmake, dogfood-divergence-report) ------- # cdb-compare does the whole comparison; no jq, no allow-list file: # --output-from-o match TUs by absolute object path (directory + the # -o argument), so the two producers' differing # `output` encodings align and a source compiled # into several targets stays distinct. # --drop-dependency-flags drop the benign `-M*` depfile flags the make-time # command carries but CMake's configure-time export # omits (the only difference on matched TUs). # --substitute-compiler absorb any compiler-driver path difference. # --intersection gate on matched-but-differing TUs only; entries on # just one side are advisory extras (the divergence # report), never a failure. # Exit 0 = matched TUs equivalent (PASS); 1 = real divergence OR a load error. REPORT="$RESULTS_DIR/oracle-report.txt" REPORT_JSON="$RESULTS_DIR/oracle-compare.json" NORM_FLAGS="--substitute-compiler cc --output-from-o --drop-dependency-flags" info "comparing Bear vs CMake on the TU intersection (cdb-compare)" set +e # shellcheck disable=SC2086 # NORM_FLAGS is a deliberate word list cdb_compare compare --intersection $NORM_FLAGS "/run/${FRESH##*/}" "/run/${ORACLE##*/}" >"$REPORT" 2>&1 ORACLE_RC=$? # Archive a machine-readable copy of the three-set report (same normalization). # shellcheck disable=SC2086 cdb_compare compare $NORM_FLAGS --format json "/run/${FRESH##*/}" "/run/${ORACLE##*/}" >"$REPORT_JSON" 2>/dev/null set -e # The `summary:` line is printed only when cdb-compare ran to completion in # --intersection mode; its absence means a load/parse error (cdb-compare exits # 1 for that too), so treat a missing summary as an infra ERROR, not a FAIL. SUMMARY="$(sed -n 's/^summary: //p' "$REPORT")" if [ -z "$SUMMARY" ]; then finish ERROR "cdb-compare did not produce an oracle comparison; see $REPORT" fi info "oracle: $SUMMARY" info "full divergence report: $REPORT (machine-readable: $REPORT_JSON)" # Non-vacuity / coverage floor: a zero-matched intersection compared nothing # (matching broken, or the inputs share no TUs) and must not pass. cdb-compare # is slated to enforce this itself; until then the harness refuses it. MATCHED="$(printf '%s' "$SUMMARY" | sed -n 's/^matched=\([0-9]*\) .*/\1/p')" if [ -z "$MATCHED" ] || [ "$MATCHED" -eq 0 ]; then finish ERROR "oracle matched 0 translation units - nothing was compared (matching broken?); see $REPORT" fi case "$ORACLE_RC" in 0) finish PASS "matched TUs equivalent to the CMake oracle ($SUMMARY)" ;; 1) err "oracle mismatch: matched TUs diverge from CMake's database" err "see the 'matched but differing' section of $REPORT (and $REPORT_JSON)" finish FAIL "oracle mismatch - matched TUs differ from the CMake oracle; see $REPORT" ;; *) finish ERROR "cdb-compare failed during the oracle comparison (exit $ORACLE_RC); see $REPORT" ;; esac fi # === STEP 6 (golden): GATE (dogfood-golden-regression) ======================= # The image's cdb-compare (via cdb_compare) gates the fresh CDB against the # committed golden. On --rebless, write the golden instead (dogfood-golden-rebless). # # Per the resolved decision, no normalization flags are used: the same pinned # image and fixed /src path make the raw multiset reproducible. If a benign # compiler-path diff ever appears, add --substitute-compiler cc to BOTH the # bless and the check below, consistently. if [ "$REBLESS" -eq 1 ]; then info "reblessing golden: $GOLDEN" mkdir -p "$(dirname "$GOLDEN")" # normalize writes to stdout (no -o); capture it on the host, then install it # atomically so a failed normalize never leaves a truncated golden. if ! cdb_compare normalize --sort "/run/${FRESH##*/}" >"$RESULTS_DIR/golden.new.json"; then rm -f "$RESULTS_DIR/golden.new.json" finish ERROR "cdb-compare normalize failed during rebless" fi mv "$RESULTS_DIR/golden.new.json" "$GOLDEN" info "golden rewritten from this run; review and commit it" finish PASS "reblessed golden at $GOLDEN" fi if [ ! -f "$GOLDEN" ]; then err "no golden at $GOLDEN; produce one with: $0 --rebless $TARGET" finish ERROR "missing golden (run --rebless first)" fi info "gating fresh CDB against golden" DIFF_HUMAN="$RESULTS_DIR/golden-diff.txt" DIFF_JSON="$RESULTS_DIR/golden-diff.json" # Stage the committed golden into the run dir so the single read-only /run mount # covers both sides of the comparison (the fresh capture already lives there). cp "$GOLDEN" "$RESULTS_DIR/golden.json" if cdb_compare compare /run/golden.json "/run/${FRESH##*/}" >"$DIFF_HUMAN" 2>&1; then cat "$DIFF_HUMAN" >&2 finish PASS "fresh CDB matches golden (no regression)" else cat "$DIFF_HUMAN" >&2 # Save a machine-readable diff alongside the human one for review. cdb_compare compare --format json /run/golden.json "/run/${FRESH##*/}" >"$DIFF_JSON" 2>&1 || true err "golden mismatch; diffs saved to $DIFF_HUMAN and $DIFF_JSON" finish FAIL "fresh CDB differs from golden (regression) - see $DIFF_HUMAN" fi ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/targets/�����������������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0021545�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/targets/curl/������������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0022512�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/targets/curl/Containerfile�����������������������������������0000664�0000000�0000000�00000002435�15271777203�0025223�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SPDX-License-Identifier: GPL-3.0-or-later # # curl target image (Stage 3, oracle). Layers on the locally-built dogfooding # base (which carries Bear-under-test + cdb-compare) and installs curl's # minimal CMake build dependencies plus the pinned, sha256-verified source at # the fixed path /src (dogfood-fixed-paths). The actual CMake configure+build # is run at 'podman run' time (so Bear intercepts the build), NOT here, so the # image build stays free of any Bear invocation. ARG BASE_TAG FROM ${BASE_TAG} ARG CURL_URL ARG CURL_SHA256 ARG SRC_DIR=/src # Target build deps: cmake + gcc + make build curl with all optional # dependencies disabled (see config.env's -D...=OFF flags); the system curl # binary + tar fetch and unpack the pinned source. RUN dnf -y install cmake gcc make curl tar gzip \ && dnf -y clean all \ && rm -rf /var/cache/dnf # Fetch, verify by sha256, extract to the fixed SRC_DIR. A checksum mismatch # fails the build (caught by the harness as INCONCLUSIVE: target infra). RUN mkdir -p "${SRC_DIR}" \ && curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/curl.tar.gz "${CURL_URL}" \ && echo "${CURL_SHA256} /tmp/curl.tar.gz" | sha256sum -c - \ && tar -xzf /tmp/curl.tar.gz -C "${SRC_DIR}" --strip-components=1 \ && rm -f /tmp/curl.tar.gz WORKDIR ${SRC_DIR} �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/targets/curl/config.env��������������������������������������0000664�0000000�0000000�00000011373�15271777203�0024476�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SPDX-License-Identifier: GPL-3.0-or-later # # Pinned configuration for the curl dogfooding target (Stage 3, oracle). # A dogfooding configuration is (base image digest, target source revision, # target build type); all are pinned so the comparison stays stable # (dogfood-pinned-target). Sourced by run.sh; values are also consumed by the # target Containerfile via podman build --build-arg. # Validation mode for this target: 'oracle' compares Bear's capture against # the database CMake itself emits (CMAKE_EXPORT_COMPILE_COMMANDS=ON), scoped to # the intersection of translation units (dogfood-oracle-cmake). zlib uses # 'golden'. run.sh dispatches on this selector. VALIDATION="oracle" # Pinned base image: the same digest zlib uses (this IS fedora:44, the # devcontainer base). An unpinned base would let a package update change the # compiler under a fixed comparison and produce phantom diffs. BASE_IMAGE="registry.fedoraproject.org/fedora@sha256:3baf5f0dededfd939eb8f0b271ff8ad17bdb381cdd5768bd7d6f45bba795aa62" # Pinned curl source. CURL_VERSION="8.11.1" CURL_URL="https://github.com/curl/curl/releases/download/curl-8_11_1/curl-8.11.1.tar.gz" CURL_SHA256="a889ac9dbba3644271bd9d1302b5c22a088893719b72be3487bc3d401e5c4e80" # Pinned build type. Release is representative of shipped use. BUILD_TYPE="Release" # Fixed in-container paths (dogfood-fixed-paths). curl builds with CMake, which # supports out-of-tree builds, so source and build are separate fixed paths. # Fixed paths keep absolute paths in 'directory'/'file' reproducible. BUILD_DIR # is also the build dir passed to the replay sampler (dogfood-replay) as # `cdb-compare sample --build-dir`, so it prefers TUs whose flags do not # reference build-dir includes (more of curl's TUs then replay cleanly). SRC_DIR="/src" BUILD_DIR="/build" # Object-count instrument for the entry-count invariant (dogfood-invariants). # curl's CMake build leaves every object on disk under the out-of-tree build # dir /build (CMakeFiles/*.dir/*.o), so the DEFAULT instrument - a post-build # `find $OBJECTS_DIR -name '*.o' | wc -l` - is exact; curl does NOT override # OBJECT_COUNT_CMD. (Contrast zlib, which deletes its PIC objects and must count # make's dependency graph instead.) # shellcheck disable=SC2034 OBJECTS_DIR="/build" # Tolerance percent for the entry-count band # (--expected-objects N --tolerance OBJECT_TOLERANCE_PCT). curl compiles most # library sources twice (a shared-lib PIC object and a static object), so the # entry count tracks the object count closely; 10% absorbs CMake's own # feature-probe scratch objects (CMakeScratch / try_compile) that land under # /build but are not library TUs, without a false fail. # shellcheck disable=SC2034 OBJECT_TOLERANCE_PCT="10" # Per-target preflight minimum free disk on the podman graphroot, in KiB # (dogfood-preflight). curl needs more than zlib: base + target images plus a # full CMake configure+build tree. ~4 GiB is a safe floor. MIN_FREE_KIB="4194304" # The build command run INSIDE the target container. Two phases: # # 1. CMake CONFIGURE (NOT wrapped by Bear - configure is not a compile). It # emits CMake's own oracle database at /build/compile_commands.json via # CMAKE_EXPORT_COMPILE_COMMANDS=ON. Optional dependencies are turned OFF so # curl configures and builds with just cmake + gcc + make: no TLS backend, # no nghttp2/zlib/zstd/brotli/psl/idn/ssh/ldap/librtmp. This keeps the # image minimal and the build deterministic. # 2. BUILD wrapped by Bear -> Bear's capture at /out/compile_commands.json. # # Finally CMake's oracle DB is copied to /out/oracle.json. Both DBs are pulled # out of the stopped container with 'podman cp'. run.sh's oracle branch then # compares them on the intersection of files. # # INJECT_CFLAGS is the determinism self-test hook (dogfood-determinism): empty # on every normal run and on determinism run 1, it carries a perturbing flag on # determinism run 2 under --inject-fault. It is appended to CMAKE_C_FLAGS so it # lands in the recorded compiler arguments and the two captures diverge, showing # the determinism check FAIL. /build is fresh each run (a new container), so the # CMake cache is regenerated and the flag takes effect. # shellcheck disable=SC2034 TARGET_BUILD_CMD='mkdir -p /out && cmake -S /src -B /build -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="${INJECT_CFLAGS:-}" -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCURL_USE_OPENSSL=OFF -DCURL_USE_LIBSSH2=OFF -DCURL_USE_LIBSSH=OFF -DCURL_USE_LIBPSL=OFF -DCURL_USE_GSSAPI=OFF -DUSE_NGHTTP2=OFF -DUSE_LIBIDN2=OFF -DCURL_BROTLI=OFF -DCURL_ZSTD=OFF -DCURL_ZLIB=OFF -DUSE_LIBRTMP=OFF -DCURL_DISABLE_LDAP=ON -DCURL_DISABLE_LDAPS=ON -DBUILD_CURL_EXE=ON -DBUILD_SHARED_LIBS=ON && ${RPROF_PREFIX:-} bear --output /out/compile_commands.json -- cmake --build /build && cp /build/compile_commands.json /out/oracle.json' ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/targets/ffmpeg/����������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0023011�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/targets/ffmpeg/Containerfile���������������������������������0000664�0000000�0000000�00000004023�15271777203�0025515�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SPDX-License-Identifier: GPL-3.0-or-later # # FFmpeg target image (Stage 4, scale validation). Layers on the locally-built # dogfooding base (which carries Bear-under-test + cdb-compare) and installs # FFmpeg's minimal build dependencies plus the pinned, sha256-verified source at # the fixed path /src (dogfood-fixed-paths). FFmpeg uses a HAND-WRITTEN POSIX-sh # `configure` (no autotools, no CMake), which is the build-system variety this # midsize target adds to the harness. The actual 'make' is wrapped by Bear at # 'podman run' time, NOT here, so the image build stays free of any Bear call. ARG BASE_TAG FROM ${BASE_TAG} ARG SRC_DIR=/src # Target build deps: gcc + make + perl build FFmpeg (perl drives some of the # build's text generation); pkgconfig + diffutils are probed by FFmpeg's # configure; curl fetches the source; tar + xz unpack the .tar.xz source. No # nasm/yasm: the configure flags disable all assembly, so FFmpeg builds with # just gcc + make (see config.env's TARGET_BUILD_CMD). RUN dnf -y install gcc make perl curl tar xz gzip pkgconfig diffutils \ && dnf -y clean all \ && rm -rf /var/cache/dnf # Pinned FFmpeg source. Unlike zlib/curl, the URL and sha256 are HARDCODED here # (not build-args): the pin is part of this target's definition and run.sh does # not thread an FFMPEG_URL through. ffmpeg-7.1 is a stable release from the # upstream release archive; the sha256 was verified against the downloaded # tarball. A checksum mismatch fails the build (caught by the harness as # INCONCLUSIVE: target infra). ARG FFMPEG_URL="https://ffmpeg.org/releases/ffmpeg-7.1.tar.xz" ARG FFMPEG_SHA256="40973d44970dbc83ef302b0609f2e74982be2d85916dd2ee7472d30678a7abe6" # Fetch, verify by sha256, extract to the fixed SRC_DIR. RUN mkdir -p "${SRC_DIR}" \ && curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/ffmpeg.tar.xz "${FFMPEG_URL}" \ && echo "${FFMPEG_SHA256} /tmp/ffmpeg.tar.xz" | sha256sum -c - \ && tar -xJf /tmp/ffmpeg.tar.xz -C "${SRC_DIR}" --strip-components=1 \ && rm -f /tmp/ffmpeg.tar.xz WORKDIR ${SRC_DIR} �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/targets/ffmpeg/config.env������������������������������������0000664�0000000�0000000�00000012331�15271777203�0024770�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SPDX-License-Identifier: GPL-3.0-or-later # # Pinned configuration for the FFmpeg dogfooding target (Stage 4, scale). # FFmpeg is a MIDSIZE scale-validation target (~1945 translation units, between # curl's ~221 and the kernel's thousands) that exercises a build system the # harness did not previously cover: FFmpeg's HAND-WRITTEN POSIX-sh `configure` # (not autotools, not CMake). A dogfooding configuration is (base image digest, # target source revision, target build type); all are pinned so the checks stay # stable (dogfood-pinned-target). Sourced by run.sh; SRC_DIR is also consumed by # the target Containerfile via podman build --build-arg. # Validation mode for this target: 'none'. A scale target has no committed # golden (too large to bless and review) and no CMake oracle (FFmpeg does not # use CMake), so it is run ONLY with the target-agnostic Stage 4 checks # (--invariants / --determinism / --replay). run.sh dispatches on this selector. # shellcheck disable=SC2034 VALIDATION="none" # Pinned base image: the same digest zlib/curl use (this IS fedora:44, the # devcontainer base). An unpinned base would let a package update change the # compiler under the fixed checks and produce phantom diffs. # shellcheck disable=SC2034 BASE_IMAGE="registry.fedoraproject.org/fedora@sha256:3baf5f0dededfd939eb8f0b271ff8ad17bdb381cdd5768bd7d6f45bba795aa62" # Fixed in-container build path (dogfood-fixed-paths). FFmpeg builds IN-TREE by # default (objects land next to their sources under /src); its configure has no # out-of-tree mode worth the extra moving parts, so source, objects, and build # dir are all this one fixed path. A fixed path keeps absolute paths in # 'directory' reproducible across runs (so the determinism check needs no # normalization). # shellcheck disable=SC2034 SRC_DIR="/src" # Object-count instrument for the entry-count invariant (dogfood-invariants). # FFmpeg's in-tree make leaves EVERY object on disk under /src (it does not # delete PIC duplicates the way zlib does, and it compiles each source exactly # once - no shared/static double-compile the way curl does), so the DEFAULT # instrument - a post-build `find $OBJECTS_DIR -name '*.o' | wc -l` - is exact. # Verified empirically on a real build: find=1945, make-graph=1945, and Bear's # entry_count=1945 all agree exactly, so FFmpeg does NOT override # OBJECT_COUNT_CMD. (Contrast zlib, which deletes its PIC objects and must count # make's dependency graph instead.) # shellcheck disable=SC2034 OBJECTS_DIR="/src" # Build directory passed to the replay sampler (dogfood-replay) as # `cdb-compare sample --build-dir`. FFmpeg builds in-tree, so its build dir IS # /src. # shellcheck disable=SC2034 BUILD_DIR="/src" # Tolerance percent for the entry-count band # (--expected-objects N --tolerance OBJECT_TOLERANCE_PCT). The find instrument # matched Bear's entry count EXACTLY on a real build (1945 == 1945), so the band # only needs to absorb an incidental build-system object across FFmpeg releases. # 10% is deliberately small headroom: wide enough that a stray generated .o does # not false-fail, tight enough that a real entry drop or duplication still trips # the check. # shellcheck disable=SC2034 OBJECT_TOLERANCE_PCT="10" # Per-target preflight minimum free disk on the podman graphroot, in KiB # (dogfood-preflight). FFmpeg needs more than curl: base + target images plus a # full in-tree object tree of ~1945 objects. ~4 GiB is a safe floor. # shellcheck disable=SC2034 MIN_FREE_KIB="4194304" # The build command run INSIDE the target container. Two phases: # # 1. CONFIGURE (NOT wrapped by Bear - FFmpeg's configure is a sh script that # probes the toolchain and writes config.mak; it is not a compile). The # flags pick a LARGE internal codebase with NO external library deps and NO # assembler toolchain so the image stays minimal and reproducible: # --disable-autodetect do not link any auto-detected external library # --disable-doc skip documentation (no texi2html/perl-doc deps) # --disable-programs library TUs only - no ffmpeg/ffprobe/ffplay CLIs # (keeps the count to the codecs/formats themselves) # --disable-asm build pure C - no nasm/yasm, no .S assembly, so # the only toolchain needed is gcc + make + perl # This still compiles ~1945 C translation units across libav*/libsw*. # # 2. BUILD wrapped by Bear -> Bear's capture at /out/compile_commands.json. # # INJECT_CFLAGS is the determinism self-test hook (dogfood-determinism): empty # on every normal run and on determinism run 1, it carries a perturbing flag on # determinism run 2 under --inject-fault. FFmpeg's configure accepts # `--extra-cflags=...`, which appends to the recorded compiler arguments, so the # fault lands in the capture and the two builds diverge - showing the # determinism check FAIL. Empty `--extra-cflags=""` is a harmless no-op on a # normal run. /src is fresh each run (a new container), so configure re-runs and # the flag takes effect. # shellcheck disable=SC2034 TARGET_BUILD_CMD='mkdir -p /out && cd /src && ./configure --disable-autodetect --disable-doc --disable-programs --disable-asm --extra-cflags="${INJECT_CFLAGS:-}" && ${RPROF_PREFIX:-} bear --output /out/compile_commands.json -- make -j"$(nproc)"' �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/targets/kernel/����������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0023025�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/targets/kernel/Containerfile���������������������������������0000664�0000000�0000000�00000005604�15271777203�0025537�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SPDX-License-Identifier: GPL-3.0-or-later # # Linux kernel target image (Stage 4 scale validation). Layers on the # locally-built dogfooding base (which carries Bear-under-test + cdb-compare) # and installs the kernel's own build dependencies plus the pinned, # sha256-verified source at the fixed path /src (dogfood-fixed-paths). The # actual 'make' is wrapped by Bear at 'podman run' time, NOT here, so the # image build stays free of any Bear invocation. # # This target exists ONLY to exercise the target-agnostic Stage 4 checks # (--invariants / --determinism / --replay) at LLVM/kernel scale: a defconfig # x86_64 build is tens of thousands of translation units. It has no committed # golden and no oracle (VALIDATION=none in config.env). ARG BASE_TAG FROM ${BASE_TAG} ARG SRC_DIR=/src # The pinned kernel source. The URL and sha256 are HARDCODED here (not # build-args): the source revision is part of the pinned dogfooding # configuration (dogfood-pinned-target), so it belongs in the committed image # definition, not in a caller-supplied argument. Pinned to 6.12.94 (an LTS # series). A checksum mismatch fails the build (caught by the harness as # INCONCLUSIVE: target infra). ARG KERNEL_URL="https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.12.94.tar.xz" ARG KERNEL_SHA256="e998a232b9418db3301cb58468e291a4f41d6ab8306029b30d991f56251dc8d2" # Kernel build dependencies for `make defconfig && make` on x86_64: # gcc make - the compiler and the build driver # flex bison - generate the device-tree / kconfig parsers # bc - arithmetic in Kbuild scripts (e.g. timeconst) # elfutils-libelf-devel - <libelf.h> for objtool / CONFIG_UNWINDER # openssl-devel - module signing / certs (<openssl/...>) # perl gawk - assorted Kbuild and header-generation scripts # diffutils findutils - cmp/diff and find used throughout Kbuild # xz tar gzip - fetch + unpack the .tar.xz, in-tree gzip use # hostname - kernel records the build host # util-linux - misc coreutils-adjacent tools Kbuild expects # curl - fetch the pinned tarball RUN dnf -y install \ gcc make flex bison bc \ elfutils-libelf-devel openssl-devel \ perl gawk diffutils findutils \ xz tar gzip hostname util-linux curl \ && dnf -y clean all \ && rm -rf /var/cache/dnf # Fetch, verify by sha256, extract to the fixed SRC_DIR. A checksum mismatch # fails the build (caught by the harness as INCONCLUSIVE: target infra). RUN mkdir -p "${SRC_DIR}" \ && curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/linux.tar.xz "${KERNEL_URL}" \ && echo "${KERNEL_SHA256} /tmp/linux.tar.xz" | sha256sum -c - \ && tar -xJf /tmp/linux.tar.xz -C "${SRC_DIR}" --strip-components=1 \ && rm -f /tmp/linux.tar.xz WORKDIR ${SRC_DIR} ����������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/targets/kernel/config.env������������������������������������0000664�0000000�0000000�00000013500�15271777203�0025003�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SPDX-License-Identifier: GPL-3.0-or-later # # Pinned configuration for the Linux kernel dogfooding target (Stage 4 scale # validation). A dogfooding configuration is (base image digest, target source # revision, target build type); all are pinned so the Stage 4 checks stay # stable (dogfood-pinned-target). Sourced by run.sh; values are also consumed # by the target Containerfile via podman build --build-arg. # # This is a LARGE scale-validation target: an x86_64 defconfig build is several # thousand translation units, used to prove the target-agnostic Stage 4 checks # (--invariants / --determinism / --replay) hold at kernel scale. # Validation mode: 'none' - no committed golden and no oracle. The kernel is # not a CMake project (no CMAKE_EXPORT_COMPILE_COMMANDS oracle), and a golden # CDB of tens of thousands of entries is impractical to commit and review. This # target is therefore run ONLY with the Stage 4 checks; run.sh refuses a # no-mode run for a VALIDATION=none target. VALIDATION="none" # Pinned base image: the same digest zlib/curl use (this IS fedora:44, the # devcontainer base). An unpinned base would let a package update change the # compiler under the scale checks. BASE_IMAGE="registry.fedoraproject.org/fedora@sha256:3baf5f0dededfd939eb8f0b271ff8ad17bdb381cdd5768bd7d6f45bba795aa62" # Pinned kernel source. The URL/SHA256 are HARDCODED in the Containerfile (the # source revision is part of the pinned configuration, not a caller arg); this # variable is documentation only. # shellcheck disable=SC2034 KERNEL_VERSION="6.12.94" # Pinned build type. An x86_64 'make defconfig' build: a fixed, architecture- # pinned configuration so the set of compiled translation units is reproducible # across runs (required for --determinism). # shellcheck disable=SC2034 BUILD_TYPE="defconfig-x86_64" # Fixed in-container build path (dogfood-fixed-paths). The kernel builds # IN-TREE (Kbuild emits objects next to sources, or under an O= dir; we use the # in-tree default), so source, build, and objects all share /src. A fixed path # keeps absolute paths in 'directory' reproducible across the two determinism # runs. SRC_DIR="/src" # shellcheck disable=SC2034 BUILD_DIR="/src" # shellcheck disable=SC2034 OBJECTS_DIR="/src" # Object-count instrument for the entry-count invariant (dogfood-invariants). # # A post-build `find /src -name '*.o' | wc -l` is the WRONG instrument for the # kernel: Kbuild aggregates objects into per-directory built-in.a archives and # linked .o files, so the surviving *.o set is a poor proxy for "C sources # actually compiled". A `make -Bn` dry-run is also unreliable here: the kernel's # recursive Kbuild does not enumerate the full compile set in dry-run (it needs # generated prerequisites a dry-run never makes), so it counts ~0. (The harness # degrades gracefully if this instrument yields 0 - the structural invariants # still run, only the entry-count cross-check is skipped.) # # Instead we count Kbuild's OWN per-compile command files, independent of Bear: # for every compiled object `foo.o` Kbuild writes a hidden `.foo.o.cmd` file # recording the exact command. Compile commands carry the ` -c ` flag; the # aggregate/link `.cmd` files (ar/ld) do not, and archives are `.a.cmd` (not # `.o.cmd`). So counting `.*.o.cmd` files whose recorded command contains ` -c ` # yields one count per C compile, directly comparable to Bear's one-entry-per- # compile capture. These files persist after the build (cleanup-independent) and # are produced by Kbuild, not Bear. OBJECT_TOLERANCE_PCT absorbs the residual # slack (host-tool compiles, a handful of generated-source compiles, etc.). # shellcheck disable=SC2034 OBJECT_COUNT_CMD="find /src -name '.*.o.cmd' 2>/dev/null -exec grep -lF -- ' -c ' {} + 2>/dev/null | wc -l" # Tolerance percent for the entry-count band (--expected-objects N --tolerance # OBJECT_TOLERANCE_PCT). The dry-run count and Bear's capture are produced by # two different mechanisms (Kbuild's printed recipe set vs Bear's intercepted # exec set), so they are close but not identical: recipes whose command line is # computed at build time, host-tool compiles, and conditional rules can shift # the two by a few percent. 15% is wide enough to absorb that structural slack # at tens-of-thousands scale while still catching a gross entry drop or a # systematic duplication (which would move the count by far more than 15%). # shellcheck disable=SC2034 OBJECT_TOLERANCE_PCT="15" # Per-target preflight minimum free disk on the podman graphroot, in KiB # (dogfood-preflight). A kernel defconfig build needs several GiB of object # tree on top of the base + target images and the ~1.4 GiB extracted source; # --determinism builds it TWICE (two fresh containers). ~12 GiB is a generous # floor that keeps the run from starting on a near-full disk. MIN_FREE_KIB="12582912" # The build command run INSIDE the target container, wrapped by Bear. # # Phase 1: `make defconfig` (NOT wrapped by Bear - configuration is not a # compile) produces a fixed, architecture-pinned .config for x86_64 so the set # of compiled translation units is reproducible. # # Phase 2: the real `make` build, wrapped by Bear -> Bear's capture at # /out/compile_commands.json. ARCH=x86_64 is pinned for determinism. # # INJECT_CFLAGS is the determinism self-test hook (dogfood-determinism): empty # on every normal run and on determinism run 1, it carries a perturbing flag on # determinism run 2 under --inject-fault. The kernel honors KCFLAGS, which is # appended to the compiler command line for kernel objects, so the flag lands # in the recorded arguments and the two captures diverge, showing the # determinism check FAIL. /src is fresh each run (a new container). # shellcheck disable=SC2034 TARGET_BUILD_CMD='mkdir -p /out && cd /src && make ARCH=x86_64 defconfig && ${RPROF_PREFIX:-} bear --output /out/compile_commands.json -- make ARCH=x86_64 -j"$(nproc)" KCFLAGS="${INJECT_CFLAGS:-}"' ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/targets/nasm/������������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0022503�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/targets/nasm/Containerfile�����������������������������������0000664�0000000�0000000�00000004747�15271777203�0025224�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SPDX-License-Identifier: GPL-3.0-or-later # # x264 target image (Stage 7, assembler dialect). Layers on the locally-built # dogfooding base (which carries Bear-under-test + cdb-compare + clang tooling) # and installs x264's minimal build dependencies - gcc, make, and nasm - plus # the pinned, sha256-verified source at the fixed path /src (dogfood-fixed- # paths). x264 uses a hand-written POSIX-sh `configure` (no autotools, no # CMake); the nasm assembler is the reason this target exists. The actual 'make' # is wrapped by Bear at 'podman run' time, NOT here, so the image build stays # free of any Bear call. ARG BASE_TAG FROM ${BASE_TAG} ARG SRC_DIR=/src # Target build deps: gcc + make build the C translation units and link, nasm # assembles the x86_64 .asm sources (the dialect this target dogfoods); curl # fetches the source; tar + bzip2 unpack the .tar.bz2 snapshot. No pkgconfig or # external libraries: a bare configure builds x264 from just gcc + nasm + make. RUN dnf -y install gcc make nasm curl tar bzip2 \ && dnf -y clean all \ && rm -rf /var/cache/dnf # Pinned x264 source. Like ffmpeg, the URL and sha256 are HARDCODED here (not # build-args): the pin is part of this target's definition and run.sh does not # thread an X264_URL through. This is the head of x264's 'stable' branch at # commit b35605ac (2025-06-08), fetched as a VideoLAN GitLab archive snapshot # and pinned by the sha256 of that archive. A checksum mismatch fails the build # (caught by the harness as INCONCLUSIVE: target infra). ARG X264_URL="https://code.videolan.org/videolan/x264/-/archive/b35605ace3ddf7c1a5d67a2eb553f034aef41d55/x264-b35605ace3ddf7c1a5d67a2eb553f034aef41d55.tar.bz2" ARG X264_SHA256="6eeb82934e69fd51e043bd8c5b0d152839638d1ce7aa4eea65a3fedcf83ff224" # Fetch, verify by sha256, extract to the fixed SRC_DIR. RUN mkdir -p "${SRC_DIR}" \ && curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/x264.tar.bz2 "${X264_URL}" \ && echo "${X264_SHA256} /tmp/x264.tar.bz2" | sha256sum -c - \ && tar -xjf /tmp/x264.tar.bz2 -C "${SRC_DIR}" --strip-components=1 \ && rm -f /tmp/x264.tar.bz2 # Bear config that adds `arguments` to the duplicate match key so x264's 8-bit # and 10-bit double-compiles are BOTH captured (see bear.yaml and config.env). # The build context is this target dir (run.sh builds with -f Containerfile # TARGET_DIR), so bear.yaml is COPYable from here. config.env's TARGET_BUILD_CMD # passes it to bear with --config. COPY bear.yaml /opt/bear-config/duplicates.yaml WORKDIR ${SRC_DIR} �������������������������rizsotto-Bear-0c30b27/tests/dogfooding/targets/nasm/bear.yaml���������������������������������������0000664�0000000�0000000�00000002333�15271777203�0024301�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SPDX-License-Identifier: GPL-3.0-or-later # # Bear configuration for the x264 dogfooding target (Stage 7). x264 compiles # every bit-depth-dependent source TWICE - once for 8-bit and once for 10-bit # high-bit-depth - producing two distinct objects (predict-c-8.o and # predict-c-10.o) from ONE source path (common/x86/predict-c.c) that differ only # in their arguments (-DBIT_DEPTH=8 vs =10, and the -o object). Bear's DEFAULT # duplicate filter keys on (directory, file), so it would collapse each such pair # to a single entry and drop the 10-bit compile - leaving a tool like clangd # blind to the high-bit-depth translation unit. # # Adding `arguments` to the duplicate match key keeps both compiles: they share a # directory and file but differ in arguments, so they are no longer duplicates. # The captured database then holds every real compile (both depths, C and asm), # which is what an x264 developer actually wants AND makes the invariants # entry-count cross-check a true "did Bear capture every compile?" gate against # the on-disk object count. This is the one target that exercises Bear's # duplicate-detection config surface in a real build. schema: "4.2" duplicates: match_on: - directory - file - arguments �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/targets/nasm/config.env��������������������������������������0000664�0000000�0000000�00000013213�15271777203�0024462�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SPDX-License-Identifier: GPL-3.0-or-later # # Pinned configuration for the x264 dogfooding target (Stage 7, assembler # dialect). x264 is a production codebase that MIXES nasm and gcc: ~60 C # translation units alongside ~20 hand-written .asm files assembled by nasm on # x86_64. It validates that Bear now recognizes the nasm assembler dialect and # records a well-formed entry for each .asm compile in a real build - not just # the synthetic event order the integration tests simulate. A dogfooding # configuration is (base image digest, target source revision, target build # type); all are pinned so the checks stay stable (dogfood-pinned-target). # Sourced by run.sh; SRC_DIR is also consumed by the target Containerfile via # podman build --build-arg. # Validation mode for this target: 'none'. x264 has no committed golden (its # nasm/gcc command lines are not worth blessing and re-reviewing) and no CMake # oracle (x264 uses a hand-written sh `configure`, like ffmpeg), so it is run # ONLY with the target-agnostic on-demand checks. Per the Stage 7 plan the # meaningful ones here are --invariants (structural well-formedness of BOTH the # nasm and gcc entries, plus the entry-count cross-check) and --consumer (feeds # the C entries to clang-tidy; it already filters to C-family sources, so the # nasm entries are simply not sampled by it). run.sh dispatches on this selector. # shellcheck disable=SC2034 VALIDATION="none" # Pinned base image: the same digest zlib/curl/ffmpeg use (this IS fedora:44, # the devcontainer base). An unpinned base would let a package update change the # compiler or the nasm version under the fixed checks and produce phantom diffs. # shellcheck disable=SC2034 BASE_IMAGE="registry.fedoraproject.org/fedora@sha256:3baf5f0dededfd939eb8f0b271ff8ad17bdb381cdd5768bd7d6f45bba795aa62" # Fixed in-container build path (dogfood-fixed-paths). x264 builds IN-TREE: # objects land next to their sources under /src and its configure has no # out-of-tree mode, so source, objects, and build dir are all this one fixed # path. A fixed path keeps absolute paths in 'directory' reproducible across # runs (so the determinism check needs no normalization). # shellcheck disable=SC2034 SRC_DIR="/src" # Object-count instrument for the entry-count invariant (dogfood-invariants). # x264's in-tree make leaves EVERY object on disk under /src and compiles each # source (C and .asm) once PER BIT DEPTH - the 8-bit and 10-bit compiles of a # depth-dependent source produce two distinct objects (predict-c-8.o and # predict-c-10.o). Because this target runs Bear with a config that adds # `arguments` to the duplicate match key (see bear.yaml and TARGET_BUILD_CMD), # BOTH compiles are captured as distinct entries, so the DEFAULT instrument, a # post-build `find $OBJECTS_DIR -name '*.o' | wc -l`, exactly equals Bear's entry # count (verified on a real build: 114 objects == 114 entries). x264 therefore # does NOT override OBJECT_COUNT_CMD - the config file, not a bespoke counting # hack, is what makes the entry-count cross-check a true "did Bear capture every # compile?" gate here. # shellcheck disable=SC2034 OBJECTS_DIR="/src" # Build directory passed to the replay/consumer sampler (dogfood-replay, # dogfood-clang-consumer) as `cdb-compare sample --build-dir`. x264 builds # in-tree, so its build dir IS /src. # shellcheck disable=SC2034 BUILD_DIR="/src" # Tolerance percent for the entry-count band # (--expected-objects N --tolerance OBJECT_TOLERANCE_PCT). x264 compiles each C # and each .asm source exactly once and leaves the object on disk, so `find` # tracks Bear's entry count tightly; 10% absorbs an incidental generated object # (e.g. config-probe scratch) across x264 revisions without a false fail. # shellcheck disable=SC2034 OBJECT_TOLERANCE_PCT="10" # Per-target preflight minimum free disk on the podman graphroot, in KiB # (dogfood-preflight). x264 is small (~114 objects), but the base + target # images still dominate; ~4 GiB is a safe floor consistent with the other # targets. # shellcheck disable=SC2034 MIN_FREE_KIB="4194304" # The build command run INSIDE the target container. Two phases: # # 1. CONFIGURE (NOT wrapped by Bear - x264's configure is a hand-written sh # script that probes the toolchain, detects nasm for the x86_64 assembly, # and writes config.mak; it is not a compile). No flags are needed for a # minimal build: configure links the cli and the library from just gcc + # nasm + make. --extra-cflags carries the determinism self-test hook. # # 2. BUILD wrapped by Bear -> Bear's capture at /out/compile_commands.json. The # build compiles the C TUs with gcc and the .asm TUs with nasm; Bear must # record an entry for each. --config points Bear at the target's bear.yaml # (baked into the image at /opt/bear-config/duplicates.yaml), which adds # `arguments` to the duplicate match key so x264's 8-bit and 10-bit # double-compiles are both kept (see bear.yaml and the OBJECTS_DIR note). # # INJECT_CFLAGS is the determinism self-test hook (dogfood-determinism): empty # on every normal run and on determinism run 1, it carries a perturbing flag on # determinism run 2 under --inject-fault. x264's configure accepts # `--extra-cflags=...`, which appends to the recorded gcc arguments, so the # fault lands in the capture and the two builds diverge. Empty # `--extra-cflags=""` is a harmless no-op on a normal run. /src is fresh each run # (a new container), so configure re-runs and the flag takes effect. # shellcheck disable=SC2034 TARGET_BUILD_CMD='mkdir -p /out && cd /src && ./configure --extra-cflags="${INJECT_CFLAGS:-}" && ${RPROF_PREFIX:-} bear --config /opt/bear-config/duplicates.yaml --output /out/compile_commands.json -- make -j"$(nproc)"' �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/targets/openmpi/���������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0023214�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/targets/openmpi/Containerfile��������������������������������0000664�0000000�0000000�00000003760�15271777203�0025727�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SPDX-License-Identifier: GPL-3.0-or-later # # OSU Micro-Benchmarks target image (Stage 7, MPI compiler wrapper). Layers on # the locally-built dogfooding base (which carries Bear-under-test + cdb-compare # + clang tooling) and installs the Open MPI wrappers plus gcc/make, then the # pinned, sha256-verified source at the fixed path /src (dogfood-fixed-paths). # The OSU benchmarks build with autotools (mpicc/mpicxx). The actual 'make' is # wrapped by Bear at 'podman run' time, NOT here, so the image build stays free # of any Bear call. ARG BASE_TAG FROM ${BASE_TAG} ARG SRC_DIR=/src # Target build deps: openmpi-devel provides the mpicc/mpicxx wrappers (under # /usr/lib64/openmpi/bin) and the MPI headers/libs they splice in; gcc is the # compiler the wrappers exec; make drives the autotools build. curl fetches the # source; tar + gzip unpack the .tar.gz. The wrappers are the reason this target # exists (the driver+child interception path). RUN dnf -y install openmpi-devel gcc gcc-c++ make curl tar gzip \ && dnf -y clean all \ && rm -rf /var/cache/dnf # Pinned OSU Micro-Benchmarks source. Like ffmpeg, the URL and sha256 are # HARDCODED here (not build-args): the pin is part of this target's definition # and run.sh does not thread an OSU_URL through. 7.5 is the current release from # the upstream MVAPICH download archive; the sha256 was verified against the # downloaded tarball. A checksum mismatch fails the build (caught by the harness # as INCONCLUSIVE: target infra). ARG OSU_URL="https://mvapich.cse.ohio-state.edu/download/mvapich/osu-micro-benchmarks-7.5.tar.gz" ARG OSU_SHA256="1cf84ac5419456202757a757c5f9a4f5c6ecd05c65783c7976421cfd6020b3b3" # Fetch, verify by sha256, extract to the fixed SRC_DIR. RUN mkdir -p "${SRC_DIR}" \ && curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/osu.tar.gz "${OSU_URL}" \ && echo "${OSU_SHA256} /tmp/osu.tar.gz" | sha256sum -c - \ && tar -xzf /tmp/osu.tar.gz -C "${SRC_DIR}" --strip-components=1 \ && rm -f /tmp/osu.tar.gz WORKDIR ${SRC_DIR} ����������������rizsotto-Bear-0c30b27/tests/dogfooding/targets/openmpi/config.env�����������������������������������0000664�0000000�0000000�00000012272�15271777203�0025177�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SPDX-License-Identifier: GPL-3.0-or-later # # Pinned configuration for the OSU Micro-Benchmarks dogfooding target (Stage 7, # MPI compiler wrapper). The OSU Micro-Benchmarks build with mpicc/mpicxx, the # Open MPI compiler wrappers. mpicc is a REAL wrapper binary (opal_wrapper) that # execs gcc with the MPI include/library flags spliced in, so Bear intercepts # BOTH the mpicc invocation and the gcc child it spawns. This target validates, # in a real LD_PRELOAD build, two decisions the integration tests can only # simulate: that Bear records the mpicc command verbatim (wrapper-verbatim), and # that the driver+child pair collapses to a SINGLE entry per translation unit # (no true duplicate). A dogfooding configuration is (base image digest, target # source revision, target build type); all are pinned (dogfood-pinned-target). # Sourced by run.sh; SRC_DIR is also consumed by the target Containerfile via # podman build --build-arg. # Validation mode for this target: 'none'. OSU has no committed golden and no # CMake oracle (it uses autotools), so it is run ONLY with the target-agnostic # on-demand checks. Per the Stage 7 plan the meaningful ones here are # --invariants (its no-true-duplicates clause is exactly the driver+child # collapse assertion), --determinism (two builds equivalent), and --replay (the # recorded mpicc command re-accepts -fsyntax-only, since mpicc forwards it to # gcc). run.sh dispatches on this selector. # shellcheck disable=SC2034 VALIDATION="none" # Pinned base image: the same digest zlib/curl/ffmpeg use (this IS fedora:44, # the devcontainer base). An unpinned base would let a package update change the # compiler or the Open MPI version under the fixed checks and produce phantom # diffs. # shellcheck disable=SC2034 BASE_IMAGE="registry.fedoraproject.org/fedora@sha256:3baf5f0dededfd939eb8f0b271ff8ad17bdb381cdd5768bd7d6f45bba795aa62" # Fixed in-container build path (dogfood-fixed-paths). OSU builds IN-TREE: # objects land next to their sources under /src (autotools supports VPATH, but # an in-tree build keeps this target as simple as ffmpeg's), so source, objects, # and build dir are all this one fixed path. A fixed path keeps absolute paths # in 'directory' reproducible across runs (so determinism needs no # normalization). # shellcheck disable=SC2034 SRC_DIR="/src" # Object-count instrument for the entry-count invariant (dogfood-invariants). # OSU's automake build compiles each benchmark source exactly once and leaves # the object on disk under /src, so the DEFAULT instrument, a post-build # `find $OBJECTS_DIR -name '*.o' | wc -l`, is the right cross-check: it must # equal Bear's entry count, which it does only if the driver+child collapse left # ONE entry per object (a failed collapse would roughly double Bear's count # against a fixed object count - the check the no-true-duplicates invariant and # this band both guard). OSU therefore does NOT override OBJECT_COUNT_CMD. # shellcheck disable=SC2034 OBJECTS_DIR="/src" # Build directory passed to the replay sampler (dogfood-replay) as # `cdb-compare sample --build-dir`. OSU builds in-tree, so its build dir IS # /src. # shellcheck disable=SC2034 BUILD_DIR="/src" # Tolerance percent for the entry-count band # (--expected-objects N --tolerance OBJECT_TOLERANCE_PCT). OSU compiles each # source once and leaves the object on disk, so `find` tracks Bear's entry count # tightly; 10% absorbs an incidental configure-probe object across OSU releases # without a false fail. # shellcheck disable=SC2034 OBJECT_TOLERANCE_PCT="10" # Per-target preflight minimum free disk on the podman graphroot, in KiB # (dogfood-preflight). OSU is small (a few hundred objects), but the base + # target images still dominate; ~4 GiB is a safe floor consistent with the # other targets. # shellcheck disable=SC2034 MIN_FREE_KIB="4194304" # The build command run INSIDE the target container. Two phases: # # 1. CONFIGURE (NOT wrapped by Bear - autotools configure probes the toolchain # and writes Makefiles; it is not a compile). Fedora installs the Open MPI # wrappers under /usr/lib64/openmpi/bin, NOT on the default PATH, so that # directory is prepended for the whole command and CC/CXX are set to the # bare wrapper names; make then finds them at build time too. CFLAGS carries # the determinism self-test hook. # # 2. BUILD wrapped by Bear -> Bear's capture at /out/compile_commands.json. # Each translation unit is compiled by mpicc (or mpicxx), which execs gcc; # Bear must record ONE entry per unit (the wrapper), collapsing the gcc # child. # # INJECT_CFLAGS is the determinism self-test hook (dogfood-determinism): empty # on every normal run and on determinism run 1, it carries a perturbing flag on # determinism run 2 under --inject-fault. It is appended to CFLAGS so it lands # in the recorded mpicc arguments and the two captures diverge. Empty CFLAGS is # a harmless no-op on a normal run. /src is fresh each run (a new container), so # configure re-runs and the flag takes effect. # shellcheck disable=SC2034 TARGET_BUILD_CMD='mkdir -p /out && export PATH=/usr/lib64/openmpi/bin:$PATH && cd /src && ./configure CC=mpicc CXX=mpicxx CFLAGS="${INJECT_CFLAGS:-}" && ${RPROF_PREFIX:-} bear --output /out/compile_commands.json -- make -j"$(nproc)"' ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/targets/zlib/������������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0022505�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/targets/zlib/Containerfile�����������������������������������0000664�0000000�0000000�00000002154�15271777203�0025214�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SPDX-License-Identifier: GPL-3.0-or-later # # zlib target image (Stage 2). Layers on the locally-built dogfooding base # (which carries Bear-under-test + cdb-compare) and installs zlib's own build # dependencies plus the pinned, sha256-verified source at the fixed path /src # (dogfood-fixed-paths). The actual 'make' is wrapped by Bear at 'podman run' # time, NOT here, so the image build stays free of any Bear invocation. ARG BASE_TAG FROM ${BASE_TAG} ARG ZLIB_URL ARG ZLIB_SHA256 ARG SRC_DIR=/src # Target build deps: gcc + make to build zlib, curl + tar to fetch and unpack. RUN dnf -y install gcc make curl tar gzip \ && dnf -y clean all \ && rm -rf /var/cache/dnf # Fetch, verify by sha256, extract to the fixed SRC_DIR. A checksum mismatch # fails the build (caught by the harness as INCONCLUSIVE: target infra). RUN mkdir -p "${SRC_DIR}" \ && curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/zlib.tar.gz "${ZLIB_URL}" \ && echo "${ZLIB_SHA256} /tmp/zlib.tar.gz" | sha256sum -c - \ && tar -xzf /tmp/zlib.tar.gz -C "${SRC_DIR}" --strip-components=1 \ && rm -f /tmp/zlib.tar.gz WORKDIR ${SRC_DIR} ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/dogfooding/targets/zlib/config.env��������������������������������������0000664�0000000�0000000�00000007610�15271777203�0024470�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SPDX-License-Identifier: GPL-3.0-or-later # # Pinned configuration for the zlib dogfooding target (Stage 2). # A dogfooding configuration is (base image digest, target source revision, # target build type); all are pinned so the golden stays stable # (dogfood-pinned-target). Sourced by run.sh; values are also consumed by # the target Containerfile via podman build --build-arg. # Pinned base image: this digest IS fedora:44 (the devcontainer base). # An unpinned base would let a package update change the compiler under a # fixed golden and produce phantom diffs. BASE_IMAGE="registry.fedoraproject.org/fedora@sha256:3baf5f0dededfd939eb8f0b271ff8ad17bdb381cdd5768bd7d6f45bba795aa62" # Pinned zlib source. ZLIB_VERSION="1.3.1" ZLIB_URL="https://github.com/madler/zlib/releases/download/v1.3.1/zlib-1.3.1.tar.gz" ZLIB_SHA256="9a93b2b7dfdac77ceba5a558a580e74667dd6fede4585b91eefb60f03b72df23" # Pinned build type. Release is representative of shipped use. BUILD_TYPE="Release" # Fixed in-container build path (dogfood-fixed-paths). zlib builds in-tree; # its ./configure has no usable out-of-tree mode, so source and build share # this path. A fixed path keeps absolute paths in 'directory' reproducible. SRC_DIR="/src" # Build directory passed to the replay sampler (dogfood-replay) as # `cdb-compare sample --build-dir`, so it prefers TUs whose flags do not # reference build-dir includes. zlib builds in-tree, so its build dir IS /src. # shellcheck disable=SC2034 BUILD_DIR="/src" # Object-count instrument for the entry-count invariant (dogfood-invariants). # OBJECTS_DIR is where zlib's objects live, but a post-build `find` is the WRONG # instrument for zlib: its in-tree make compiles each library source twice (a # static *.o and a PIC objs/*.o) but DELETES the whole objs/ directory at link # time, so only the 19 static objects survive while 34 are produced - a ~1.8x # undercount no sane tolerance should absorb. So zlib overrides OBJECT_COUNT_CMD # to count make's OWN dependency graph (`make -Bn` lists every compile rule), # which is the build-system-native, cleanup-independent, Bear-independent count # of objects produced (34). It is run after the real build (the configure cache # is already in place). OBJECTS_DIR is retained for documentation / the default. # shellcheck disable=SC2034 OBJECTS_DIR="/src" # shellcheck disable=SC2034 # `grep -oE -e PATTERN`: -e marks the pattern so grep does not mistake the # leading "-o " for its own -o option. OBJECT_COUNT_CMD="cd /src && make -Bn 2>/dev/null | grep -oE -e '-o [^ ]+\\.o' | sort -u | wc -l" # Tolerance percent for the entry-count band # (--expected-objects N --tolerance OBJECT_TOLERANCE_PCT). With OBJECT_COUNT_CMD # counting produced objects (34) and Bear recording 34 entries, the two match # exactly; 10% is headroom for an incidental build-system object and keeps the # check sensitive to a real entry drop or duplication. Deliberately small: # over-wide tolerance would hide a real drop. # shellcheck disable=SC2034 OBJECT_TOLERANCE_PCT="10" # Per-target preflight minimum free disk on the podman graphroot, in KiB # (dogfood-preflight). zlib needs roughly 2 GiB for the base + target images. MIN_FREE_KIB="2097152" # The build command run INSIDE the target container, wrapped by Bear. zlib's # configure passes CFLAGS through; Release maps to -O3 -DNDEBUG. The output # lands at /out/compile_commands.json, copied out with 'podman cp'. # # INJECT_CFLAGS is the determinism self-test hook (dogfood-determinism): it is # empty on every normal run and on determinism run 1, and carries a perturbing # flag on determinism run 2 under --inject-fault, so the two captures diverge # and the check is shown to FAIL. It is appended to the configure-time CFLAGS so # it lands in the recorded compiler arguments. # shellcheck disable=SC2034 TARGET_BUILD_CMD='cd /src && CFLAGS="-O3 -DNDEBUG ${INJECT_CFLAGS:-}" ./configure && ${RPROF_PREFIX:-} bear --output /out/compile_commands.json -- make' ������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/������������������������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0020300�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/CLAUDE.md���������������������������������������������������0000664�0000000�0000000�00000006571�15271777203�0021570�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������## Integration test rules The test infrastructure (TestEnvironment, BearOutput, CompilationDatabase, the `bear_test!` macro) lives in `tests/fixtures/infrastructure/`; read those sources for the full API and platform-specific patterns. ## Before writing tests - A debug build must exist: run `cargo build` first. The tests run the driver, wrapper, and preload library that `build.rs` finds in the profile directory. - Gate platform-specific tests on the `cfg` keys that `build.rs` emits, for example `#[cfg(has_executable_make)]`. - `build.rs` probes a fixed list of host tools. To gate a test on a tool that is not in the list, add the probe there first. ## Test pattern ```rust // Requirements: output-json-compilation-database #[test] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn single_file_compilation() -> Result<()> { let env = TestEnvironment::new("single_file_compilation")?; env.create_source_files(&[("test.c", "int main() { return 0; }")])?; let build = format!("{} -c test.c", filename_of(COMPILER_C_PATH)); let script = env.create_shell_script("build.sh", &build)?; let output = env.run_bear(&["--output", "db.json", "--", SHELL_PATH, script.to_str().unwrap()])?; output.assert_success()?; let db = env.load_compilation_database("db.json")?; db.assert_count(1)?; Ok(()) } ``` ## Linking tests to requirements Tests that protect a requirement cite its ID with a `Requirements:` tag. This is the sole source of truth for the test-to-requirement link; requirement files do not list their tests. Format: ```rust // Requirements: output-json-compilation-database, output-append #[test] fn append_works_as_expected() -> Result<()> { ... } ``` Rules: - Value is a comma-separated list of requirement IDs (see `docs/requirements/CLAUDE.md`). - Place the tag on the line(s) directly above `#[test]` (or the test macro). - If every test in a file covers the same requirement, a file-level `//! Requirements: <id>` near the top is sufficient. Test-level tags override file-level tags. - A test may cite multiple requirements when it legitimately exercises more than one. To find tests for a requirement: ```sh grep -rn "Requirements:.*<requirement-id>" crates/ tests/ ``` To verify every `implemented` requirement has at least one tagged test (exits non-zero if any lacks coverage): ```sh ./scripts/check-requirements-coverage.sh ``` ## Debugging When a test panics, the fixture automatically dumps the last captured bear stdout/stderr to the test binary's stderr. `run_bear` inherits `RUST_LOG` when set; if unset it defaults to `info` (so warn/info/error log lines reach the panic dump without the per-event debug spam from the preload library). ```sh cargo test # info-level dump on failure RUST_LOG=debug cargo test # full per-event trace (recommended for local triage) BEAR_TEST_PRESERVE_FAILURES=1 cargo test # also keep temp dirs at /tmp/bear-test-<name>-<pid> RUST_LOG=debug BEAR_TEST_PRESERVE_FAILURES=1 cargo test # both ``` CI sets `RUST_LOG=debug` so failures on platforms that can't be reproduced locally carry full diagnostic context without a re-run. ## Organization - `tests/cases/` - test implementations grouped by feature area - `tests/fixtures/` - test infrastructure (TestEnvironment, assertions, constants) - `tests/integration.rs` - test entry point ���������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/Cargo.toml��������������������������������������������������0000664�0000000�0000000�00000001333�15271777203�0022230�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������[package] name = "integration-tests" version.workspace = true authors.workspace = true repository.workspace = true homepage.workspace = true license.workspace = true edition.workspace = true rust-version.workspace = true publish = false [dev-dependencies] bear-test-tools = { path = "../tools" } tempfile.workspace = true anyhow.workspace = true assert_cmd.workspace = true assert_fs.workspace = true predicates.workspace = true serde_json.workspace = true serde-saphyr.workspace = true encoding_rs.workspace = true [build-dependencies] platform-checks = { path = "../../build-support/platform-checks" } which.workspace = true [[test]] name = "integration" path = "tests/integration.rs" harness = true [lints] workspace = true �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/build.rs����������������������������������������������������0000664�0000000�0000000�00000025131�15271777203�0021747�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * SPDX-License-Identifier: GPL-3.0-or-later * * This build script is responsible for setting up environment variables and * cfg flags required by the integration tests, mirroring the logic from the * original bear/build.rs. */ /// Driver executable name (platform-dependent) #[cfg(windows)] const DRIVER_NAME: &str = "bear-driver.exe"; #[cfg(not(windows))] const DRIVER_NAME: &str = "bear-driver"; /// Wrapper executable name (platform-dependent) #[cfg(windows)] const WRAPPER_NAME: &str = "bear-wrapper.exe"; #[cfg(not(windows))] const WRAPPER_NAME: &str = "bear-wrapper"; /// Preload library name (platform-dependent) #[cfg(target_os = "macos")] const PRELOAD_NAME: &str = "libexec.dylib"; #[cfg(not(target_os = "macos"))] const PRELOAD_NAME: &str = "libexec.so"; fn main() { // Re-run build script if env changes println!("cargo:rerun-if-env-changed=PATH"); println!("cargo:rerun-if-env-changed=CARGO_TARGET_DIR"); println!("cargo:rerun-if-env-changed=PROFILE"); println!("cargo:rerun-if-env-changed=INTERCEPT_LIBDIR"); // Forward INTERCEPT_LIBDIR so integration tests use the same value as bear-driver let intercept_libdir = std::env::var("INTERCEPT_LIBDIR").unwrap_or_else(|_| "lib".to_string()); validate_intercept_libdir(&intercept_libdir); println!("cargo:rustc-env=INTERCEPT_LIBDIR={}", intercept_libdir); // Re-run if bear or intercept-preload artifacts change println!("cargo:rerun-if-changed=../../crates/bear/src"); println!("cargo:rerun-if-changed=../../crates/semantic/src"); println!("cargo:rerun-if-changed=../../crates/intercept-preload/src"); // Locate install script and repo root for integration tests. // CARGO_MANIFEST_DIR is <root>/tests/integration, two levels below root. let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("cargo sets CARGO_MANIFEST_DIR for build scripts"); let repo_root = std::path::Path::new(&manifest_dir) .ancestors() .nth(2) .expect("tests/integration sits two levels below the repo root"); let install_script = repo_root.join("scripts").join("install.sh"); println!("cargo:rerun-if-changed={}", install_script.display()); println!("cargo:rustc-env=REPO_ROOT={}", repo_root.display()); println!("cargo:rustc-env=INSTALL_SCRIPT_PATH={}", install_script.display()); // Set up paths for driver, wrapper and preload artifacts let (driver_path, wrapper_path, preload_path) = find_intercept_artifacts(); println!("cargo:rustc-env=DRIVER_EXECUTABLE={}", DRIVER_NAME); println!("cargo:rustc-env=DRIVER_EXECUTABLE_PATH={}", driver_path); println!("cargo:rustc-env=WRAPPER_EXECUTABLE={}", WRAPPER_NAME); println!("cargo:rustc-env=WRAPPER_EXECUTABLE_PATH={}", wrapper_path); println!("cargo:rustc-env=PRELOAD_LIBRARY={}", PRELOAD_NAME); println!("cargo:rustc-env=PRELOAD_LIBRARY_PATH={}", preload_path); // Replay platform-checks results as cfg directives for this crate platform_checks::emit_cfg(&platform_checks::detect()); platform_checks::emit_check_cfg(); // Perform additional checks for executables check_executable_exists("true"); check_executable_exists("false"); check_executable_exists("echo"); check_executable_exists("sleep"); check_executable_exists("setsid"); check_executable_exists("cat"); check_executable_exists("ls"); check_executable_exists("mkdir"); check_executable_exists("rm"); check_executable_exists("nm"); check_one_executable_exists("shell", &["sh", "zsh", "bash"]); check_one_executable_exists("make", &["make", "gmake", "mingw32-make"]); check_one_executable_exists("compiler_c", &["gcc", "clang", "cc"]); check_one_executable_exists("compiler_cxx", &["g++", "clang++", "c++"]); check_ccache_masquerade_dir(); check_one_executable_exists("compiler_fortran", &["gfortran", "flang"]); check_one_executable_exists("compiler_cuda", &["nvcc"]); check_executable_exists("libtool"); check_executable_exists("fakeroot"); check_executable_exists("valgrind"); check_executable_exists("ar"); check_executable_exists("env"); // Check for preload library availability check_preload_library_availability(&preload_path); } fn find_intercept_artifacts() -> (String, String, String) { let out_dir = std::env::var("OUT_DIR").expect("cargo sets OUT_DIR for build scripts"); let target_dir = profile_dir_of(std::path::Path::new(&out_dir)); let driver_path = target_dir.join(DRIVER_NAME); let wrapper_path = target_dir.join(WRAPPER_NAME); let preload_path = target_dir.join(PRELOAD_NAME); ( format!("{}", driver_path.display()), format!("{}", wrapper_path.display()), format!("{}", preload_path.display()), ) } /// Climb from a build script's `OUT_DIR` to the profile directory that holds /// the linked binaries (`target/debug` or `target/release`). /// /// Counting components is not safe here: cargo has shipped two `OUT_DIR` /// layouts, `<profile>/build/<pkg>-<hash>/out` and, on newer toolchains, /// `<profile>/build/<pkg>/<hash>/out`. Both put exactly one `build` component /// under the profile directory, so anchor on that instead of on a depth. fn profile_dir_of(out_dir: &std::path::Path) -> &std::path::Path { out_dir .ancestors() .find(|a| a.file_name().and_then(|n| n.to_str()) == Some("build")) .and_then(|build| build.parent()) .unwrap_or_else(|| panic!("no 'build' component in OUT_DIR: {}", out_dir.display())) } fn check_executable_exists(executable: &str) { println!("cargo:rustc-check-cfg=cfg(has_executable_{})", executable); match which::which(executable) { Ok(path) => { println!("cargo:rustc-cfg=has_executable_{}", executable); println!("cargo:rustc-env={}_PATH={}", executable.to_uppercase(), path.display()); println!("cargo:warning=Checking for executable: {} ... {}", executable, path.display()); } Err(_) => { println!("cargo:warning=Checking for executable: {} ... missing", executable); } } } fn check_one_executable_exists(define: &str, executables: &[&str]) { println!("cargo:rustc-check-cfg=cfg(has_executable_{})", define); for executable in executables { if let Ok(path) = which::which(executable) { println!("cargo:rustc-cfg=has_executable_{}", define); println!("cargo:rustc-env={}_PATH={}", define.to_uppercase(), path.display()); println!("cargo:warning=Checking for executable: {} ... {}", define, path.display()); return; } } println!("cargo:warning=Checking for executable: {} ... missing", define); } /// Locate a ccache masquerade directory on this host, independent of PATH. /// When one is found, expose it via the `CCACHE_MASQUERADE_DIR` env var and /// set `cfg(host_has_ccache_masquerade)`. The dedicated recursion test (see /// `interception-wrapper-recursion`) prepends that dir to its own PATH at /// runtime so the masquerade setup is exercised regardless of whether the /// host's default PATH already includes it. CI installs ccache so the dir /// exists on the Ubuntu matrix entry. fn check_ccache_masquerade_dir() { println!("cargo:rustc-check-cfg=cfg(host_has_ccache_masquerade)"); let candidates = [ // Debian/Ubuntu "/usr/lib/ccache", // Fedora/Arch/Gentoo (lib64 multilib) "/usr/lib64/ccache", // Some BSDs / older distros "/usr/libexec/ccache", // Homebrew on Apple Silicon "/opt/homebrew/opt/ccache/libexec", // Homebrew on Intel macOS / Linuxbrew "/usr/local/opt/ccache/libexec", ]; for dir in candidates { if let Some(path) = detect_ccache_masquerade_dir(dir) { println!("cargo:rustc-cfg=host_has_ccache_masquerade"); println!("cargo:rustc-env=CCACHE_MASQUERADE_DIR={}", path); println!("cargo:warning=ccache masquerade directory found at {}", path); return; } } println!("cargo:warning=no ccache masquerade directory detected"); } /// A directory qualifies as a ccache masquerade dir if it contains a `gcc`, /// `cc`, `g++`, `c++`, `clang`, or `clang++` entry whose ultimate target's /// file name is `ccache`. fn detect_ccache_masquerade_dir(dir: &str) -> Option<String> { let path = std::path::Path::new(dir); if !path.is_dir() { return None; } for name in ["gcc", "cc", "g++", "c++", "clang", "clang++"] { let candidate = path.join(name); if !candidate.exists() { continue; } if let Ok(target) = std::fs::canonicalize(&candidate) && target.file_name().and_then(|n| n.to_str()) == Some("ccache") { return Some(dir.to_string()); } } None } fn check_preload_library_availability(preload_path: &str) { println!("cargo:rustc-check-cfg=cfg(has_preload_library)"); // Check if we're on a platform that supports LD_PRELOAD (Unix-like systems) let platform_supports_preload = !cfg!(windows); // Check if the preload library file exists let preload_file_exists = std::path::Path::new(preload_path).exists(); // Check for platform-specific restrictions let runtime_supports_preload = is_preload_supported_at_runtime(); if platform_supports_preload && preload_file_exists && runtime_supports_preload { println!("cargo:rustc-cfg=has_preload_library"); println!("cargo:warning=Preload library available at: {}", preload_path); } else { println!( "cargo:warning=Preload library not available (platform_supports: {}, file_exists: {}, runtime_supports: {})", platform_supports_preload, preload_file_exists, runtime_supports_preload ); } } /// Check if preload is supported at runtime, considering platform-specific restrictions. fn is_preload_supported_at_runtime() -> bool { #[cfg(windows)] { // Windows doesn't support LD_PRELOAD false } #[cfg(any(target_os = "macos", target_os = "ios"))] { // Disable integration test on macOS, because could not find out how to compile // the libexec.dylib that works on arm64e, and that makes the CI build broken. false } #[cfg(not(any(target_os = "macos", target_os = "ios", windows)))] { // Other Unix-like systems should support preload true } } fn validate_intercept_libdir(value: &str) { if value.trim().is_empty() { panic!("INTERCEPT_LIBDIR must not be empty or whitespace-only"); } let path = std::path::Path::new(value); if path.is_absolute() { panic!("INTERCEPT_LIBDIR must be a relative path, got: {}", value); } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/tests/������������������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0021442�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/tests/cases/������������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0022540�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/tests/cases/compilation_output.rs���������������������������0000664�0000000�0000000�00000141176�15271777203�0027056�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: GPL-3.0-or-later //! Compilation database output tests for Bear integration //! //! These tests verify that Bear generates correct compilation databases //! for various build scenarios, ported from the Python/lit test suite. use crate::fixtures::constants::*; use crate::fixtures::infrastructure::{ CompilationEntryMatcher, TestEnvironment, compilation_entry, filename_of, }; use anyhow::{Context, Result}; #[cfg(target_family = "unix")] use serde_json::Value; /// Test compilation with build script that calls compiler /// This generates events that the semantic analyzer can process // Requirements: output-json-compilation-database, output-compilation-entries, output-atomic-write, output-path-format #[test] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn simple_single_file_compilation() -> Result<()> { let env = TestEnvironment::new("simple_single_file_compilation")?; // Create a simple source file env.create_source_files(&[("simple.c", "int main() { return 0; }")])?; // Create a shell script that calls the compiler let build_commands = format!("{} -c simple.c -o simple.o", filename_of(COMPILER_C_PATH)); let build_script_path = env.create_shell_script("build.sh", &build_commands)?; // Step 1: Run intercept command to capture events from the build script let result = env.run_bear(&[ "intercept", "-o", "events.json", "--", SHELL_PATH, build_script_path.to_str().unwrap(), ])?; result.assert_success()?; // Check if events file was created assert!(env.file_exists("events.json")); // Step 2: Run semantic command to convert events to compilation database let result = env.run_bear(&["semantic", "-i", "events.json", "-o", "compile_commands.json"])?; result.assert_success()?; // Verify compilation database was created assert!(env.file_exists("compile_commands.json")); // Load and verify the compilation database let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; // Verify the entry contains expected information db.assert_contains(&compilation_entry!( file: "simple.c".to_string(), directory: env.test_dir().to_str().unwrap().to_string(), arguments: vec![ COMPILER_C_PATH.to_string(), "-c".to_string(), "simple.c".to_string(), "-o".to_string(), "simple.o".to_string(), ] ))?; Ok(()) } /// Test successful build with multiple sources (C and C++) /// Verifies Bear handles mixed compilation units // Requirements: output-json-compilation-database, output-compilation-entries #[test] #[cfg(all(has_executable_compiler_c, has_executable_compiler_cxx, has_executable_shell))] fn successful_build_multiple_sources() -> Result<()> { let env = TestEnvironment::new("successful_build_multiple_sources")?; // Create multiple source files env.create_source_files(&[ ("test1.c", "int func1() { return 1; }"), ("test2.c", "int func2() { return 2; }"), ("test3.cpp", "extern \"C\" int func3() { return 3; }"), ("test4.cpp", "extern \"C\" int func4() { return 4; }"), ])?; // Create build script that compiles all files let build_commands = [ format!("{} -c -o test1.o test1.c", filename_of(COMPILER_C_PATH)), format!("{} -c -o test2.o test2.c", filename_of(COMPILER_C_PATH)), format!("{} -c -o test3.o test3.cpp", filename_of(COMPILER_CXX_PATH)), format!("{} -c -o test4.o test4.cpp", filename_of(COMPILER_CXX_PATH)), ] .join("\n"); let build_script_path = env.create_shell_script("build.sh", &build_commands)?; // Run bear env.run_bear_success(&[ "--output", "compile_commands.json", "--", SHELL_PATH, build_script_path.to_str().unwrap(), ])?; // Verify compilation database assert!(env.file_exists("compile_commands.json")); let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(4)?; // Verify entries for each source file let temp_dir = env.test_dir().to_str().unwrap(); db.assert_contains(&compilation_entry!( file: "test1.c".to_string(), directory: temp_dir.to_string(), arguments: vec![ COMPILER_C_PATH.to_string(), "-c".to_string(), "-o".to_string(), "test1.o".to_string(), "test1.c".to_string(), ] ))?; db.assert_contains(&compilation_entry!( file: "test2.c".to_string(), directory: temp_dir.to_string(), arguments: vec![ COMPILER_C_PATH.to_string(), "-c".to_string(), "-o".to_string(), "test2.o".to_string(), "test2.c".to_string(), ] ))?; db.assert_contains(&compilation_entry!( file: "test3.cpp".to_string(), directory: temp_dir.to_string(), arguments: vec![ COMPILER_CXX_PATH.to_string(), "-c".to_string(), "-o".to_string(), "test3.o".to_string(), "test3.cpp".to_string(), ] ))?; db.assert_contains(&compilation_entry!( file: "test4.cpp".to_string(), directory: temp_dir.to_string(), arguments: vec![ COMPILER_CXX_PATH.to_string(), "-c".to_string(), "-o".to_string(), "test4.o".to_string(), "test4.cpp".to_string(), ] ))?; Ok(()) } /// Regression guard for #601: pre-4.x versions of Bear dropped /// `-xc++-header` precompiled-header invocations from the database /// because the source-file heuristic did not recognize `.hpp` as a /// source. 4.x added `.hpp`/`.h`/`.hxx`/`.tcc` to SOURCE_EXTENSIONS /// (semantic/src/interpreters/matchers/source.rs); no test /// asserts this for the -xc++-header flow. Lock it in so a future /// reshuffle of the extension table cannot silently drop PCH builds. // Requirements: output-json-compilation-database, output-compilation-entries #[test] #[cfg(all(has_executable_compiler_cxx, has_executable_shell))] fn precompiled_header_invocation_is_captured() -> Result<()> { let env = TestEnvironment::new("precompiled_header")?; env.create_source_files(&[("header.hpp", "#pragma once\nint pch_value();\n")])?; let cmd = format!("{} -xc++-header -c header.hpp -o header.hpp.gch", filename_of(COMPILER_CXX_PATH),); let script_path = env.create_shell_script("build.sh", &cmd)?; env.run_bear_success(&[ "--output", "compile_commands.json", "--", SHELL_PATH, script_path.to_str().unwrap(), ])?; // The original bug was an empty database. Assert "at least one entry" // plus "an entry for header.hpp", which both pins the regression // (empty database) and tolerates host-dependent multi-event captures // (e.g., ccache masquerade). let db = env.load_compilation_database("compile_commands.json")?; assert!( !db.entries().is_empty(), "expected at least one compilation entry for a -xc++-header invocation" ); db.assert_contains(&CompilationEntryMatcher::new().file("header.hpp"))?; Ok(()) } /// Test the second run replaces the database, named or not /// /// `--overwrite` selects the behaviour that is already the default, so both /// rows must end with only the second run's entry. The rows diverge again /// when appending becomes the default: the bare one starts accumulating. // Requirements: output-append #[test] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn second_run_overwrites_output() -> Result<()> { // The flags the second run adds on top of `--output`. let cases: [(&str, &[&str]); 2] = [ ("without_append_output_is_overwritten", &[]), ("overwrite_flag_overwrites_output", &["--overwrite"]), ]; for (case, extra_flags) in cases { let env = TestEnvironment::new(case)?; // Create multiple source files env.create_source_files(&[ ("test1.c", "int func1() { return 1; }"), ("test2.c", "int func2() { return 2; }"), ])?; // Create build scripts that compile one file each let build_command1 = format!("{} -c -o test1.o test1.c", filename_of(COMPILER_C_PATH)); let build_script1_path = env.create_shell_script("build1.sh", &build_command1)?; let build_command2 = format!("{} -c -o test2.o test2.c", filename_of(COMPILER_C_PATH)); let build_script2_path = env.create_shell_script("build2.sh", &build_command2)?; // Run bear once env.run_bear_success(&[ "--output", "compile_commands.json", "--", SHELL_PATH, build_script1_path.to_str().unwrap(), ])?; // Verify the first run landed assert!(env.file_exists("compile_commands.json"), "case {case}"); let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1).with_context(|| format!("case {case} first run"))?; db.assert_contains(&CompilationEntryMatcher::new().file("test1.c")) .with_context(|| format!("case {case} first run"))?; // Run bear again over the same database let mut arguments = vec!["--output", "compile_commands.json"]; arguments.extend_from_slice(extra_flags); arguments.extend_from_slice(&["--", SHELL_PATH, build_script2_path.to_str().unwrap()]); env.run_bear_success(&arguments)?; // Verify only the second run survives assert!(env.file_exists("compile_commands.json"), "case {case}"); let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1).with_context(|| format!("case {case} second run"))?; db.assert_contains(&CompilationEntryMatcher::new().file("test2.c")) .with_context(|| format!("case {case} second run must replace the first"))?; } Ok(()) } /// Test append functionality // Requirements: output-append #[test] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn append_works_as_expected() -> Result<()> { let env = TestEnvironment::new("append_works_as_expected")?; // Create multiple source files env.create_source_files(&[ ("test1.c", "int func1() { return 1; }"), ("test2.c", "int func2() { return 2; }"), ])?; // Create build script that compiles all files let build_command1 = format!("{} -c -o test1.o test1.c", filename_of(COMPILER_C_PATH)); let build_script1_path = env.create_shell_script("build1.sh", &build_command1)?; let build_command2 = format!("{} -c -o test2.o test2.c", filename_of(COMPILER_C_PATH)); let build_script2_path = env.create_shell_script("build2.sh", &build_command2)?; // Run bear once env.run_bear_success(&[ "--output", "compile_commands.json", "--", SHELL_PATH, build_script1_path.to_str().unwrap(), ])?; // Verify compilation database assert!(env.file_exists("compile_commands.json")); let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; // Run bear again with append env.run_bear_success(&[ "--output", "compile_commands.json", "--append", "--", SHELL_PATH, build_script2_path.to_str().unwrap(), ])?; // Verify compilation database assert!(env.file_exists("compile_commands.json")); let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(2)?; Ok(()) } /// Test build with compilation failures - should still generate partial database /// Verifies Bear can handle partial build failures // Requirements: output-json-compilation-database #[test] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn broken_build_partial_success() -> Result<()> { let env = TestEnvironment::new("broken_build_partial_success")?; // Create one valid and one invalid source file env.create_source_files(&[ ("valid.c", "int main() { return 0; }"), ("invalid.c", "this is not valid C code #@!%"), ])?; // Create build script that tries to compile both (one will fail) let build_commands = [ format!("{} -c -o valid.o valid.c", filename_of(COMPILER_C_PATH)), format!("{} -c -o invalid.o invalid.c", filename_of(COMPILER_C_PATH)), ] .join("\n"); let build_script_path = env.create_shell_script("build.sh", &build_commands)?; // Run bear - should fail due to compilation error but generate partial DB let result = env.run_bear(&[ "--output", "compile_commands.json", "--", SHELL_PATH, build_script_path.to_str().unwrap(), ])?; result.assert_failure()?; // Build should fail // Compilation database should still be created with successful entries assert!(env.file_exists("compile_commands.json")); let db = env.load_compilation_database("compile_commands.json")?; // Should have entries for both attempted compilations // NOTE: commented out because of compiler wrappers produces extra entries for // the failed one. probably re-running the command with `-fdiagnostics-color`. // db.assert_count(2)?; // Should contain entry for valid compilation db.assert_contains(&compilation_entry!( file: "valid.c".to_string(), directory: env.test_dir().to_str().unwrap().to_string(), arguments: vec![ COMPILER_C_PATH.to_string(), "-c".to_string(), "-o".to_string(), "valid.o".to_string(), "valid.c".to_string(), ] ))?; // Should also contain entry for failed compilation attempt db.assert_contains(&compilation_entry!( file: "invalid.c".to_string(), directory: env.test_dir().to_str().unwrap().to_string(), arguments: vec![ COMPILER_C_PATH.to_string(), "-c".to_string(), "-o".to_string(), "invalid.o".to_string(), "invalid.c".to_string(), ] ))?; Ok(()) } /// Test empty build - should generate empty compilation database /// Verifies Bear handles builds with no compilation commands // Requirements: output-json-compilation-database #[test] #[cfg(all(has_executable_true, has_executable_shell, has_executable_echo))] fn empty_build_generates_empty_database() -> Result<()> { let env = TestEnvironment::new("empty_build_generates_empty_database")?; // Create shell script that doesn't compile anything let build_commands = format!("\"{}\" && \"{}\" 'Build completed'", TRUE_PATH, ECHO_PATH); let build_script_path = env.create_shell_script("build.sh", &build_commands)?; let result = env.run_bear(&[ "--output", "compile_commands.json", "--", SHELL_PATH, build_script_path.to_str().unwrap(), ])?; result.assert_success()?; // Should create empty compilation database assert!(env.file_exists("compile_commands.json")); let content = env.read_file("compile_commands.json")?; assert_eq!(content.trim(), "[]"); Ok(()) } /// Test compilation with multiple source files using single command /// Verifies Bear handles batch compilation commands // Requirements: output-json-compilation-database, output-compilation-entries #[test] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn multiple_sources_single_command() -> Result<()> { let env = TestEnvironment::new("multiple_sources_single_command")?; // Create multiple source files env.create_source_files(&[ ("src1.c", "int func1() { return 1; }"), ("src2.c", "int func2() { return 2; }"), ("src3.c", "int func3() { return 3; }"), ])?; // Create build script with single command compiling multiple files let build_commands = format!("{} -c src1.c src2.c src3.c", filename_of(COMPILER_C_PATH)); let build_script_path = env.create_shell_script("build.sh", &build_commands)?; // Run bear with build script let result = env.run_bear(&[ "--output", "compile_commands.json", "--", SHELL_PATH, build_script_path.to_str().unwrap(), ])?; result.assert_success()?; // Verify compilation database was created assert!(env.file_exists("compile_commands.json")); let db = env.load_compilation_database("compile_commands.json")?; // Verify entries exist for each source file // NOTE: exact count not asserted because ccache may split multi-file // commands and produce additional entries via the underlying compiler. db.assert_contains(&compilation_entry!( file: "src1.c".to_string(), directory: env.test_dir().to_str().unwrap().to_string(), arguments: vec![ COMPILER_C_PATH.to_string(), "-c".to_string(), "src1.c".to_string(), ] ))?; db.assert_contains(&compilation_entry!( file: "src2.c".to_string(), directory: env.test_dir().to_str().unwrap().to_string(), arguments: vec![ COMPILER_C_PATH.to_string(), "-c".to_string(), "src2.c".to_string(), ] ))?; db.assert_contains(&compilation_entry!( file: "src3.c".to_string(), directory: env.test_dir().to_str().unwrap().to_string(), arguments: vec![ COMPILER_C_PATH.to_string(), "-c".to_string(), "src3.c".to_string(), ] ))?; Ok(()) } /// Helper to extract the arguments array from a compilation database entry. #[cfg(target_family = "unix")] fn get_arguments(entry: &Value) -> Vec<String> { entry .get("arguments") .and_then(Value::as_array) .unwrap() .iter() .filter_map(Value::as_str) .map(String::from) .collect() } /// Verifies that CPATH environment variable survives interception and appears /// as -I flags in the compilation database. /// /// This exercises the full pipeline: shell sets CPATH, compiler is intercepted, /// environment is trimmed (must keep CPATH), event is sent over TCP, semantic /// analyzer converts CPATH to -I flags, and the compilation database is written. #[test] #[cfg(target_family = "unix")] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn env_cpath_produces_include_flags() -> Result<()> { let env = TestEnvironment::new("env_cpath_produces_include_flags")?; env.create_source_files(&[("test.c", "int main() { return 0; }")])?; let build_commands = format!( "export CPATH=/test/include_a:/test/include_b\n{} -c test.c -o test.o", filename_of(COMPILER_C_PATH) ); let build_script = env.create_shell_script("build.sh", &build_commands)?; env.run_bear_success(&[ "--output", "compile_commands.json", "--", SHELL_PATH, build_script.to_str().unwrap(), ])?; let db = env.load_compilation_database("compile_commands.json")?; // Find the entry for test.c (ccache may produce extra entries) let entry = db .entries() .iter() .find(|e| e.get("file").and_then(Value::as_str) == Some("test.c")) .expect("Expected a compilation entry for test.c"); let args = get_arguments(entry); assert!( args.windows(2).any(|w| w[0] == "-I" && w[1] == "/test/include_a"), "Expected '-I /test/include_a' from CPATH in: {:?}", args ); assert!( args.windows(2).any(|w| w[0] == "-I" && w[1] == "/test/include_b"), "Expected '-I /test/include_b' from CPATH in: {:?}", args ); Ok(()) } /// Verifies that C_INCLUDE_PATH environment variable survives interception /// and appears as -isystem flags in the compilation database. #[test] #[cfg(target_family = "unix")] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn env_c_include_path_produces_isystem_flags() -> Result<()> { let env = TestEnvironment::new("env_c_include_path_produces_isystem_flags")?; env.create_source_files(&[("test.c", "int main() { return 0; }")])?; let build_commands = format!( "export C_INCLUDE_PATH=/test/sys_include\n{} -c test.c -o test.o", filename_of(COMPILER_C_PATH) ); let build_script = env.create_shell_script("build.sh", &build_commands)?; env.run_bear_success(&[ "--output", "compile_commands.json", "--", SHELL_PATH, build_script.to_str().unwrap(), ])?; let db = env.load_compilation_database("compile_commands.json")?; let entry = db .entries() .iter() .find(|e| e.get("file").and_then(Value::as_str) == Some("test.c")) .expect("Expected a compilation entry for test.c"); let args = get_arguments(entry); assert!( args.windows(2).any(|w| w[0] == "-isystem" && w[1] == "/test/sys_include"), "Expected '-isystem /test/sys_include' from C_INCLUDE_PATH in: {:?}", args ); Ok(()) } /// Given cc -o a.out src1.c src2.c src3.c (compile-and-link in one invocation), /// one compile entry is produced per source and no entry describes the link /// output a.out. // Requirements: output-compilation-entries #[test] #[cfg(target_family = "unix")] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn compile_and_link_split_produces_compile_entries() -> Result<()> { let env = TestEnvironment::new("compile_and_link_split")?; env.create_source_files(&[ ("src1.c", "int f1(void) { return 1; }"), ("src2.c", "int f2(void) { return 2; }"), ("src3.c", "int main(void) { return 0; }"), ])?; let build = format!("{} -o a.out src1.c src2.c src3.c", filename_of(COMPILER_C_PATH)); let script = env.create_shell_script("build.sh", &build)?; env.run_bear(&["--output", "compile_commands.json", "--", SHELL_PATH, script.to_str().unwrap()])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_contains(&CompilationEntryMatcher::new().file("src1.c"))?; db.assert_contains(&CompilationEntryMatcher::new().file("src2.c"))?; db.assert_contains(&CompilationEntryMatcher::new().file("src3.c"))?; let has_linker_output = db.entries().iter().any(|e| e.get("file").and_then(Value::as_str) == Some("a.out")); assert!(!has_linker_output, "link output a.out must not appear as a source entry"); Ok(()) } /// Given cc -o a.out src.o (pure link of a pre-built object file), no entry /// is produced for this invocation. // Requirements: output-compilation-entries #[test] #[cfg(target_family = "unix")] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn pure_link_invocation_produces_no_entries() -> Result<()> { let env = TestEnvironment::new("pure_link_no_entries")?; env.create_source_files(&[("src.c", "int main(void) { return 0; }")])?; // Pre-build src.o outside of Bear so only the link step is captured. let prep = format!("{} -c src.c -o src.o", filename_of(COMPILER_C_PATH)); let prep_script = env.create_shell_script("prep.sh", &prep)?; let status = std::process::Command::new(SHELL_PATH) .arg(prep_script.to_str().unwrap()) .current_dir(env.test_dir()) .status()?; assert!(status.success(), "setup compile step failed"); // Now capture only the pure-link invocation. let link = format!("{} -o a.out src.o", filename_of(COMPILER_C_PATH)); let link_script = env.create_shell_script("link.sh", &link)?; env.run_bear(&["--output", "compile_commands.json", "--", SHELL_PATH, link_script.to_str().unwrap()])?; let content = env.read_file("compile_commands.json")?; assert_eq!(content.trim(), "[]", "pure-link invocation must produce zero entries"); Ok(()) } /// Given cc -E src.c, cc -M src.c, and cc -MM src.c (preprocess-only or /// dependency-generation-only invocations that stop before compiling), no /// entries are produced. Guards the -M/-MM stops_at_preprocessing /// classification in gcc.yaml. // Requirements: output-compilation-entries #[test] #[cfg(target_family = "unix")] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn preprocess_and_dependency_only_invocations_produce_no_entries() -> Result<()> { let env = TestEnvironment::new("preprocess_dependency_only_no_entries")?; env.create_source_files(&[("src.c", "int main(void) { return 0; }")])?; let cc = filename_of(COMPILER_C_PATH); let build = format!("{cc} -E src.c > /dev/null\n{cc} -M src.c > /dev/null\n{cc} -MM src.c > /dev/null"); let script = env.create_shell_script("build.sh", &build)?; env.run_bear(&["--output", "compile_commands.json", "--", SHELL_PATH, script.to_str().unwrap()])?; let content = env.read_file("compile_commands.json")?; assert_eq!(content.trim(), "[]", "preprocess- and dependency-only invocations must produce zero entries"); Ok(()) } /// Given cc -MD -c src.c (a dependency file emitted as a side effect of a /// real compile), exactly one entry is produced for src.c. // Requirements: output-compilation-entries #[test] #[cfg(target_family = "unix")] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn dependency_side_effect_compile_produces_entry() -> Result<()> { let env = TestEnvironment::new("dependency_side_effect_compile")?; env.create_source_files(&[("src.c", "int main(void) { return 0; }")])?; let build = format!("{} -MD -c src.c", filename_of(COMPILER_C_PATH)); let script = env.create_shell_script("build.sh", &build)?; env.run_bear(&["--output", "compile_commands.json", "--", SHELL_PATH, script.to_str().unwrap()])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; db.assert_contains(&CompilationEntryMatcher::new().file("src.c"))?; Ok(()) } /// Given cc -o a.out -lm -O2 src.c, the resulting compile entry preserves -O2 /// but drops the link-only -lm flag. // Requirements: output-compilation-entries #[test] #[cfg(target_family = "unix")] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn link_only_flags_are_stripped_from_entries() -> Result<()> { let env = TestEnvironment::new("link_only_flags_stripped")?; env.create_source_files(&[("src.c", "int main(void) { return 0; }")])?; let build = format!("{} -o a.out -lm -O2 src.c", filename_of(COMPILER_C_PATH)); let script = env.create_shell_script("build.sh", &build)?; env.run_bear(&["--output", "compile_commands.json", "--", SHELL_PATH, script.to_str().unwrap()])?; let db = env.load_compilation_database("compile_commands.json")?; let entry = db .entries() .iter() .find(|e| e.get("file").and_then(Value::as_str) == Some("src.c")) .expect("expected a compile entry for src.c"); let args = get_arguments(entry); assert!(args.iter().any(|a| a == "-O2"), "compile flag -O2 must be preserved, got {:?}", args); assert!(!args.iter().any(|a| a == "-lm"), "link-only flag -lm must be stripped, got {:?}", args); Ok(()) } /// Given cc -I first -I second -DFOO -DBAR -c src.c, the entry preserves the /// original relative order of flags (consumers depend on it for include search /// order and macro overrides). // Requirements: output-compilation-entries #[test] #[cfg(target_family = "unix")] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn argument_order_is_preserved_in_entries() -> Result<()> { let env = TestEnvironment::new("argument_order_preserved")?; env.create_source_files(&[("src.c", "int main(void) { return 0; }")])?; std::fs::create_dir_all(env.test_dir().join("first"))?; std::fs::create_dir_all(env.test_dir().join("second"))?; let build = format!("{} -I first -I second -DFOO -DBAR -c src.c", filename_of(COMPILER_C_PATH)); let script = env.create_shell_script("build.sh", &build)?; env.run_bear(&["--output", "compile_commands.json", "--", SHELL_PATH, script.to_str().unwrap()])?; let db = env.load_compilation_database("compile_commands.json")?; let entry = db .entries() .iter() .find(|e| e.get("file").and_then(Value::as_str) == Some("src.c")) .expect("expected a compile entry for src.c"); let args = get_arguments(entry); let pos = |needle: &str| args.iter().position(|a| a == needle); let first = pos("first").expect("missing include path 'first'"); let second = pos("second").expect("missing include path 'second'"); let foo = pos("-DFOO").expect("missing -DFOO"); let bar = pos("-DBAR").expect("missing -DBAR"); assert!(first < second, "'-I first' must precede '-I second', got {:?}", args); assert!(foo < bar, "'-DFOO' must precede '-DBAR', got {:?}", args); Ok(()) } /// Given cc --version (info-only invocation), no entry is produced. // Requirements: output-compilation-entries #[test] #[cfg(target_family = "unix")] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn info_only_invocation_produces_no_entries() -> Result<()> { let env = TestEnvironment::new("info_only_no_entries")?; let build = format!("{} --version", filename_of(COMPILER_C_PATH)); let script = env.create_shell_script("build.sh", &build)?; env.run_bear(&["--output", "compile_commands.json", "--", SHELL_PATH, script.to_str().unwrap()])?; let content = env.read_file("compile_commands.json")?; assert_eq!(content.trim(), "[]", "info-only invocation must produce zero entries"); Ok(()) } /// Given a successful build, Bear's atomic-write step removes the deterministic /// temp file (a hidden, randomly named file next to it) so only the final /// compile_commands.json remains in the output directory. // Requirements: output-atomic-write #[test] #[cfg(target_family = "unix")] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn atomic_write_cleans_up_temp_file_on_success() -> Result<()> { let env = TestEnvironment::new("atomic_write_temp_cleanup")?; env.create_source_files(&[("src.c", "int main(void) { return 0; }")])?; let build = format!("{} -c src.c -o src.o", filename_of(COMPILER_C_PATH)); let script = env.create_shell_script("build.sh", &build)?; env.run_bear_success(&["--output", "compile_commands.json", "--", SHELL_PATH, script.to_str().unwrap()])?; assert!(env.file_exists("compile_commands.json")); assert_no_temp_files(&env)?; Ok(()) } /// When the build command cannot start, the run ends in Bear's own error: /// neither the compilation database nor the events file may be published, /// an existing one must stay as it was, and no temp file may remain (#718). // Requirements: output-atomic-write, interception-events-format #[test] fn atomic_write_publishes_nothing_when_build_cannot_start() -> Result<()> { let cases: [(&[&str], &str); 2] = [ (&["--output", "compile_commands.json", "--", "nosuchcmd"], "compile_commands.json"), (&["intercept", "--output", "events.json", "--", "nosuchcmd"], "events.json"), ]; for (args, output) in cases { let env = TestEnvironment::new("atomic_write_no_publish_on_start_failure")?; env.run_bear(args)?.assert_failure()?; assert!(!env.file_exists(output), "{output}: must not be created"); assert_no_temp_files(&env)?; env.create_source_files(&[(output, "previous content")])?; env.run_bear(args)?.assert_failure()?; assert_eq!(env.read_file(output)?, "previous content", "{output}: must stay unchanged"); assert_no_temp_files(&env)?; } Ok(()) } /// The events file is staged like the compilation database: written to a /// temp file during the build and renamed into place at the end. // Requirements: output-atomic-write, interception-events-format #[test] #[cfg(has_executable_true)] fn atomic_write_publishes_events_file_on_success() -> Result<()> { let env = TestEnvironment::new("atomic_write_events_on_success")?; env.run_bear_success(&["intercept", "--output", "events.json", "--", TRUE_PATH])?; assert!(env.file_exists("events.json")); assert_no_temp_files(&env)?; Ok(()) } /// A replay whose input is rejected entirely ends in Bear's own error, so /// it must not replace an existing compilation database. // Requirements: output-atomic-write #[test] fn atomic_write_keeps_database_when_replay_fails() -> Result<()> { let env = TestEnvironment::new("atomic_write_keeps_db_on_replay_failure")?; env.create_source_files(&[ ("compile_commands.json", "previous content"), ("events.json", "this is not an event\n"), ])?; env.run_bear(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])? .assert_failure()?; assert_eq!(env.read_file("compile_commands.json")?, "previous content"); assert_no_temp_files(&env)?; Ok(()) } /// Given an output path that exists as a non-empty directory, Bear fails /// before the build runs (the destination cannot be written), exits non-zero, /// and leaves the pre-existing filesystem object untouched. // Requirements: output-atomic-write, cli-exit-codes #[test] #[cfg(target_family = "unix")] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn atomic_write_preserves_existing_object_on_failure() -> Result<()> { use std::fs; let env = TestEnvironment::new("atomic_write_preserves_on_failure")?; env.create_source_files(&[("src.c", "int main(void) { return 0; }")])?; // Make the output path a non-empty directory. Bear cannot write a // directory, and that holds even for root, so this is stable across // test environments. let out_path = env.test_dir().join("compile_commands.json"); fs::create_dir(&out_path)?; let sentinel = out_path.join("sentinel.txt"); fs::write(&sentinel, "untouched")?; let build = format!("{} -c src.c -o src.o", filename_of(COMPILER_C_PATH)); let script = env.create_shell_script("build.sh", &build)?; // The database cannot be written (the output path is a directory), so // Bear must exit non-zero, and the pre-existing object must survive. let result = env.run_bear(&["--output", out_path.to_str().unwrap(), "--", SHELL_PATH, script.to_str().unwrap()])?; result.assert_failure()?; assert!(out_path.is_dir(), "output path must still be a directory after failed write"); assert_eq!( fs::read_to_string(&sentinel)?, "untouched", "pre-existing content inside output path must not be modified on write failure" ); Ok(()) } /// Given a destination that becomes unwritable during the build (the build /// itself makes a non-empty directory at the output path), the final rename /// fails: Bear exits non-zero although the build succeeded, the destination /// is untouched, and the temporary file is kept for debugging. // Requirements: output-atomic-write, cli-exit-codes #[test] #[cfg(target_family = "unix")] #[cfg(all(has_executable_compiler_c, has_executable_shell, has_executable_mkdir))] fn atomic_write_keeps_temp_file_when_final_rename_fails() -> Result<()> { use std::fs; let env = TestEnvironment::new("atomic_write_rename_fails")?; env.create_source_files(&[("src.c", "int main(void) { return 0; }")])?; let out_path = env.test_dir().join("compile_commands.json"); let sentinel = out_path.join("sentinel.txt"); let build = format!( "{} -c src.c -o src.o\n{} '{}'\necho untouched > '{}'", filename_of(COMPILER_C_PATH), MKDIR_PATH, out_path.display(), sentinel.display() ); let script = env.create_shell_script("build.sh", &build)?; let result = env.run_bear(&["--output", out_path.to_str().unwrap(), "--", SHELL_PATH, script.to_str().unwrap()])?; result.assert_failure()?; assert!(env.file_exists("src.o"), "the build must have compiled the source"); assert!(out_path.is_dir(), "output path must still be the directory the build made"); assert_eq!(fs::read_to_string(&sentinel)?, "untouched\n"); assert!( result.stderr().contains(out_path.to_str().unwrap()), "the error must name the output path: {}", result.stderr() ); let mut kept = Vec::new(); for entry in fs::read_dir(env.test_dir())? { let name = entry?.file_name().to_string_lossy().into_owned(); if name.starts_with(".compile_commands.json.") && name.ends_with(".tmp") { kept.push(name); } } assert_eq!(kept.len(), 1, "the temporary file must be kept next to the output: {kept:?}"); Ok(()) } /// Given cc -o a.out src1.c src2.c with the output field enabled via /// configuration, every resulting entry records output = a.out (the known /// limitation: the single -o value is copied verbatim, not inferred per /// source). // Requirements: output-compilation-entries #[test] #[cfg(target_family = "unix")] #[cfg(has_preload_library)] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn output_field_is_recorded_when_enabled() -> Result<()> { let env = TestEnvironment::new("output_field_enabled")?; env.create_source_files(&[ ("src1.c", "int f1(void) { return 1; }"), ("src2.c", "int main(void) { return 0; }"), ])?; let build = format!("{} -o a.out src1.c src2.c", filename_of(COMPILER_C_PATH)); let script = env.create_shell_script("build.sh", &build)?; let config = format!( r#" schema: "4.2" intercept: mode: preload path: "{preload}" format: paths: directory: as-is file: as-is entries: use_array_format: true include_output_field: true "#, preload = PRELOAD_LIBRARY_PATH ); let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; env.run_bear(&[ "--output", "compile_commands.json", "--config", config_path.to_str().unwrap(), "--", SHELL_PATH, script.to_str().unwrap(), ])?; let db = env.load_compilation_database("compile_commands.json")?; // Every entry from this invocation must record output = a.out. let compile_entries: Vec<_> = db .entries() .iter() .filter(|e| { let file = e.get("file").and_then(Value::as_str).unwrap_or(""); file == "src1.c" || file == "src2.c" }) .collect(); assert!(!compile_entries.is_empty(), "expected compile entries for src1.c / src2.c"); for entry in compile_entries { let output = entry.get("output").and_then(Value::as_str); assert_eq!( output, Some("a.out"), "entry {:?} must have output = a.out, got {:?}", entry.get("file"), output ); } Ok(()) } /// A fake `cc` binary whose basename is the ambiguous name `cc` and whose /// `--version` output advertises Clang. Bear must probe the binary, classify /// it as Clang (not GCC, which is what the regex would say), and dispatch /// to the Clang interpreter. Verified by: /// 1. an entry for `hello.c` in the compilation database (recognition + /// end-to-end pipeline succeeded); /// 2. a `Clang` debug log line for the cc execution AND no `GCC` log line /// (proves dispatch went through the Clang interpreter, not the regex /// fallback to GCC). // Requirements: recognition-ambiguous-name-probe #[cfg(target_family = "unix")] #[cfg(has_preload_library)] #[cfg(has_executable_shell)] #[test] fn probe_dispatches_cc_to_clang_when_version_advertises_clang() -> Result<()> { use std::os::unix::fs::PermissionsExt; let env = TestEnvironment::new("probe_dispatches_cc_to_clang")?; env.create_source_files(&[("hello.c", "int main(void) { return 0; }")])?; // Fake `cc`: a shell script that prints a Clang version banner on // --version and otherwise exits 0 without touching the source file. // The probe only cares about --version output; the build itself does // not need to produce object code for the recognizer to record an entry. let fake_cc_dir = env.test_dir().join("fake-cc"); std::fs::create_dir(&fake_cc_dir)?; let fake_cc = fake_cc_dir.join("cc"); std::fs::write( &fake_cc, "#!/bin/sh\n\ if [ \"$1\" = \"--version\" ]; then\n\ echo 'clang version 17.0.0 (probe-test)'\n\ exit 0\n\ fi\n\ exit 0\n", )?; std::fs::set_permissions(&fake_cc, std::fs::Permissions::from_mode(0o755))?; // Build script prepends the fake-cc dir to PATH so the bare name `cc` // resolves to our script. The preload then records the absolute path // of the fake cc as the execution's executable, basename `cc`, which // is what triggers the probe. let build_commands = format!("PATH=\"{}:$PATH\" cc -c hello.c\n", fake_cc_dir.to_str().expect("test dir is valid UTF-8"),); let script = env.create_shell_script("build.sh", &build_commands)?; // Use command_bear() directly so we can force RUST_LOG=debug without // touching the test process env (which would race with parallel tests). // // `CPATH` carries a value that names GCC, to reproduce #716: the debug // log dumps the intercepted execution's environment, so a build // environment that merely mentions GCC (as a Gentoo package build does) // must not read as "the GCC interpreter recognized this command". // // It has to be a compiler-read variable rather than `CC`, which is how // the reported build spelled it. `CC` is a program variable: in wrapper // mode the runner resolves its value on PATH, so a Gentoo triplet name // would log a resolution warning on every other distro. `CPATH` reaches // the log through the same env filter (`intercept::environment`) without // ever being resolved, so the reproduction costs nothing on hosts that // have no such compiler. let mut cmd = env.command_bear(); cmd.current_dir(env.test_dir()) .env("RUST_LOG", "debug") .env("RUST_BACKTRACE", "1") .env("CPATH", "/nonexistent/x86_64-pc-linux-gnu-gcc") .args(["--output", "compile_commands.json", "--", SHELL_PATH, script.to_str().unwrap()]); let output = cmd.output()?; let stderr = String::from_utf8_lossy(&output.stderr); assert!(output.status.success(), "bear failed:\n{}", stderr); // The repro only works while the debug log carries the GCC-naming value. assert!( stderr.contains("x86_64-pc-linux-gnu-gcc"), "the debug log no longer shows the GCC-naming environment value.\nstderr:\n{}", stderr ); // (1) Entry for hello.c must exist. let db = env.load_compilation_database("compile_commands.json")?; let hello_entries: Vec<_> = db.entries().iter().filter(|e| e.get("file").and_then(Value::as_str) == Some("hello.c")).collect(); assert!(!hello_entries.is_empty(), "expected an entry for hello.c, got entries: {:#?}", db.entries()); // (2) Dispatch must have gone through the Clang interpreter. The // OutputLogger combinator labels each line with the compiler id, so it // emits `clang : Recognized(...)` when the Clang flag table // parses the command, and `gcc : Recognized(...)` if the // GCC flag table did. We assert the former is present and the latter is // absent for this run. let recognized = interpreters_that_recognized(&stderr); assert!( recognized.contains(&"clang"), "expected a `clang ... Recognized(` log line proving probe dispatched to Clang.\nstderr:\n{}", stderr ); assert!( !recognized.contains(&"gcc"), "did not expect a `gcc ... Recognized(` log line; that would mean the probe was bypassed and the regex fell back to GCC.\nstderr:\n{}", stderr ); Ok(()) } /// Collect the interpreter ids that reported a `Recognized` verdict in a /// debug log, as emitted by the `OutputLogger` combinator: /// /// ```text /// [... semantic::interpreters::combinators] clang : Recognized(Command { ... }) /// ``` /// /// The id and the verdict are matched positionally, not by substring: both /// the `Execution` dump of a `NotRecognized(...)` verdict and the build /// environment it carries can mention a compiler id in passing (a Gentoo /// build sets `CC=x86_64-pc-linux-gnu-gcc` and `CFLAGS=-frecord-gcc-switches`, /// for instance), and `NotRecognized(` itself ends in `Recognized(`. fn interpreters_that_recognized(stderr: &str) -> Vec<&str> { stderr .lines() .filter_map(|line| line.split_once("combinators] ")) .filter_map(|(_, entry)| entry.split_once(": ")) .filter(|(_, verdict)| verdict.starts_with("Recognized(")) .map(|(id, _)| id.trim_end()) .collect() } /// Regression guard for #716: the probe test above read the debug log with /// plain `contains` checks, so a `NotRecognized` line whose environment dump /// mentioned GCC (the Gentoo package build sets `CC=x86_64-pc-linux-gnu-gcc`) /// read as "the probe fell back to GCC" and failed the test on a run where /// the probe had in fact dispatched to Clang. The lines below are taken from /// that build log. // Requirements: recognition-ambiguous-name-probe #[test] fn recognized_ids_come_from_the_verdict_not_the_execution_dump() { let log = concat!( "[21:17:10.137 DEBUG bear[2649] semantic::interpreters::combinators] coreutils_to_ignore : ", "NotRecognized(Execution { executable: \"/tmp/fake-cc/cc\", arguments: [\"cc\", \"-c\", ", "\"hello.c\"], environment: {\"CC\": \"x86_64-pc-linux-gnu-gcc\", \"CFLAGS\": ", "\"-O2 -ggdb3 -frecord-gcc-switches\"} })\n", "[21:17:10.142 DEBUG bear[2649] semantic::interpreters::combinators] clang : ", "Recognized(Command { executable: \"/tmp/fake-cc/cc\", source_mode: PerSourceStripped })\n", ); let sut = interpreters_that_recognized(log); assert_eq!(sut, vec!["clang"], "only the Recognized verdict's own id counts, got {sut:?}"); } /// Regression guard for #532: --append was unusable on large projects in the /// 3.x C++ implementation. output-append states no time bound, so this test /// guards the linear scaling. It synthesizes a large existing database and /// asserts the append completes well inside a linear time budget. An O(N^2) /// regression in the read/merge path would blow past the budget. // Requirements: output-append #[test] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn append_to_large_database_stays_within_budget() -> Result<()> { let env = TestEnvironment::new("append_to_large_database")?; // Synthesize an existing compile_commands.json with N entries. N is // large enough that a quadratic regression takes much longer than the // budget, but small enough that linear behaviour stays well under it. let n: usize = 5_000; let dir = env.test_dir().to_str().unwrap().to_string(); let entries: Vec<_> = (0..n) .map(|i| { let file = format!("file_{i}.c"); let out = format!("file_{i}.o"); serde_json::json!({ "directory": dir, "arguments": [COMPILER_C_PATH, "-c", file, "-o", out], "file": file, }) }) .collect(); let existing = serde_json::to_string(&entries)?; std::fs::write(env.test_dir().join("compile_commands.json"), existing)?; env.create_source_files(&[("new.c", "int main() { return 0; }")])?; let cmd = format!("{} -c new.c -o new.o", filename_of(COMPILER_C_PATH)); let script_path = env.create_shell_script("build.sh", &cmd)?; let started = std::time::Instant::now(); env.run_bear_success(&[ "--append", "--output", "compile_commands.json", "--", SHELL_PATH, script_path.to_str().unwrap(), ])?; let elapsed = started.elapsed(); // Soft upper bound: linear append of N+1 entries fits inside a second // on any modern machine. 60s gives huge headroom for slow CI runners // doing linear work, while still failing loudly on a quadratic // regression (5_000^2 = 25M ops vs 5_000 ops). assert!( elapsed < std::time::Duration::from_secs(60), "append of one entry into a {n}-entry database took {elapsed:?}; \ expected linear-time behaviour" ); // The N synthesized entries must survive plus at least one new entry // for new.c. Some hosts (e.g. ccache masquerade in PATH) capture // multiple events for a single compile; the exact count is host- // dependent, but the lower bound is invariant. let db = env.load_compilation_database("compile_commands.json")?; assert!( db.entries().len() > n, "expected more than {n} entries after appending one compile to a \ {n}-entry database, got {}", db.entries().len() ); db.assert_contains(&CompilationEntryMatcher::new().file("new.c"))?; Ok(()) } /// Fails when a temporary output file (hidden, `.tmp` suffix) is left in the /// test directory. fn assert_no_temp_files(env: &TestEnvironment) -> Result<()> { let mut leftovers = Vec::new(); for entry in std::fs::read_dir(env.test_dir())? { let name = entry?.file_name().to_string_lossy().into_owned(); if name.starts_with('.') && name.ends_with(".tmp") { leftovers.push(name); } } assert!(leftovers.is_empty(), "temp files must not be left behind: {leftovers:?}"); Ok(()) } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/tests/cases/config.rs���������������������������������������0000664�0000000�0000000�00000164407�15271777203�0024367�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: GPL-3.0-or-later //! Configuration tests for Bear integration //! //! These tests verify that Bear correctly handles configuration files //! and applies filtering rules, adapted to Bear's actual configuration format. use crate::fixtures::constants::*; use crate::fixtures::infrastructure::*; use anyhow::Result; /// Test basic configuration file loading /// Verifies Bear can load a valid configuration file #[test] #[cfg(has_preload_library)] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn basic_config_loading() -> Result<()> { let env = TestEnvironment::new("basic_config")?; // Create source file env.create_source_files(&[("test.c", "int main() { return 0; }")])?; let build_commands = format!("{} -c test.c", COMPILER_C_PATH); let script_path = env.create_shell_script("build.sh", &build_commands)?; // Create basic valid config let config = format!( r#" schema: "4.2" intercept: mode: preload path: "{}" sources: only_existing_files: true format: paths: directory: as-is file: as-is "#, PRELOAD_LIBRARY_PATH ); let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; // Run bear with config let _output = env.run_bear_success(&[ "--output", "compile_commands.json", "--config", config_path.to_str().unwrap(), "--", SHELL_PATH, script_path.to_str().unwrap(), ])?; // Verify the compilation database was created let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; // Verify the entry contains the expected compilation command let expected_args = vec![COMPILER_C_PATH.to_string(), "-c".to_string(), "test.c".to_string()]; let matcher = CompilationEntryMatcher::new() .file("test.c") .directory(env.test_dir().to_str().unwrap()) .arguments(expected_args); db.assert_contains(&matcher)?; Ok(()) } /// Test compiler ignore functionality /// Verifies that compilers marked with ignore: true are excluded #[test] #[cfg(has_preload_library)] #[cfg(all(has_executable_compiler_c, has_executable_compiler_cxx, has_executable_shell))] fn compiler_ignore_config() -> Result<()> { let env = TestEnvironment::new("compiler_ignore")?; // Create source files for both C and C++ env.create_source_files(&[ ("source.c", "int main() { return 0; }"), ("source.cpp", "int main() { return 0; }"), ])?; let build_commands = format!("{} -c source.c\n{} -c source.cpp", COMPILER_C_PATH, COMPILER_CXX_PATH); let script_path = env.create_shell_script("build.sh", &build_commands)?; // Create config that ignores the C++ compiler let config = format!( r#" schema: "4.2" intercept: mode: preload path: "{preload_path}" compilers: - path: "{cxx}" ignore: true sources: only_existing_files: true "#, preload_path = PRELOAD_LIBRARY_PATH, cxx = COMPILER_CXX_PATH ); let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; // Run bear with config let _output = env.run_bear_success(&[ "--output", "compile_commands.json", "--config", config_path.to_str().unwrap(), "--", SHELL_PATH, script_path.to_str().unwrap(), ])?; // Should only capture C compiler invocation, not C++ let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; // Verify the entry contains the expected C compilation command let expected_args = vec![COMPILER_C_PATH.to_string(), "-c".to_string(), "source.c".to_string()]; let matcher = CompilationEntryMatcher::new() .file("source.c") .directory(env.test_dir().to_str().unwrap()) .arguments(expected_args); db.assert_contains(&matcher)?; Ok(()) } /// Test source file filtering /// Verifies only_existing_files configuration option #[test] #[cfg(has_preload_library)] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn source_file_filtering() -> Result<()> { let env = TestEnvironment::new("source_filtering")?; // Create only one source file (the other will be missing) env.create_source_files(&[("existing.c", "int main() { return 0; }")])?; let build_commands = format!( "{} -c existing.c\n{} -c nonexistent.c 2>/dev/null || true", COMPILER_C_PATH, COMPILER_C_PATH ); let script_path = env.create_shell_script("build.sh", &build_commands)?; // Config to include only existing source files let config = format!( r#" schema: "4.2" intercept: mode: preload path: "{}" sources: only_existing_files: true "#, PRELOAD_LIBRARY_PATH ); let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; let _output = env.run_bear_success(&[ "--output", "compile_commands.json", "--config", config_path.to_str().unwrap(), "--", SHELL_PATH, script_path.to_str().unwrap(), ])?; // Only existing.c should be in the output let db = env.load_compilation_database("compile_commands.json")?; // Verify that we have entries and at least one is for existing.c let expected_args = vec![COMPILER_C_PATH.to_string(), "-c".to_string(), "existing.c".to_string()]; let matcher = CompilationEntryMatcher::new() .file("existing.c") .directory(env.test_dir().to_str().unwrap()) .arguments(expected_args); db.assert_contains(&matcher)?; Ok(()) } /// Test source directory filter with include/exclude rules. /// Verifies last-match-wins semantics and default-include behavior end-to-end /// through the YAML config -> output pipeline path. // Requirements: output-source-directory-filter #[test] #[cfg(has_preload_library)] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn source_directory_filter_config() -> Result<()> { let env = TestEnvironment::new("source_directory_filter")?; // Four files across three directories: one top-level matched by `include src`, // one excluded by `exclude src/test`, one re-included by a more specific // `include src/test/integration` (last-match-wins), and one outside any rule // (default include). env.create_source_files(&[ ("src/main.c", "int main() { return 0; }"), ("src/test/unit.c", "int unit() { return 0; }"), ("src/test/integration/api.c", "int api() { return 0; }"), ("lib/util.c", "int util() { return 0; }"), ])?; let build_commands = [ format!("{} -c src/main.c -o src/main.o", COMPILER_C_PATH), format!("{} -c src/test/unit.c -o src/test/unit.o", COMPILER_C_PATH), format!("{} -c src/test/integration/api.c -o src/test/integration/api.o", COMPILER_C_PATH), format!("{} -c lib/util.c -o lib/util.o", COMPILER_C_PATH), ] .join("\n"); let script_path = env.create_shell_script("build.sh", &build_commands)?; let config = format!( r#" schema: "4.2" intercept: mode: preload path: "{preload}" sources: directories: - path: src action: include - path: src/test action: exclude - path: src/test/integration action: include format: paths: directory: as-is file: as-is "#, preload = PRELOAD_LIBRARY_PATH ); let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; env.run_bear_success(&[ "--output", "compile_commands.json", "--config", config_path.to_str().unwrap(), "--", SHELL_PATH, script_path.to_str().unwrap(), ])?; let db = env.load_compilation_database("compile_commands.json")?; // Should contain: src/main.c (include), src/test/integration/api.c (re-included // by the more specific rule), lib/util.c (default include). db.assert_contains(&CompilationEntryMatcher::new().file("src/main.c"))?; db.assert_contains(&CompilationEntryMatcher::new().file("src/test/integration/api.c"))?; db.assert_contains(&CompilationEntryMatcher::new().file("lib/util.c"))?; // Must NOT contain src/test/unit.c - excluded by `exclude src/test`. let excluded = db .entries() .iter() .any(|entry| entry.get("file").and_then(|v| v.as_str()) == Some("src/test/unit.c")); assert!(!excluded, "src/test/unit.c should have been excluded by `exclude src/test` rule"); Ok(()) } /// Test source filename-pattern filter excluding a generated file by basename glob. /// Verifies that a `sources.files` exclude rule drops the matching file while keeping /// hand-written sources, through the YAML config -> output pipeline path. // Requirements: output-generated-file-filter #[test] #[cfg(has_preload_library)] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn source_file_pattern_filter_config() -> Result<()> { let env = TestEnvironment::new("source_file_pattern_filter")?; // A hand-written source and a Qt-moc-style generated source. env.create_source_files(&[ ("main.cpp", "int main() { return 0; }"), ("moc_window.cpp", "int moc_window() { return 0; }"), ])?; let build_commands = [ format!("{} -c main.cpp -o main.o", COMPILER_C_PATH), format!("{} -c moc_window.cpp -o moc_window.o", COMPILER_C_PATH), ] .join("\n"); let script_path = env.create_shell_script("build.sh", &build_commands)?; let config = format!( r#" schema: "4.2" intercept: mode: preload path: "{preload}" sources: files: - pattern: "moc_*.cpp" action: exclude format: paths: directory: as-is file: as-is "#, preload = PRELOAD_LIBRARY_PATH ); let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; env.run_bear_success(&[ "--output", "compile_commands.json", "--config", config_path.to_str().unwrap(), "--", SHELL_PATH, script_path.to_str().unwrap(), ])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_contains(&CompilationEntryMatcher::new().file("main.cpp"))?; let excluded = db.entries().iter().any(|entry| entry.get("file").and_then(|v| v.as_str()) == Some("moc_window.cpp")); assert!(!excluded, "moc_window.cpp should have been excluded by the `moc_*.cpp` pattern rule"); Ok(()) } /// Test that with no `sources.files` configuration, generated-looking sources are not /// filtered: behaviour is unchanged from before the file-pattern filter existed. // Requirements: output-generated-file-filter #[test] #[cfg(has_preload_library)] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn source_file_pattern_filter_default_off() -> Result<()> { let env = TestEnvironment::new("source_file_pattern_filter_default_off")?; env.create_source_files(&[ ("main.cpp", "int main() { return 0; }"), ("moc_window.cpp", "int moc_window() { return 0; }"), ])?; let build_commands = [ format!("{} -c main.cpp -o main.o", COMPILER_C_PATH), format!("{} -c moc_window.cpp -o moc_window.o", COMPILER_C_PATH), ] .join("\n"); let script_path = env.create_shell_script("build.sh", &build_commands)?; let config = format!( r#" schema: "4.2" intercept: mode: preload path: "{preload}" format: paths: directory: as-is file: as-is "#, preload = PRELOAD_LIBRARY_PATH ); let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; env.run_bear_success(&[ "--output", "compile_commands.json", "--config", config_path.to_str().unwrap(), "--", SHELL_PATH, script_path.to_str().unwrap(), ])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_contains(&CompilationEntryMatcher::new().file("main.cpp"))?; db.assert_contains(&CompilationEntryMatcher::new().file("moc_window.cpp"))?; Ok(()) } /// Test header-entry synthesis with the `siblings` strategy. /// Verifies that a header file sharing a directory with a compiled source /// receives a synthesized entry cloning that source's arguments, through the /// YAML config -> output pipeline path. // Requirements: output-header-entries #[test] #[cfg(has_preload_library)] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn header_entries_siblings_config() -> Result<()> { let env = TestEnvironment::new("header_entries_siblings")?; // A compiled source and a sibling header that is never compiled itself. env.create_source_files(&[("main.c", "int main() { return 0; }"), ("util.h", "int util(void);")])?; let build_commands = format!("{} -c main.c -o main.o", COMPILER_C_PATH); let script_path = env.create_shell_script("build.sh", &build_commands)?; let config = format!( r#" schema: "4.2" intercept: mode: preload path: "{preload}" headers: enabled: true strategy: siblings format: paths: directory: as-is file: as-is "#, preload = PRELOAD_LIBRARY_PATH ); let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; env.run_bear_success(&[ "--output", "compile_commands.json", "--config", config_path.to_str().unwrap(), "--", SHELL_PATH, script_path.to_str().unwrap(), ])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(2)?; db.assert_contains(&CompilationEntryMatcher::new().file("main.c"))?; let synthesized_args = vec![COMPILER_C_PATH.to_string(), "-c".to_string(), "util.h".to_string()]; let synthesized_matcher = CompilationEntryMatcher::new() .file("util.h") .directory(env.test_dir().to_str().unwrap()) .arguments(synthesized_args); db.assert_contains(&synthesized_matcher)?; Ok(()) } /// Test header-entry synthesis with the `dependency-files` strategy. /// Verifies that headers listed as prerequisites in the make-style /// dependency file a build emits (via `-MMD -MF`) receive synthesized /// entries, cloning the donor's arguments with the source swapped and the /// output-file flag removed. // Requirements: output-header-entries #[test] #[cfg(has_preload_library)] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn header_entries_dependency_files_config() -> Result<()> { let env = TestEnvironment::new("header_entries_dependency_files")?; // main.c actually includes util.h, so the compiler's dependency file // lists it as a prerequisite. env.create_source_files(&[ ("main.c", "#include \"util.h\"\nint main() { return 0; }"), ("util.h", "int util(void);"), ])?; let build_commands = format!("{} -c main.c -MMD -MF main.d -o main.o", COMPILER_C_PATH); let script_path = env.create_shell_script("build.sh", &build_commands)?; let config = format!( r#" schema: "4.2" intercept: mode: preload path: "{preload}" headers: enabled: true strategy: dependency-files format: paths: directory: as-is file: as-is "#, preload = PRELOAD_LIBRARY_PATH ); let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; env.run_bear_success(&[ "--output", "compile_commands.json", "--config", config_path.to_str().unwrap(), "--", SHELL_PATH, script_path.to_str().unwrap(), ])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(2)?; db.assert_contains(&CompilationEntryMatcher::new().file("main.c"))?; let synthesized_args = vec![ COMPILER_C_PATH.to_string(), "-c".to_string(), "util.h".to_string(), "-MMD".to_string(), "-MF".to_string(), "main.d".to_string(), ]; let synthesized_matcher = CompilationEntryMatcher::new() .file("util.h") .directory(env.test_dir().to_str().unwrap()) .arguments(synthesized_args); db.assert_contains(&synthesized_matcher)?; Ok(()) } /// A header that is BOTH compiled directly by the build and discoverable by /// the siblings strategy gets exactly one entry: the real compile streams /// first, and the synthesized clone is collapsed by the default duplicate /// filter (directory + file). // Requirements: output-header-entries #[test] #[cfg(has_preload_library)] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn header_entries_direct_header_compile_yields_one_entry() -> Result<()> { let env = TestEnvironment::new("header_entries_direct_compile")?; env.create_source_files(&[("main.c", "int main() { return 0; }"), ("util.h", "int util(void);")])?; // util.h is compiled directly (a precompiled-header build step), then // main.c beside it -- the siblings strategy would synthesize a second // util.h entry from main.c's arguments. The -DDIRECT_COMPILE marker // distinguishes the real compile from the synthesized clone (whose // arguments derive from main.c's and lack the marker), so the assertion // below proves the real entry existed and won first-seen dedup. let build_commands = format!("{cc} -DDIRECT_COMPILE -c util.h\n{cc} -c main.c -o main.o", cc = COMPILER_C_PATH); let script_path = env.create_shell_script("build.sh", &build_commands)?; let config = format!( r#" schema: "4.2" intercept: mode: preload path: "{preload}" headers: enabled: true strategy: siblings format: paths: directory: as-is file: as-is "#, preload = PRELOAD_LIBRARY_PATH ); let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; env.run_bear_success(&[ "--output", "compile_commands.json", "--config", config_path.to_str().unwrap(), "--", SHELL_PATH, script_path.to_str().unwrap(), ])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(2)?; db.assert_contains(&CompilationEntryMatcher::new().file("main.c"))?; let header_entries: Vec<_> = db .entries() .iter() .filter(|entry| entry.get("file").and_then(|v| v.as_str()) == Some("util.h")) .collect(); assert_eq!( header_entries.len(), 1, "exactly one util.h entry expected (real compile + synthesized clone must dedup): {header_entries:?}" ); let survivor_args: Vec<&str> = header_entries[0] .get("arguments") .and_then(|v| v.as_array()) .map(|a| a.iter().filter_map(|v| v.as_str()).collect()) .unwrap_or_default(); assert!( survivor_args.contains(&"-DDIRECT_COMPILE"), "the surviving util.h entry must be the real compile (first seen), not the synthesized clone: {survivor_args:?}" ); Ok(()) } /// Test that with no `headers` configuration, no synthesized header entries /// appear in the output: behaviour is unchanged from before header-entry /// synthesis existed. // Requirements: output-header-entries #[test] #[cfg(has_preload_library)] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn header_entries_default_off() -> Result<()> { let env = TestEnvironment::new("header_entries_default_off")?; env.create_source_files(&[("main.c", "int main() { return 0; }"), ("util.h", "int util(void);")])?; let build_commands = format!("{} -c main.c -o main.o", COMPILER_C_PATH); let script_path = env.create_shell_script("build.sh", &build_commands)?; let config = format!( r#" schema: "4.2" intercept: mode: preload path: "{preload}" format: paths: directory: as-is file: as-is "#, preload = PRELOAD_LIBRARY_PATH ); let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; env.run_bear_success(&[ "--output", "compile_commands.json", "--config", config_path.to_str().unwrap(), "--", SHELL_PATH, script_path.to_str().unwrap(), ])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; db.assert_contains(&CompilationEntryMatcher::new().file("main.c"))?; let has_header_entry = db.entries().iter().any(|entry| entry.get("file").and_then(|v| v.as_str()) == Some("util.h")); assert!(!has_header_entry, "util.h should not have a synthesized entry when headers are not enabled"); Ok(()) } /// Test path format configuration /// Verifies different path formatting options // Requirements: output-path-format #[test] #[cfg(has_preload_library)] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn path_format_config() -> Result<()> { let env = TestEnvironment::new("path_format")?; env.create_source_files(&[("src/main.c", "int main() { return 0; }")])?; let build_commands = format!("cd src && {} -c main.c", COMPILER_C_PATH); let script_path = env.create_shell_script("build.sh", &build_commands)?; // Test absolute path format let config = format!( r#" schema: "4.2" intercept: mode: preload path: "{}" format: paths: directory: absolute file: absolute sources: only_existing_files: true "#, PRELOAD_LIBRARY_PATH ); let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; let _output = env.run_bear_success(&[ "--output", "compile_commands.json", "--config", config_path.to_str().unwrap(), "--", "sh", script_path.to_str().unwrap(), ])?; // Verify the format is applied let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; // Verify the entry contains the expected compilation command // When absolute path format is used, the source file argument is also absolute let src_dir = env.test_dir().join("src"); let absolute_src_dir = src_dir.canonicalize().unwrap_or_else(|_| src_dir.clone()); let absolute_file_path = absolute_src_dir.join("main.c"); let expected_args = vec![COMPILER_C_PATH.to_string(), "-c".to_string(), absolute_file_path.to_str().unwrap().to_string()]; // For absolute path format, we expect the file and directory to be absolute paths let matcher = CompilationEntryMatcher::new() .file(absolute_file_path.to_str().unwrap()) .directory(absolute_src_dir.to_str().unwrap()) .arguments(expected_args); db.assert_contains(&matcher)?; Ok(()) } /// With `file: canonical`, symlinked source paths are written as the resolved /// real path (symlinks followed). // Requirements: output-path-format #[test] #[cfg(target_family = "unix")] #[cfg(has_preload_library)] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn canonical_path_format_resolves_symlinks() -> Result<()> { use std::os::unix::fs::symlink; let env = TestEnvironment::new("canonical_symlinks")?; // Real source under real/; src/ is a symlink pointing at real/. Compiling // via src/main.c records "src/main.c" in the event, which canonical must // resolve back to .../real/main.c. env.create_source_files(&[("real/main.c", "int main() { return 0; }")])?; symlink(env.test_dir().join("real"), env.test_dir().join("src"))?; let build = format!("{} -c src/main.c -o src/main.o", COMPILER_C_PATH); let script = env.create_shell_script("build.sh", &build)?; let config = format!( r#" schema: "4.2" intercept: mode: preload path: "{preload}" format: paths: directory: canonical file: canonical "#, preload = PRELOAD_LIBRARY_PATH ); let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; env.run_bear_success(&[ "--output", "compile_commands.json", "--config", config_path.to_str().unwrap(), "--", SHELL_PATH, script.to_str().unwrap(), ])?; let db = env.load_compilation_database("compile_commands.json")?; let file = db .entries() .first() .and_then(|e| e.get("file").and_then(|v| v.as_str())) .expect("expected at least one entry") .to_string(); assert!( file.ends_with("/real/main.c"), "canonical file field must resolve symlinks; got {file}, expected path ending with /real/main.c" ); assert!( !file.contains("/src/"), "canonical file field must not contain the symlink segment /src/; got {file}" ); Ok(()) } /// With `directory: absolute` and `file: relative`, an absolute source path /// observed at interception is rewritten relative to the (formatted) directory. // Requirements: output-path-format #[test] #[cfg(target_family = "unix")] #[cfg(has_preload_library)] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn relative_file_format_is_relative_to_directory() -> Result<()> { let env = TestEnvironment::new("relative_file_format")?; env.create_source_files(&[("src/main.c", "int main() { return 0; }")])?; // Compile using an absolute source path so the intercepted event records // the absolute form; `file: relative` must then rewrite it to src/main.c. let build = format!(r#"{} -c "$(pwd)/src/main.c" -o src/main.o"#, COMPILER_C_PATH); let script = env.create_shell_script("build.sh", &build)?; let config = format!( r#" schema: "4.2" intercept: mode: preload path: "{preload}" format: paths: directory: absolute file: relative "#, preload = PRELOAD_LIBRARY_PATH ); let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; env.run_bear_success(&[ "--output", "compile_commands.json", "--config", config_path.to_str().unwrap(), "--", SHELL_PATH, script.to_str().unwrap(), ])?; let db = env.load_compilation_database("compile_commands.json")?; let entry = db.entries().first().expect("expected at least one entry"); let file = entry.get("file").and_then(|v| v.as_str()).unwrap_or(""); let directory = entry.get("directory").and_then(|v| v.as_str()).unwrap_or(""); assert_eq!(file, "src/main.c", "file must be relative to directory, got {file:?}"); assert!(std::path::Path::new(directory).is_absolute(), "directory must be absolute, got {directory:?}"); Ok(()) } /// With the output field enabled and `file: relative`, the `output` field is /// formatted with the same resolver as `file`: an absolute `-o` path observed /// at interception is rewritten relative to the directory. Exercised via the /// `semantic` subcommand with a hand-crafted event so the observed paths are /// exactly known. // Requirements: output-path-format #[test] #[cfg(target_family = "unix")] fn output_field_follows_file_path_format() -> Result<()> { use serde_json::json; let env = TestEnvironment::new("output_field_path_format")?; let temp_dir = env.test_dir().to_str().unwrap().to_string(); let abs_source = format!("{temp_dir}/src/main.c"); let abs_output = format!("{temp_dir}/build/main.o"); let event = json!({ "executable": "gcc", "arguments": ["gcc", "-c", abs_source, "-o", abs_output], "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[ ("events.json", &event.to_string()), ("src/main.c", "int main() { return 0; }"), ])?; let config = r#" schema: "4.2" format: paths: directory: absolute file: relative entries: use_array_format: true include_output_field: true "#; let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; env.run_bear_success(&[ "--config", config_path.to_str().unwrap(), "semantic", "--input", "events.json", "--output", "compile_commands.json", ])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; let entry = db.entries().first().expect("expected one entry"); assert_eq!( entry.get("file").and_then(|v| v.as_str()), Some("src/main.c"), "file must be rewritten relative to the directory" ); assert_eq!( entry.get("output").and_then(|v| v.as_str()), Some("build/main.o"), "output must be formatted like file (relative to the directory)" ); Ok(()) } /// With `file: canonical` and a source that does not exist at output-write /// time, Bear must not drop the entry: it falls back to the unformatted path /// (and logs a warning). Exercised via the `semantic` subcommand so we can /// feed a hand-crafted event referencing a ghost source. // Requirements: output-path-format #[test] #[cfg(target_family = "unix")] fn canonical_file_format_falls_back_for_missing_source() -> Result<()> { use serde_json::json; let env = TestEnvironment::new("canonical_missing_fallback")?; // Working dir exists (so directory canonicalization succeeds and the entry // is kept); the source file does not exist, so file canonicalization must // fall back to the unformatted path. let temp_dir = env.test_dir().to_str().unwrap().to_string(); let event = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "ghost.c"], "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[("events.json", &event.to_string())])?; let config = r#" schema: "4.2" format: paths: directory: canonical file: canonical "#; let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; env.run_bear_success(&[ "--config", config_path.to_str().unwrap(), "semantic", "--input", "events.json", "--output", "compile_commands.json", ])?; let db = env.load_compilation_database("compile_commands.json")?; let file = db .entries() .first() .and_then(|e| e.get("file").and_then(|v| v.as_str())) .expect("entry must survive the canonical-file fallback"); assert_eq!(file, "ghost.c", "file field must fall back to the unformatted path; got {file:?}"); Ok(()) } /// Reproduces GitHub issue #692: with `directory: relative`, the working /// directory resolves relative to itself and produces an empty path, which /// fails entry validation and aborts the output pipeline. The symptom the /// reporter saw was repeated "sending on a disconnected channel" errors and /// no compilation database being written. // Requirements: output-path-format #[test] #[cfg(target_family = "unix")] fn relative_directory_format_does_not_break_pipeline() -> Result<()> { use serde_json::json; let env = TestEnvironment::new("relative_directory_format")?; let temp_dir = env.test_dir().to_str().unwrap().to_string(); let event = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "main.c"], "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[("events.json", &event.to_string()), ("main.c", "int main() { return 0; }")])?; let config = r#" schema: "4.2" format: paths: directory: relative file: relative "#; let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; env.run_bear_success(&[ "--config", config_path.to_str().unwrap(), "semantic", "--input", "events.json", "--output", "compile_commands.json", ])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; let entry = db.entries().first().expect("expected at least one entry"); let directory = entry.get("directory").and_then(|v| v.as_str()).unwrap_or(""); assert!(!directory.is_empty(), "directory field must not be empty; got {directory:?}"); Ok(()) } /// Companion to `relative_directory_format_does_not_break_pipeline`: with /// only `directory: relative` (and `file: as-is`) the output pipeline /// succeeds too. Isolates the regression guard to the `directory` axis -- /// when issue #692 was open, this config reproduced the bug independently /// of the `file` setting. // Requirements: output-path-format #[test] #[cfg(target_family = "unix")] fn relative_directory_alone_does_not_break_pipeline() -> Result<()> { use serde_json::json; let env = TestEnvironment::new("relative_directory_alone")?; let temp_dir = env.test_dir().to_str().unwrap().to_string(); let event = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "main.c"], "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[("events.json", &event.to_string()), ("main.c", "int main() { return 0; }")])?; let config = r#" schema: "4.2" format: paths: directory: relative file: as-is "#; let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; env.run_bear_success(&[ "--config", config_path.to_str().unwrap(), "semantic", "--input", "events.json", "--output", "compile_commands.json", ])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; let entry = db.entries().first().expect("expected at least one entry"); let directory = entry.get("directory").and_then(|v| v.as_str()).unwrap_or(""); assert!(!directory.is_empty(), "directory field must not be empty; got {directory:?}"); Ok(()) } /// A single invalid entry must not prevent valid ones from being written. /// Feeds two events: one with an empty working_dir (produces an entry with /// an empty directory field, which fails validation) and one with a valid /// working_dir. Expects the valid entry to survive, exit code 0, and a /// per-drop WARN log line. // Requirements: output-json-compilation-database #[test] #[cfg(target_family = "unix")] fn invalid_entry_is_dropped_with_warning_not_fatal() -> Result<()> { use serde_json::json; let env = TestEnvironment::new("invalid_entry_drop")?; let valid_dir = env.test_dir().to_str().unwrap().to_string(); let event_invalid = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "bogus.c"], "working_dir": "", "environment": {} }); let event_valid = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "main.c"], "working_dir": valid_dir, "environment": {} }); let events_content = format!("{}\n{}", event_invalid, event_valid); env.create_source_files(&[("events.json", &events_content), ("main.c", "int main() { return 0; }")])?; let output = env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; // Valid entry survives; invalid one is dropped. let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; db.assert_contains(&CompilationEntryMatcher::new().file("main.c"))?; // The drop is visible in logs -- both per-entry WARN and the pipeline // summary counter. let stderr = output.stderr(); assert!( stderr.contains("Dropping invalid compilation database entry"), "expected WARN log for dropped entry in stderr; got:\n{stderr}" ); assert!( stderr.contains("dropped entries (invalid): 1"), "expected pipeline summary to report 1 dropped entry; got:\n{stderr}" ); Ok(()) } /// When every candidate entry fails validation, the pipeline still exits 0 /// and produces an empty compilation database, but emits a single /// ERROR-level summary line so the empty result is not silent. // Requirements: output-json-compilation-database #[test] #[cfg(target_family = "unix")] fn all_entries_dropped_emits_error_summary() -> Result<()> { use serde_json::json; let env = TestEnvironment::new("all_entries_dropped")?; let event = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "main.c"], "working_dir": "", "environment": {} }); env.create_source_files(&[("events.json", &event.to_string())])?; let output = env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; // Empty DB is still valid JSON. let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(0)?; let stderr = output.stderr(); assert!( stderr.contains("Compilation database is empty"), "expected ERROR summary line when every entry is dropped; got:\n{stderr}" ); Ok(()) } #[cfg(target_family = "unix")] fn canonical_directory_config(env: &TestEnvironment) -> Result<std::path::PathBuf> { let config = r#" schema: "4.2" format: paths: directory: canonical file: as-is "#; let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; Ok(config_path) } /// With `directory: canonical`, an entry whose working directory no longer /// exists is dropped under the validation-drop contract: a WARN names the /// file, and the run summary counts the drop. Other entries survive. // Requirements: output-path-format, output-json-compilation-database #[test] #[cfg(target_family = "unix")] fn canonical_directory_failure_drops_entry_like_validation() -> Result<()> { use serde_json::json; let env = TestEnvironment::new("canonical_directory_drop")?; let valid_dir = env.test_dir().to_str().unwrap().to_string(); let missing_dir = env.test_dir().join("removed").to_str().unwrap().to_string(); let event_missing = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "orphan.c"], "working_dir": missing_dir, "environment": {} }); let event_valid = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "main.c"], "working_dir": valid_dir, "environment": {} }); let events_content = format!("{}\n{}", event_missing, event_valid); env.create_source_files(&[("events.json", &events_content), ("main.c", "int main() { return 0; }")])?; let config_path = canonical_directory_config(&env)?; let output = env.run_bear_success(&[ "--config", config_path.to_str().unwrap(), "semantic", "--input", "events.json", "--output", "compile_commands.json", ])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; db.assert_contains(&CompilationEntryMatcher::new().file("main.c"))?; let stderr = output.stderr(); let warning = stderr .lines() .find(|line| line.contains("WARN") && line.contains("orphan.c")) .unwrap_or_else(|| panic!("expected a WARN naming orphan.c; got:\n{stderr}")); assert_eq!(warning.matches("orphan.c").count(), 1, "warning must name the file once; got: {warning}"); assert!( stderr.contains("dropped entries (invalid): 1"), "expected pipeline summary to report 1 dropped entry; got:\n{stderr}" ); Ok(()) } /// With `directory: canonical`, when every entry is dropped because its /// working directory cannot be resolved, the all-dropped ERROR fires. // Requirements: output-path-format, output-json-compilation-database #[test] #[cfg(target_family = "unix")] fn canonical_directory_failure_for_all_entries_emits_error_summary() -> Result<()> { use serde_json::json; let env = TestEnvironment::new("canonical_directory_all_dropped")?; let missing_dir = env.test_dir().join("removed").to_str().unwrap().to_string(); let event = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "orphan.c"], "working_dir": missing_dir, "environment": {} }); env.create_source_files(&[("events.json", &event.to_string())])?; let config_path = canonical_directory_config(&env)?; let output = env.run_bear_success(&[ "--config", config_path.to_str().unwrap(), "semantic", "--input", "events.json", "--output", "compile_commands.json", ])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(0)?; let stderr = output.stderr(); assert!( stderr.contains("Compilation database is empty"), "expected ERROR summary line when every entry is dropped; got:\n{stderr}" ); Ok(()) } /// Test invalid configuration handling /// Verifies Bear handles invalid config gracefully #[test] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn invalid_config_handling() -> Result<()> { let env = TestEnvironment::new("invalid_config")?; env.create_source_files(&[("test.c", "int main() { return 0; }")])?; // Create invalid YAML config let invalid_config = "{ invalid yaml content }"; let config_path = env.test_dir().join("invalid_config.yaml"); std::fs::write(&config_path, invalid_config)?; let build_commands = format!("{} -c test.c", COMPILER_C_PATH); let script_path = env.create_shell_script("build.sh", &build_commands)?; // Bear should handle invalid config gracefully (likely with error) let output = env.run_bear(&[ "--config", config_path.to_str().unwrap(), "--", SHELL_PATH, script_path.to_str().unwrap(), ])?; // Should fail with non-zero exit code assert!(output.exit_code() != Some(0)); Ok(()) } /// Test unsupported schema version /// Verifies Bear rejects unsupported schema versions #[test] #[cfg(has_preload_library)] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn unsupported_schema_version() -> Result<()> { let env = TestEnvironment::new("unsupported_schema")?; env.create_source_files(&[("test.c", "int main() { return 0; }")])?; // Create config with unsupported schema version let config = format!( r#" schema: "3.0" intercept: mode: preload path: "{}" sources: only_existing_files: true "#, PRELOAD_LIBRARY_PATH ); let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; let build_commands = format!("{} -c test.c", COMPILER_C_PATH); let script_path = env.create_shell_script("build.sh", &build_commands)?; // Bear should reject unsupported schema version let output = env.run_bear(&[ "--config", config_path.to_str().unwrap(), "--", SHELL_PATH, script_path.to_str().unwrap(), ])?; // Should fail with non-zero exit code and mention schema assert!(output.exit_code() != Some(0)); // Error message should mention schema version issue assert!(output.stderr().contains("schema")); Ok(()) } /// Test duplicate filter configuration /// Verifies duplicate filtering options work // Requirements: output-duplicate-detection #[test] #[cfg(has_preload_library)] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn duplicate_filter_config() -> Result<()> { let env = TestEnvironment::new("duplicate_filter")?; env.create_source_files(&[("test.c", "int main() { return 0; }")])?; // Build script that might generate duplicate entries let build_commands = format!("{} -c test.c\n{} -c test.c", COMPILER_C_PATH, COMPILER_C_PATH); let script_path = env.create_shell_script("build.sh", &build_commands)?; // Config with duplicate filtering let config = format!( r#" schema: "4.2" intercept: mode: preload path: "{}" duplicates: match_on: ["file", "directory"] sources: only_existing_files: true "#, PRELOAD_LIBRARY_PATH ); let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; let _output = env.run_bear_success(&[ "--output", "compile_commands.json", "--config", config_path.to_str().unwrap(), "--", "sh", script_path.to_str().unwrap(), ])?; // Verify duplicate filtering worked let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; Ok(()) } /// Accepted entries appear in the output in the order they were received: /// three sources compiled as a.c, b.c, c.c (with a duplicate of a.c in /// between, dropped by the default filter) come out in exactly that order. /// The bare `gcc` executable with an empty environment keeps the events /// host-independent. // Requirements: output-duplicate-detection #[test] fn duplicate_filter_preserves_input_order() -> Result<()> { use serde_json::json; let env = TestEnvironment::new("dup_output_order")?; let temp_dir = env.test_dir().to_str().unwrap().to_string(); let mut events: Vec<String> = ["a.c", "b.c", "c.c"] .iter() .map(|source| { json!({ "executable": "gcc", "arguments": ["gcc", "-c", source], "working_dir": temp_dir, "environment": {} }) .to_string() }) .collect(); // A duplicate of a.c between b.c and c.c: it must be dropped without // disturbing the order of the survivors. events.insert(2, events[0].clone()); let events_content = events.join("\n") + "\n"; env.create_source_files(&[("events.json", &events_content)])?; env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(3)?; let files: Vec<&str> = db.entries().iter().filter_map(|entry| entry.get("file").and_then(|v| v.as_str())).collect(); assert_eq!(files, ["a.c", "b.c", "c.c"], "entries must appear in the order they were received"); Ok(()) } /// With `match_on: [file]`, two entries for the same file with different flags /// collapse to one (the first). // Requirements: output-duplicate-detection #[test] #[cfg(target_family = "unix")] fn duplicate_match_on_file_alone_collapses_flag_variants() -> Result<()> { use serde_json::json; let env = TestEnvironment::new("dup_match_file")?; let temp_dir = env.test_dir().to_str().unwrap().to_string(); let event1 = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "-O2", "test.c"], "working_dir": temp_dir, "environment": {} }); let event2 = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "-O3", "test.c"], "working_dir": temp_dir, "environment": {} }); let events_content = format!("{}\n{}", event1, event2); env.create_source_files(&[("events.json", &events_content), ("test.c", "int main() { return 0; }")])?; let config = r#" schema: "4.2" duplicates: match_on: [file] "#; let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; env.run_bear_success(&[ "--config", config_path.to_str().unwrap(), "semantic", "--input", "events.json", "--output", "compile_commands.json", ])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; // First-occurrence wins: -O2 is kept, -O3 is dropped. let entry = db.entries().first().expect("one entry expected"); let args: Vec<String> = entry .get("arguments") .and_then(|v| v.as_array()) .unwrap() .iter() .filter_map(|v| v.as_str()) .map(String::from) .collect(); assert!(args.iter().any(|a| a == "-O2"), "first-occurrence entry must carry -O2, got {:?}", args); assert!(!args.iter().any(|a| a == "-O3"), "second-occurrence -O3 entry must be dropped, got {:?}", args); Ok(()) } /// With `match_on: [file, output]`, the same source compiled to different /// output paths yields two entries (different output means not a duplicate). // Requirements: output-duplicate-detection #[test] #[cfg(target_family = "unix")] fn duplicate_match_on_file_and_output_preserves_differing_outputs() -> Result<()> { use serde_json::json; let env = TestEnvironment::new("dup_match_file_output")?; let temp_dir = env.test_dir().to_str().unwrap().to_string(); let event1 = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "test.c", "-o", "debug/test.o"], "working_dir": temp_dir, "environment": {} }); let event2 = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "test.c", "-o", "release/test.o"], "working_dir": temp_dir, "environment": {} }); let events_content = format!("{}\n{}", event1, event2); env.create_source_files(&[("events.json", &events_content), ("test.c", "int main() { return 0; }")])?; let config = r#" schema: "4.2" duplicates: match_on: [file, output] format: entries: use_array_format: true include_output_field: true "#; let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; env.run_bear_success(&[ "--config", config_path.to_str().unwrap(), "semantic", "--input", "events.json", "--output", "compile_commands.json", ])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(2)?; Ok(()) } /// Configuration validation must reject `match_on` that contains both /// `command` and `arguments` (they are alternative representations of the /// same data). // Requirements: output-duplicate-detection #[test] fn duplicate_match_on_command_and_arguments_is_rejected() -> Result<()> { let env = TestEnvironment::new("dup_match_conflict")?; let config = r#" schema: "4.2" duplicates: match_on: [command, arguments] "#; let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; let output = env.run_bear(&[ "--config", config_path.to_str().unwrap(), "semantic", "--input", "events.json", "--output", "compile_commands.json", ])?; assert!(output.exit_code() != Some(0), "config with conflicting match_on must be rejected"); Ok(()) } /// Configuration validation must reject an empty `match_on` list. // Requirements: output-duplicate-detection #[test] fn duplicate_match_on_empty_is_rejected() -> Result<()> { let env = TestEnvironment::new("dup_match_empty")?; let config = r#" schema: "4.2" duplicates: match_on: [] "#; let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; let output = env.run_bear(&[ "--config", config_path.to_str().unwrap(), "semantic", "--input", "events.json", "--output", "compile_commands.json", ])?; assert!(output.exit_code() != Some(0), "config with empty match_on must be rejected"); Ok(()) } /// In append mode, a new entry that matches an existing one on the configured /// fields wins over the entry from the existing compilation database: the new /// arguments survive, the stale ones are dropped. New entries are emitted /// before existing ones, so first-occurrence filtering keeps the new entry. // Requirements: output-duplicate-detection #[test] #[cfg(target_family = "unix")] fn duplicate_append_mode_newest_entry_wins() -> Result<()> { use serde_json::json; let env = TestEnvironment::new("dup_append_priority")?; let temp_dir = env.test_dir().to_str().unwrap().to_string(); // First run builds the existing database with -O2. let first_event = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "-O2", "test.c"], "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[ ("events1.json", &first_event.to_string()), ("test.c", "int main() { return 0; }"), ])?; // match_on deliberately excludes arguments: same file+directory = duplicate. let config = r#" schema: "4.2" duplicates: match_on: [file, directory] "#; let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; env.run_bear_success(&[ "--config", config_path.to_str().unwrap(), "semantic", "--input", "events1.json", "--output", "compile_commands.json", ])?; // Second run adds a new entry for the same file with -O3. The append-mode // guarantee: the new -O3 entry wins and replaces the stale -O2 entry. let second_event = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "-O3", "test.c"], "working_dir": temp_dir, "environment": {} }); std::fs::write(env.test_dir().join("events2.json"), second_event.to_string())?; env.run_bear_success(&[ "--config", config_path.to_str().unwrap(), "semantic", "--append", "--input", "events2.json", "--output", "compile_commands.json", ])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; let args: Vec<String> = db .entries() .first() .and_then(|e| e.get("arguments")) .and_then(|v| v.as_array()) .unwrap() .iter() .filter_map(|v| v.as_str()) .map(String::from) .collect(); assert!(args.iter().any(|a| a == "-O3"), "new -O3 entry must win, got {:?}", args); assert!(!args.iter().any(|a| a == "-O2"), "stale -O2 entry must be dropped, got {:?}", args); Ok(()) } /// The scenario from GitHub discussion #712, exercised through the DEFAULT /// duplicate config (no `duplicates:` section): an `--append` run that /// recompiles a file with different flags must replace the stale entry rather /// than accumulate a second one. This depends on the default `match_on` being /// `[directory, file]` (arguments excluded) together with append emitting new /// entries first, so it would fail under the old `[directory, file, arguments]` /// default (which kept both entries). // Requirements: output-duplicate-detection #[test] #[cfg(target_family = "unix")] fn default_config_append_replaces_stale_entry_on_flag_change() -> Result<()> { use serde_json::json; // arrange: an existing database built with -O2, and a new build with -O3. let env = TestEnvironment::new("dup_append_default")?; let temp_dir = env.test_dir().to_str().unwrap().to_string(); let first_event = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "-O2", "test.c"], "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[ ("events1.json", &first_event.to_string()), ("test.c", "int main() { return 0; }"), ])?; // No --config: this run uses the default duplicate config. env.run_bear_success(&["semantic", "--input", "events1.json", "--output", "compile_commands.json"])?; let second_event = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "-O3", "test.c"], "working_dir": temp_dir, "environment": {} }); std::fs::write(env.test_dir().join("events2.json"), second_event.to_string())?; // act: append the -O3 build to the existing -O2 database, default config. env.run_bear_success(&[ "semantic", "--append", "--input", "events2.json", "--output", "compile_commands.json", ])?; // assert: one entry, recording the newest (-O3) flags. let sut = env.load_compilation_database("compile_commands.json")?; sut.assert_count(1)?; let args: Vec<String> = sut .entries() .first() .and_then(|e| e.get("arguments")) .and_then(|v| v.as_array()) .unwrap() .iter() .filter_map(|v| v.as_str()) .map(String::from) .collect(); assert!(args.iter().any(|a| a == "-O3"), "new -O3 entry must win, got {:?}", args); assert!(!args.iter().any(|a| a == "-O2"), "stale -O2 entry must be dropped, got {:?}", args); Ok(()) } /// The `compilers:` override must reach invocations that spell only the /// compiler's name: the config names a concrete path (validation requires /// it to exist), the build writes plain `my-quirky-cc`, and the entry is /// produced with the recorded compiler kept as observed. The name is /// outside the ambiguous set and matches no recognition regex, so the run /// WITHOUT the config proves nothing else classifies it -- the entry in the /// second run can only come from the filename-matched hint. (The hint-vs- /// probe interplay for the ambiguous `cc` itself is pinned hermetically by /// the recognizer's unit tests.) // Requirements: recognition-ambiguous-name-probe #[test] fn config_compiler_hint_matches_bare_spelled_name() -> Result<()> { use serde_json::json; let env = TestEnvironment::new("config_hint_bare_name")?; let temp_dir = env.test_dir().to_str().unwrap().to_string(); env.create_source_files(&[("toolchain/my-quirky-cc", ""), ("hello.c", "int main(void) { return 0; }")])?; let event = json!({ "executable": "my-quirky-cc", "arguments": ["my-quirky-cc", "-c", "hello.c"], "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[("events.json", &event.to_string())])?; // YAML single quotes: a double-quoted scalar treats the backslashes of a // Windows temp path as escape sequences and fails to parse. let config = format!( r#" schema: "4.2" compilers: - path: '{dir}/toolchain/my-quirky-cc' as: gcc "#, dir = temp_dir ); let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; // Without the config nothing recognizes the name; this is what makes the // second phase conclusive. env.run_bear_success(&["semantic", "--input", "events.json", "--output", "without_config.json"])?; env.load_compilation_database("without_config.json")?.assert_count(0)?; env.run_bear_success(&[ "--config", config_path.to_str().unwrap(), "semantic", "--input", "events.json", "--output", "compile_commands.json", ])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; db.assert_contains(&compilation_entry!( file: "hello.c".to_string(), directory: temp_dir.clone(), arguments: vec!["my-quirky-cc".to_string(), "-c".to_string(), "hello.c".to_string()] ))?; Ok(()) } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/tests/cases/diagnostics.rs����������������������������������0000664�0000000�0000000�00000017574�15271777203�0025433�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: GPL-3.0-or-later //! User-facing error messages. //! //! Each case runs Bear without opting into logging and checks the one //! diagnostic line it prints. The line is matched exactly up to the cause, //! which is the operating system's text and varies by platform. use crate::fixtures::infrastructure::TestEnvironment; use anyhow::{Context, Result}; /// How a case prepares the test directory before Bear runs. type Setup = fn(&TestEnvironment) -> Result<()>; fn nothing(_: &TestEnvironment) -> Result<()> { Ok(()) } fn output_is_a_directory(env: &TestEnvironment) -> Result<()> { std::fs::create_dir(env.test_dir().join("out"))?; Ok(()) } fn invalid_existing_database(env: &TestEnvironment) -> Result<()> { env.create_source_files(&[("compile_commands.json", "this is not json")]) } fn broken_configuration(env: &TestEnvironment) -> Result<()> { env.create_source_files(&[("broken.yml", "schema: \"4.2\"\nbogus: [\n")]) } /// Runs Bear and returns its standard error lines, checking the exit status. fn run(env: &TestEnvironment, args: &[&str], stdin: &[u8], succeeds: bool) -> Result<Vec<String>> { let result = env.run_bear_with_stdin_default_log(args, stdin)?; if succeeds { result.assert_success()?; } else { result.assert_failure()?; } Ok(result.stderr().lines().map(str::to_string).collect()) } /// Asserts that `lines` is a single diagnostic that starts with `expected` /// and carries a cause after it. fn assert_one_diagnostic(case: &str, lines: &[String], expected: &str) { assert_eq!(lines.len(), 1, "{case}: expected one diagnostic line, got {lines:?}"); let line = &lines[0]; assert!(line.starts_with(expected), "{case}: expected {expected:?}..., got {line:?}"); assert!(line.len() > expected.len(), "{case}: no cause after {expected:?} in {line:?}"); } // Requirements: cli-diagnostic-format #[test] fn errors_name_the_file_and_the_cause() -> Result<()> { let cases: [(&str, Setup, &[&str], &str); 8] = [ ( "output directory is missing", nothing, &["--output", "missing/db.json", "--", "nosuchcmd"], "bear: error: cannot create missing/db.json: ", ), ( "output is a directory", output_is_a_directory, &["--output", "out", "--", "nosuchcmd"], "bear: error: cannot create out: ", ), ( "events output directory is missing", nothing, &["intercept", "--output", "missing/events.json", "--", "nosuchcmd"], "bear: error: cannot create missing/events.json: ", ), ( "events input is missing", nothing, &["semantic", "--input", "missing.json", "--output", "db.json"], "bear: error: cannot read missing.json: ", ), ( "shell text input is missing", nothing, &["parse-sh", "--input", "missing.sh", "--output", "db.json"], "bear: error: cannot read missing.sh: ", ), ( "configuration is missing", nothing, &["--config", "missing.yml", "--", "nosuchcmd"], "bear: error: cannot read missing.yml: ", ), ( "configuration cannot be parsed", broken_configuration, &["--config", "broken.yml", "--", "nosuchcmd"], "bear: error: cannot parse broken.yml: ", ), ("build cannot start", nothing, &["--", "nosuchcmd"], "bear: error: cannot run 'nosuchcmd': "), ]; for (case, setup, args, expected) in cases { let env = TestEnvironment::new("diagnostics_errors")?; setup(&env)?; let sut = run(&env, args, b"", false)?; assert_one_diagnostic(case, &sut, expected); } Ok(()) } // Requirements: cli-diagnostic-format #[test] #[cfg(unix)] fn error_when_the_output_cannot_be_written() -> Result<()> { // A device that fails every write; not every unix has one. if !std::path::Path::new("/dev/full").exists() { return Ok(()); } let env = TestEnvironment::new("diagnostics_write_error")?; let sut = run(&env, &["parse-sh", "--output", "/dev/full"], b"gcc -c a.c\n", false)?; assert_one_diagnostic("disk full", &sut, "bear: error: cannot write /dev/full: "); Ok(()) } // Requirements: cli-diagnostic-format, output-append #[test] fn warning_when_previous_entries_cannot_be_read() -> Result<()> { let env = TestEnvironment::new("diagnostics_append_warning")?; invalid_existing_database(&env)?; let sut = run(&env, &["parse-sh", "--append", "--output", "compile_commands.json"], b"gcc -c a.c\n", true)?; assert_one_diagnostic( "invalid previous database", &sut, "bear: warning: cannot read previous entries from compile_commands.json: ", ); env.load_compilation_database("compile_commands.json")?.assert_count(1)?; Ok(()) } // Requirements: cli-diagnostic-format, output-append #[test] fn warning_when_previous_entries_are_empty_or_truncated() -> Result<()> { use std::io::{Read, Write}; use std::process::{Command, Stdio}; use std::time::{Duration, Instant}; let one_entry = r#"[ {"directory": "/", "file": "/b.c", "arguments": ["gcc", "-c", "/b.c"]} ,"#; for (content, count) in [("", 1), ("[", 1), (one_entry, 2)] { let env = TestEnvironment::new("diagnostics_append_truncated")?; env.create_source_files(&[("compile_commands.json", content)])?; // Spawned with a deadline: the reader used to loop on EOF forever. let mut child = Command::new(env.bear_path()) .current_dir(env.test_dir()) .env("RUST_BACKTRACE", "1") .env_remove("RUST_LOG") .args(["parse-sh", "--append", "--output", "compile_commands.json"]) .stdin(Stdio::piped()) .stdout(Stdio::null()) .stderr(Stdio::piped()) .spawn()?; child.stdin.take().context("stdin is piped")?.write_all(b"gcc -c a.c\n")?; let deadline = Instant::now() + Duration::from_secs(10); let status = loop { if let Some(status) = child.try_wait()? { break status; } if Instant::now() >= deadline { let _ = child.kill(); let _ = child.wait(); anyhow::bail!("{content:?}: bear did not exit within 10s"); } std::thread::sleep(Duration::from_millis(20)); }; let mut stderr = String::new(); child.stderr.take().context("stderr is piped")?.read_to_string(&mut stderr)?; let sut: Vec<String> = stderr.lines().map(str::to_string).collect(); assert!(status.success(), "{content:?}: bear failed: {stderr}"); assert_one_diagnostic( &format!("previous database {content:?}"), &sut, "bear: warning: cannot read previous entries from compile_commands.json: ", ); env.load_compilation_database("compile_commands.json")?.assert_count(count)?; } Ok(()) } // Requirements: cli-diagnostic-format, output-append #[test] #[cfg(unix)] fn error_when_previous_entries_cannot_be_opened() -> Result<()> { use std::os::unix::fs::PermissionsExt; let env = TestEnvironment::new("diagnostics_append_unreadable")?; invalid_existing_database(&env)?; let database = env.test_dir().join("compile_commands.json"); std::fs::set_permissions(&database, std::fs::Permissions::from_mode(0o000))?; // Permissions do not stop a privileged user (root), so there is nothing to test. if std::fs::File::open(&database).is_ok() { return Ok(()); } let sut = run(&env, &["--append", "--output", "compile_commands.json", "--", "nosuchcmd"], b"", false)?; assert_one_diagnostic( "unreadable previous database", &sut, "bear: error: cannot read compile_commands.json: ", ); Ok(()) } ������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/tests/cases/environment_arguments.rs������������������������0000664�0000000�0000000�00000005710�15271777203�0027542�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: GPL-3.0-or-later //! Tests for folding compiler environment variables into entry arguments. //! //! Exercise `format.arguments.from_environment` end to end: a build that sets //! `CPATH`, analysed with the option enabled (the default) and disabled. //! //! The whole module is gated (at its declaration in `cases/mod.rs`) on a //! preload library plus a C compiler and shell, since every test needs all //! three. use crate::fixtures::constants::*; use crate::fixtures::infrastructure::*; use anyhow::Result; const SRC: &str = "int main() { return 0; }"; /// Config that toggles environment-derived flags. `from_environment` is left at /// its default (`true`) when `enabled` is `None`. fn config(enabled: Option<bool>) -> String { let arguments = match enabled { Some(value) => format!(" arguments:\n from_environment: {value}\n"), None => String::new(), }; format!( r#" schema: "4.2" intercept: mode: preload path: "{}" format: paths: directory: as-is file: as-is {}"#, PRELOAD_LIBRARY_PATH, arguments ) } /// Runs a `cc -c src.c -o src.o` build with `CPATH=/opt/envinc` exported, under /// the given config, and returns the single entry's `arguments`. fn arguments_with_cpath(name: &str, config_yaml: &str) -> Result<Vec<String>> { let env = TestEnvironment::new(name)?; env.create_source_files(&[("src.c", SRC)])?; let build = format!("export CPATH=/opt/envinc\n{} -c src.c -o src.o", filename_of(COMPILER_C_PATH)); let script = env.create_shell_script("build.sh", &build)?; env.run_bear_success(&[ "--output", "compile_commands.json", "--config", env.create_config(config_yaml)?.to_str().unwrap(), "--", SHELL_PATH, script.to_str().unwrap(), ])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; let entry = db.entries()[0].clone(); let arguments = entry .get("arguments") .and_then(|v| v.as_array()) .map(|a| a.iter().filter_map(|v| v.as_str().map(str::to_string)).collect()) .unwrap_or_default(); Ok(arguments) } // Requirements: output-env-derived-flags #[test] fn environment_flags_included_by_default() -> Result<()> { let sut = arguments_with_cpath("env_args_default", &config(None))?; assert!( sut.windows(2).any(|w| w == ["-I", "/opt/envinc"]), "CPATH should contribute -I /opt/envinc by default, got {sut:?}" ); Ok(()) } // Requirements: output-env-derived-flags #[test] fn environment_flags_excluded_when_disabled() -> Result<()> { let sut = arguments_with_cpath("env_args_disabled", &config(Some(false)))?; let expected = vec![ COMPILER_C_PATH.to_string(), "-c".to_string(), "src.c".to_string(), "-o".to_string(), "src.o".to_string(), ]; assert_eq!(sut, expected, "disabled from_environment must drop CPATH-derived flags"); Ok(()) } ��������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/tests/cases/exit_codes.rs�����������������������������������0000664�0000000�0000000�00000100735�15271777203�0025242�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: GPL-3.0-or-later use crate::fixtures::constants::*; use crate::fixtures::infrastructure::TestEnvironment; #[cfg(all(target_family = "unix", has_executable_compiler_c, has_executable_shell))] use crate::fixtures::infrastructure::filename_of; use anyhow::Result; #[cfg(all(target_family = "unix", has_executable_shell, has_executable_sleep))] use std::io::Read; #[cfg(has_executable_sleep)] use std::process::Stdio; #[cfg(has_executable_sleep)] use std::time::Instant; // Requirements: cli-exit-codes #[test] fn exit_code_for_empty_arguments() -> Result<()> { // Executing Bear with no arguments should return a non-zero exit code, // and print usage information. let env = TestEnvironment::new("exit_code_for_empty_arguments")?; let result = env.run_bear(&[])?; result.assert_failure()?; assert!(result.stderr().contains("Usage: bear")); Ok(()) } // Requirements: cli-exit-codes #[test] fn exit_code_for_help() -> Result<()> { // Executing help and subcommand help should always has zero exit code, // and print out usage information let env = TestEnvironment::new("exit_code_for_help")?; // Test main help let result = env.run_bear(&["--help"])?; result.assert_success()?; assert!(result.stdout().contains("Usage: bear")); // Test intercept help let result = env.run_bear(&["intercept", "--help"])?; result.assert_success()?; assert!(result.stdout().contains("Usage: bear")); // Test semantic help let result = env.run_bear(&["semantic", "--help"])?; result.assert_success()?; assert!(result.stdout().contains("Usage: bear")); Ok(()) } // Requirements: cli-exit-codes #[test] fn exit_code_for_invalid_argument() -> Result<()> { // Executing Bear with an invalid argument should always has non-zero exit code, // and print relevant information about the reason about the failure. let env = TestEnvironment::new("exit_code_for_invalid_argument")?; let result = env.run_bear(&["invalid_argument"])?; result.assert_failure()?; assert!(result.stderr().contains("error: unexpected argument")); Ok(()) } // Requirements: cli-exit-codes #[test] fn exit_code_for_non_existing_command() -> Result<()> { // Executing a non-existing command should always has non-zero exit code, // and print relevant information about the reason about the failure. let env = TestEnvironment::new("exit_code_for_non_existing_command")?; let result = env.run_bear(&["--", "invalid_command"])?; result.assert_failure()?; assert!(result.stderr().contains("cannot run 'invalid_command': ")); Ok(()) } // Requirements: cli-exit-codes #[test] #[cfg(has_executable_true)] fn exit_code_for_true() -> Result<()> { // When the executed command returns successfully, Bear exit code should be zero. let env = TestEnvironment::new("exit_code_for_true")?; let result = env.run_bear(&["--", TRUE_PATH])?; result.assert_success()?; Ok(()) } // Requirements: cli-exit-codes #[test] #[cfg(has_executable_false)] fn exit_code_for_false() -> Result<()> { // When the executed command returns unsuccessfully, Bear exit code should be non-zero. let env = TestEnvironment::new("exit_code_for_false")?; let result = env.run_bear(&["--", FALSE_PATH])?; result.assert_failure()?; Ok(()) } /// The build's exit code is propagated byte-for-byte, not collapsed to a /// generic failure: a build that exits 42 makes Bear exit exactly 42, in /// both the combined and the intercept build-running modes. // Requirements: cli-exit-codes #[test] #[cfg(has_executable_shell)] fn exit_code_propagates_exact_build_code() -> Result<()> { let env = TestEnvironment::new("exit_code_exact_42")?; let result = env.run_bear(&["--output", "compile_commands.json", "--", SHELL_PATH, "-c", "exit 42"])?; assert_eq!(result.exit_code(), Some(42), "combined mode must propagate the build's exact exit code"); let result = env.run_bear(&["intercept", "--output", "events.json", "--", SHELL_PATH, "-c", "exit 42"])?; assert_eq!(result.exit_code(), Some(42), "intercept mode must propagate the build's exact exit code"); Ok(()) } /// A build-running mode whose every candidate entry is dropped at output /// validation still exits with the build's exit code: an `--append` run /// feeds the existing database's entries back through output validation, so /// a pre-seeded entry with an empty directory (modeled on the all-dropped /// semantic test in config.rs) is the sole candidate and is rejected -- yet /// the build succeeded, so Bear exits 0 and the database is written empty. /// Data-quality outcomes must never change the exit code. // Requirements: cli-exit-codes #[test] #[cfg(has_executable_true)] fn build_exit_code_is_preserved_when_every_entry_is_dropped_at_output() -> Result<()> { let env = TestEnvironment::new("exit_code_all_entries_dropped")?; // A well-formed database whose only entry fails validation (empty // directory field), so it is rejected when --append reads it back. let invalid_entry = r#"[{"directory": "", "file": "main.c", "arguments": ["gcc", "-c", "main.c"]}]"#; env.create_source_files(&[("compile_commands.json", invalid_entry)])?; let output = env.run_bear(&["--append", "--output", "compile_commands.json", "--", TRUE_PATH])?; // The build exited 0, so Bear must exit 0 despite the empty database. output.assert_success()?; // The candidate entry existed and was rejected at output validation. assert!( output.stderr().contains("Entry has an empty directory field"), "expected the validation rejection to be visible in stderr: {}", output.stderr() ); let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(0)?; Ok(()) } // Requirements: cli-signal-forwarding, cli-exit-codes #[test] #[cfg(has_executable_sleep)] fn exit_code_when_signaled() -> Result<()> { // When the bear process is signaled, Bear exit code should be non-zero. // And should terminate the child process and return immediately. let env = TestEnvironment::new("exit_code_when_signaled")?; let mut cmd = env.command_bear(); cmd.current_dir(env.test_dir()) .arg("--") .arg(SLEEP_PATH) .arg("10") .env("RUST_LOG", "debug") .env("RUST_BACKTRACE", "1") .stdout(Stdio::null()) .stderr(Stdio::null()); let mut child = cmd.spawn().expect("Failed to spawn command"); // Wait 200ms to ensure that the sleep command was also executed std::thread::sleep(std::time::Duration::from_millis(200)); let kill_time = Instant::now(); child.kill().expect("Failed to signal the process"); let status = child.wait().expect("Failed to wait for command"); let wait_end = Instant::now(); assert!(!status.success()); assert!(wait_end.duration_since(kill_time).as_secs() < 1, "Process took too long to terminate.",); Ok(()) } /// The combined mode (no subcommand) runs the build too, so it must reject /// `--output -` for the same reason `bear intercept --output -` does: the /// build's own stdout shares the stream and a non-atomic write could split /// a JSON line. // Requirements: output-json-compilation-database #[test] #[cfg(has_executable_true)] fn combined_output_dash_is_rejected() -> Result<()> { let env = TestEnvironment::new("combined_output_dash_is_rejected")?; let result = env.run_bear(&["--output", "-", "--", TRUE_PATH])?; result.assert_failure()?; assert!( result.stderr().contains("cannot write the compilation database to stdout"), "stderr did not explain the rejection: {}", result.stderr() ); assert!(!env.file_exists("-"), "must not create a file literally named `-`"); Ok(()) } // Intercept mode exit code tests /// Test that intercept command returns 0 for successful interception // Requirements: cli-exit-codes #[test] #[cfg(has_executable_true)] fn intercept_exit_code_for_success() -> Result<()> { let env = TestEnvironment::new("intercept_exit_code_for_success")?; let result = env.run_bear(&["intercept", "--output", "events.json", "--", TRUE_PATH])?; result.assert_success()?; Ok(()) } /// Interception has no default event-file name: omitting the output /// destination is a usage error, reported before the build runs. // Requirements: interception-events-format #[test] #[cfg(has_executable_true)] fn intercept_exit_code_without_output() -> Result<()> { let env = TestEnvironment::new("intercept_exit_code_without_output")?; let result = env.run_bear(&["intercept", "--", TRUE_PATH])?; result.assert_failure()?; assert!( result.stderr().contains("--output"), "the usage error must name the missing option: {}", result.stderr() ); Ok(()) } /// Test that intercept command propagates command failure exit codes // Requirements: cli-exit-codes #[test] #[cfg(has_executable_false)] fn intercept_exit_code_for_failure() -> Result<()> { let env = TestEnvironment::new("intercept_exit_code_for_failure")?; let result = env.run_bear(&["intercept", "--output", "events.json", "--", FALSE_PATH])?; result.assert_failure()?; Ok(()) } /// A compiler that is blocked reading from a FIFO with no writer is in the /// mid-compile state. Signaling Bear with SIGTERM must stop both Bear and /// the compiler quickly, with Bear reporting non-success. // Requirements: cli-signal-forwarding #[test] #[cfg(target_family = "unix")] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn exit_code_when_compiler_is_interrupted_mid_compile() -> Result<()> { let env = TestEnvironment::new("exit_code_mid_compile_signal")?; // Named pipe as the compiler's input. With no writer, the compiler // blocks in read() and stays mid-compile until a signal arrives. let fifo = env.test_dir().join("source.c"); let mkfifo_status = std::process::Command::new("mkfifo").arg(&fifo).status()?; assert!(mkfifo_status.success(), "mkfifo failed -- this test needs a POSIX environment"); let mut cmd = env.command_bear(); cmd.current_dir(env.test_dir()) .args([ "--output", "compile_commands.json", "--", COMPILER_C_PATH, "-x", "c", "-c", fifo.to_str().unwrap(), "-o", "out.o", ]) .stdout(Stdio::null()) .stderr(Stdio::null()); let mut child = cmd.spawn().expect("failed to spawn bear"); // Give the compiler time to start and block on opening/reading the FIFO. std::thread::sleep(std::time::Duration::from_millis(500)); // Send SIGTERM so Bear's signal forwarding path is exercised (unlike // Child::kill() which sends SIGKILL and bypasses handlers). let signal_time = Instant::now(); let pid = child.id().to_string(); let kill_status = std::process::Command::new("kill") .arg("-TERM") .arg(&pid) .status() .expect("kill -TERM command failed to run"); assert!(kill_status.success(), "kill -TERM reported failure"); let status = child.wait().expect("failed to wait for bear"); let elapsed = signal_time.elapsed(); assert!(!status.success(), "bear must report non-success after signal"); assert!( elapsed.as_secs() < 2, "bear must exit within ~1s of signal while the compiler was mid-compile, took {:?}", elapsed ); Ok(()) } /// A build whose process tree includes a detached grandchild must be torn /// down whole: signaling Bear stops not just the direct child but the /// grandchild the build spawned. This proves process-group teardown end to /// end through the real driver, not just the killpg mechanism in isolation. // Requirements: cli-signal-forwarding #[test] #[cfg(target_family = "unix")] #[cfg(all(has_executable_shell, has_executable_sleep))] fn signal_tears_down_build_process_tree() -> Result<()> { let env = TestEnvironment::new("signal_tears_down_build_tree")?; let gpid_file = env.test_dir().join("grandchild.pid"); // The build forks a long-lived grandchild, records its pid, then blocks. let script = format!("{sleep} 60 & echo $! > {pid} ; {sleep} 60", sleep = SLEEP_PATH, pid = gpid_file.display()); let mut cmd = env.command_bear(); cmd.current_dir(env.test_dir()) .args(["--output", "compile_commands.json", "--", SHELL_PATH, "-c", &script]) .stdout(Stdio::null()) .stderr(Stdio::null()); let mut child = cmd.spawn().expect("failed to spawn bear"); // Wait for the build to start and record the grandchild pid. let gpid = { let deadline = Instant::now() + std::time::Duration::from_secs(2); loop { if let Ok(text) = std::fs::read_to_string(&gpid_file) && let Ok(pid) = text.trim().parse::<i32>() { break pid; } assert!(Instant::now() < deadline, "build never recorded its grandchild pid"); std::thread::sleep(std::time::Duration::from_millis(20)); } }; assert!(is_alive(gpid), "grandchild should be running before the signal"); // Send SIGTERM to Bear (forwarding path, unlike Child::kill()/SIGKILL). let pid = child.id().to_string(); let kill_status = std::process::Command::new("kill") .arg("-TERM") .arg(&pid) .status() .expect("kill -TERM command failed to run"); assert!(kill_status.success(), "kill -TERM reported failure"); let status = child.wait().expect("failed to wait for bear"); assert!(!status.success(), "bear must report non-success after signal"); // The grandchild must be gone: process-group teardown reached it. let deadline = Instant::now() + std::time::Duration::from_secs(2); while is_alive(gpid) && Instant::now() < deadline { std::thread::sleep(std::time::Duration::from_millis(20)); } assert!(!is_alive(gpid), "grandchild survived -- only the direct child was reaped"); Ok(()) } /// A grandchild that calls `setsid` starts its own session and process /// group, leaving the build's process group entirely -- so `killpg` can no /// longer reach it. On Linux with a usable cgroup, Bear tears down the /// build's cgroup, which a child cannot leave unprivileged, so the detached /// grandchild dies too. Where no writable cgroup is available the documented /// process-group fallback applies and this test skips (the /// `signal_tears_down_build_process_tree` test covers that path). // Requirements: cli-signal-forwarding #[test] #[cfg(target_os = "linux")] #[cfg(all(has_executable_shell, has_executable_sleep, has_executable_setsid))] fn signal_tears_down_cgroup_even_when_grandchild_detaches() -> Result<()> { if !cgroup_v2_writable() { eprintln!("skipping: no writable cgroup v2 subtree; process-group fallback applies"); return Ok(()); } let env = TestEnvironment::new("signal_tears_down_cgroup")?; let gpid_file = env.test_dir().join("grandchild.pid"); // `setsid` runs the sleep in a brand-new session/group, so it escapes the // build's process group -- only a cgroup kill can still reach it. let script = format!( "{setsid} {sleep} 60 & echo $! > {pid} ; {sleep} 60", setsid = SETSID_PATH, sleep = SLEEP_PATH, pid = gpid_file.display(), ); let mut cmd = env.command_bear(); cmd.current_dir(env.test_dir()) .args(["--output", "compile_commands.json", "--", SHELL_PATH, "-c", &script]) .stdout(Stdio::null()) .stderr(Stdio::null()); let mut child = cmd.spawn().expect("failed to spawn bear"); // Wait for the build to start and record the detached grandchild pid. let gpid = { let deadline = Instant::now() + std::time::Duration::from_secs(2); loop { if let Ok(text) = std::fs::read_to_string(&gpid_file) && let Ok(pid) = text.trim().parse::<i32>() { break pid; } assert!(Instant::now() < deadline, "build never recorded its grandchild pid"); std::thread::sleep(std::time::Duration::from_millis(20)); } }; assert!(is_alive(gpid), "detached grandchild should be running before the signal"); // Send SIGTERM to Bear (forwarding path, unlike Child::kill()/SIGKILL). let pid = child.id().to_string(); let kill_status = std::process::Command::new("kill") .arg("-TERM") .arg(&pid) .status() .expect("kill -TERM command failed to run"); assert!(kill_status.success(), "kill -TERM reported failure"); let status = child.wait().expect("failed to wait for bear"); assert!(!status.success(), "bear must report non-success after signal"); // The detached grandchild must be gone: cgroup teardown reached it even // though it left the process group. let deadline = Instant::now() + std::time::Duration::from_secs(2); while is_alive(gpid) && Instant::now() < deadline { std::thread::sleep(std::time::Duration::from_millis(20)); } assert!(!is_alive(gpid), "detached grandchild survived -- cgroup teardown did not reach it"); Ok(()) } /// Whether a child cgroup can be created under our own cgroup v2 -- the /// runtime precondition for cgroup-based teardown. Mirrors what `supervise` /// attempts, so the test runs only where the cgroup path is actually taken. #[cfg(target_os = "linux")] fn cgroup_v2_writable() -> bool { let Ok(own) = std::fs::read_to_string("/proc/self/cgroup") else { return false; }; let Some(rel) = own.lines().find_map(|l| l.strip_prefix("0::")) else { return false; }; let dir = std::path::Path::new("/sys/fs/cgroup") .join(rel.trim_start_matches('/')) .join(format!("bear-test-probe-{}", std::process::id())); if std::fs::create_dir(&dir).is_err() { return false; } let ok = dir.join("cgroup.kill").exists(); let _ = std::fs::remove_dir(&dir); ok } /// Bear relays the exact signal it received rather than substituting one of /// its own: a build that traps `SIGINT` and `SIGTERM` differently sees the /// real one. Each case sends a distinct signal and asserts the matching trap /// ran (via its marker) and that Bear propagated that trap's exit code. // Requirements: cli-signal-forwarding #[test] #[cfg(target_family = "unix")] #[cfg(all(has_executable_shell, has_executable_sleep))] fn forwards_the_exact_signal_received() -> Result<()> { // (signal for `kill`, marker its trap writes, exit code its trap uses) let cases = [("INT", "INT", 10), ("TERM", "TERM", 20)]; // Distinct INT and TERM traps: whichever marker appears reveals which // signal Bear actually forwarded. If Bear substituted a signal, the // wrong trap would fire. Starvation of the grace window can only leave // the marker empty (or the exit code forced), never write the wrong // marker, so retrying on the supervisor's starvation evidence cannot // mask a signal-substitution regression (issue #713). let traps = [("INT", "INT", 10), ("TERM", "TERM", 20)]; for (signal, expected_marker, expected_code) in cases { for attempt in 1..=STARVATION_ATTEMPTS { let (caught, code, bear_stderr) = run_trapping_build(&format!("signal_fidelity_{signal}_{attempt}"), &traps, signal)?; if caught == expected_marker && code == Some(expected_code) { break; } if attempt < STARVATION_ATTEMPTS && bear_stderr.contains(GRACE_STARVED_LINE) { eprintln!("runner starved the teardown grace window (attempt {attempt}); retrying"); continue; } assert_eq!( caught, expected_marker, "{signal}: build trapped the wrong signal; bear stderr:\n{bear_stderr}" ); assert_eq!( code, Some(expected_code), "{signal}: bear did not propagate the trap's exit code; bear stderr:\n{bear_stderr}" ); } } Ok(()) } /// Wrapper mode nests supervision: bear-driver -> sh -> bear-wrapper -> real /// cc. A compiler blocked mid-compile (reading a writer-less FIFO) is under /// the wrapper's `Inherit` supervision when the signal arrives. Signaling the /// driver must still tear the whole nested tree down within the budget -- not /// leave Bear hanging on the blocked compiler -- so the contract holds in /// wrapper mode just as in preload mode. // Requirements: cli-signal-forwarding, interception-wrapper-mechanism #[test] #[cfg(target_family = "unix")] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn signal_tears_down_nested_wrapper_supervision() -> Result<()> { let env = TestEnvironment::new("signal_wrapper_teardown")?; // Named pipe as the compiler's input: with no writer the compiler blocks // in read() and stays mid-compile until a signal arrives. let fifo = env.test_dir().join("source.c"); let mkfifo_status = std::process::Command::new("mkfifo").arg(&fifo).status()?; assert!(mkfifo_status.success(), "mkfifo failed -- this test needs a POSIX environment"); // Invoke the compiler via $CC so wrapper mode actually supervises it. let build = format!("$CC -x c -c {fifo} -o out.o", fifo = fifo.display()); let script = env.create_shell_script("build.sh", &build)?; let config = "schema: \"4.2\"\n\nintercept:\n mode: wrapper\n"; let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; let mut cmd = env.command_bear(); cmd.current_dir(env.test_dir()).env("CC", filename_of(COMPILER_C_PATH)).args([ "--config", config_path.to_str().unwrap(), "--output", "compile_commands.json", "--", SHELL_PATH, script.to_str().unwrap(), ]); cmd.stdout(Stdio::null()).stderr(Stdio::null()); let mut child = cmd.spawn().expect("failed to spawn bear"); // Let the build reach the compiler and the compiler block on the FIFO. std::thread::sleep(std::time::Duration::from_millis(800)); let signal_time = Instant::now(); let pid = child.id().to_string(); let kill_status = std::process::Command::new("kill").arg("-TERM").arg(&pid).status().expect("kill -TERM failed to run"); assert!(kill_status.success(), "kill -TERM reported failure"); let status = child.wait().expect("failed to wait for bear"); let elapsed = signal_time.elapsed(); assert!(!status.success(), "bear must report non-success after signaling a wrapper-mode build"); assert!( elapsed.as_secs() < 2, "wrapper-mode teardown must reach the blocked compiler within budget, took {elapsed:?}" ); Ok(()) } /// Whether `pid` is still running. `kill -0` probes without sending a /// signal, but it also succeeds on a zombie. In a container whose PID 1 /// does not reap orphans (GitHub Actions container jobs), a killed /// grandchild stays a zombie forever, so on Linux a zombie counts as dead. #[cfg(all(target_family = "unix", has_executable_shell, has_executable_sleep))] fn is_alive(pid: i32) -> bool { #[cfg(target_os = "linux")] if let Ok(stat) = std::fs::read_to_string(format!("/proc/{pid}/stat")) && let Some((_, rest)) = stat.rsplit_once(')') && rest.trim_start().starts_with('Z') { return false; } std::process::Command::new("kill") .args(["-0", &pid.to_string()]) .status() .map(|s| s.success()) .unwrap_or(false) } /// Block until the supervised build reports it is ready (it creates `path`), /// so the test signals Bear only once the build is actually running. #[cfg(all(target_family = "unix", has_executable_shell, has_executable_sleep))] fn wait_for_file(path: &std::path::Path) { let deadline = Instant::now() + std::time::Duration::from_secs(5); while !path.exists() { assert!(Instant::now() < deadline, "build never created {}", path.display()); std::thread::sleep(std::time::Duration::from_millis(20)); } } /// The supervisor's trace line proving the direct child did not exit within /// the teardown grace window: the runner starved the build, and its trap was /// SIGKILLed through no fault of the signal-forwarding contract. Emitted by /// `leader_teardown` in `intercept-supervisor`; the trap tests retry only on /// this evidence (issue #713). #[cfg(all(target_family = "unix", has_executable_shell, has_executable_sleep))] const GRACE_STARVED_LINE: &str = "did not exit within the grace window"; /// Upper bound on scenario attempts when the runner starves the grace /// window. A regression (no grace, wrong signal) fails every attempt, so /// retrying does not mask it. #[cfg(all(target_family = "unix", has_executable_shell, has_executable_sleep))] const STARVATION_ATTEMPTS: usize = 3; /// One attempt of a trapping-build signal scenario: run a shell script that /// installs `traps` (signal name, marker text, exit code) and blocks in a /// long sleep, wait until the script reports ready, send `signal` to bear, /// and return the trap marker's content, bear's exit code, and bear's /// captured stderr (debug level, so the supervisor's teardown trace is in /// it: which signal it forwarded and whether the grace window was starved). #[cfg(all(target_family = "unix", has_executable_shell, has_executable_sleep))] fn run_trapping_build( env_name: &str, traps: &[(&str, &str, i32)], signal: &str, ) -> Result<(String, Option<i32>, String)> { let env = TestEnvironment::new(env_name)?; let marker = env.test_dir().join("trap.marker"); let ready = env.test_dir().join("ready"); let mut script = String::new(); for (sig, text, code) in traps { script.push_str(&format!("trap 'echo {text} > {m} ; exit {code}' {sig} ; ", m = marker.display())); } script.push_str(&format!("echo ready > {r} ; {sleep} 60", r = ready.display(), sleep = SLEEP_PATH)); let mut cmd = env.command_bear(); cmd.current_dir(env.test_dir()) .env("RUST_BACKTRACE", "1") .env("RUST_LOG", "debug") .args(["--output", "compile_commands.json", "--", SHELL_PATH, "-c", &script]) .stdout(Stdio::null()) .stderr(Stdio::piped()); let mut child = cmd.spawn().expect("failed to spawn bear"); // Drain stderr on a thread: with debug logging a full pipe would // otherwise block the very build we are trying to tear down. The write // ends close when bear and its build tree exit, so the read returns EOF. let stderr = child.stderr.take().expect("bear stderr was piped"); let drain = std::thread::spawn(move || { let mut captured = String::new(); let mut stderr = stderr; let _ = stderr.read_to_string(&mut captured); captured }); wait_for_file(&ready); let pid = child.id().to_string(); let kill_status = std::process::Command::new("kill") .arg(format!("-{signal}")) .arg(&pid) .status() .expect("kill failed to run"); assert!(kill_status.success(), "kill -{signal} reported failure"); let status = child.wait().expect("failed to wait for bear"); let bear_stderr = drain.join().unwrap_or_default(); let caught = std::fs::read_to_string(&marker).unwrap_or_default(); Ok((caught.trim().to_string(), status.code(), bear_stderr)) } /// Bear forwards the real signal (not SIGKILL) and grants a grace window, so /// a build that traps the signal runs its trap and Bear's exit code reflects /// whatever the build ultimately exited with. // Requirements: cli-signal-forwarding #[test] #[cfg(target_family = "unix")] #[cfg(all(has_executable_shell, has_executable_sleep))] fn signal_lets_a_trapping_build_run_its_trap() -> Result<()> { // The build traps TERM, records that the trap ran, and exits 42. A // starved runner can blow the whole grace window before the trapping // shell is even scheduled; the supervisor then SIGKILLs the trap // mid-flight and the attempt proves nothing about the contract (issue // #713). Retry only on the supervisor's own starvation evidence. for attempt in 1..=STARVATION_ATTEMPTS { let (caught, code, bear_stderr) = run_trapping_build( &format!("signal_trapping_build_{attempt}"), &[("TERM", "caught", 42)], "TERM", )?; if caught == "caught" && code == Some(42) { return Ok(()); } if attempt < STARVATION_ATTEMPTS && bear_stderr.contains(GRACE_STARVED_LINE) { eprintln!("runner starved the teardown grace window (attempt {attempt}); retrying"); continue; } // The trap ran (real signal forwarded with grace, not an immediate // SIGKILL) and Bear reflected the build's own exit code. assert_eq!(caught, "caught", "build's TERM trap did not run; bear stderr:\n{bear_stderr}"); assert_eq!( code, Some(42), "bear did not propagate the build's trap exit code; bear stderr:\n{bear_stderr}" ); } Ok(()) } /// A build that ignores the termination signal is still stopped: after the /// grace window Bear escalates to SIGKILL, so Bear and the build both end /// within the time budget and Bear reports non-success. // Requirements: cli-signal-forwarding #[test] #[cfg(target_family = "unix")] #[cfg(all(has_executable_shell, has_executable_sleep))] fn signal_escalates_when_build_ignores_it() -> Result<()> { let env = TestEnvironment::new("signal_escalation")?; let ready = env.test_dir().join("ready"); // The build ignores TERM and keeps running, forcing the SIGKILL escalation. let script = format!( "trap '' TERM ; echo ready > {ready} ; while true ; do {sleep} 1 ; done", ready = ready.display(), sleep = SLEEP_PATH, ); let mut cmd = env.command_bear(); cmd.current_dir(env.test_dir()) .args(["--output", "compile_commands.json", "--", SHELL_PATH, "-c", &script]) .stdout(Stdio::null()) .stderr(Stdio::null()); let mut child = cmd.spawn().expect("failed to spawn bear"); wait_for_file(&ready); let signal_time = Instant::now(); let pid = child.id().to_string(); let kill_status = std::process::Command::new("kill").arg("-TERM").arg(&pid).status().expect("kill -TERM failed to run"); assert!(kill_status.success(), "kill -TERM reported failure"); let status = child.wait().expect("failed to wait for bear"); let elapsed = signal_time.elapsed(); assert!(!status.success(), "bear must report non-success after escalating"); assert!( elapsed.as_secs() < 2, "bear must stop an unresponsive build within the budget, took {elapsed:?}" ); Ok(()) } // Semantic mode exit code tests (note: this is now called 'semantic' not 'citnames') /// Test that semantic command returns 0 for valid input // Requirements: cli-exit-codes #[test] fn semantic_exit_code_for_success() -> Result<()> { let env = TestEnvironment::new("semantic_exit_code_for_success")?; // Create a sample events file let events_content = r#"{"executable":"/usr/bin/gcc","arguments":["-c","test.c"],"working_dir":"/tmp","environment":{}}"#; env.create_source_files(&[("events.json", events_content)])?; let result = env.run_bear(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; result.assert_success()?; Ok(()) } /// `bear semantic` on input whose events are all understood but name only /// non-compiler commands exits 0 and writes an empty database: the filter /// did its job, and an empty result is not a failure. // Requirements: cli-exit-codes #[test] fn semantic_exit_code_for_understood_non_compiler_events() -> Result<()> { let env = TestEnvironment::new("semantic_exit_code_non_compiler")?; // Both events conform to the format; neither names a compiler. let events_content = [ r#"{"executable":"ls","arguments":["ls","-la"],"working_dir":"/tmp","environment":{}}"#, r#"{"executable":"echo","arguments":["echo","done"],"working_dir":"/tmp","environment":{}}"#, ] .join("\n"); env.create_source_files(&[("events.json", &events_content)])?; let result = env.run_bear(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; result.assert_success()?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(0)?; Ok(()) } /// Test that semantic command with missing input file returns non-zero // Requirements: cli-exit-codes #[test] fn semantic_exit_code_for_missing_input() -> Result<()> { let env = TestEnvironment::new("semantic_exit_code_for_missing_input")?; let result = env.run_bear(&["semantic", "--input", "nonexistent.json", "--output", "compile_commands.json"])?; result.assert_failure()?; Ok(()) } �����������������������������������rizsotto-Bear-0c30b27/tests/integration/tests/cases/hardened_intercept.rs���������������������������0000664�0000000�0000000�00000057430�15271777203�0026746�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: GPL-3.0-or-later //! Hardened interception tests for Bear //! //! These tests verify that Bear's interception mechanism survives when build //! scripts and programs deliberately sabotage environment variables //! (`LD_PRELOAD`, `BEAR_INTERCEPT`). //! //! ## Test Categories //! //! ### Shell-level environment sabotage (HI-1 through HI-5) //! //! Tests where shell scripts modify the environment before invoking compilers. //! These work because the preload library is already loaded in the shell //! process and intercepts the subsequent `exec*` call. The `resolve_environment` //! function in `implementation.rs` detects that the envp is missing the session //! variables and creates a "doctored" copy with `LD_PRELOAD` and `BEAR_INTERCEPT` //! restored before forwarding to the real `exec*`. //! //! ### system()/popen() after environ modification (HI-6, HI-7) //! //! Simulates issue #660: a C program (standing in for a build orchestrator like //! perl) calls `unsetenv("LD_PRELOAD")` then `system("gcc ...")`. //! //! The preload library does not call the real `system()`/`popen()` (whose //! internal fork+exec path is libc-version-dependent); it reimplements both //! via `posix_spawnp` with a doctored envp, so the call itself is always //! intercepted. What still depends on the platform is which descendant exec //! reaches the database: where /bin/sh cannot load the preload library (a //! static busybox in the nix build sandbox), the shell's children go //! unrecorded and the first captured event comes from a later dynamic //! process (the compiler wrapper script's interpreter). The assertions //! therefore pin the compiled file rather than the exact compiler argv. //! //! ### Competing LD_PRELOAD libraries (HI-8) //! //! Simulates issue #675: another LD_PRELOAD library (`libsandbox.so` on //! Gentoo) coexists with Bear's. //! //! **Why this currently passes:** //! //! When two LD_PRELOAD libraries both override `execve`, `dlsym(RTLD_NEXT)` //! from the first library resolves to the second library's `execve`, which //! in turn resolves to libc's. This forms a working chain as long as each //! link delegates correctly. Our trivial mock library does exactly that. //! //! **When this would fail:** //! //! - The competing library modifies the envp (strips `LD_PRELOAD`) before //! delegating — our doctoring would have already run, but the downstream //! library could undo it. //! - The competing library calls the `execve` **syscall** directly (via //! `syscall(SYS_execve, ...)`) instead of using `dlsym(RTLD_NEXT)`. //! - On architectures with different calling conventions for variadic //! functions (e.g., i686), two chained shim layers for `execle` may //! corrupt the stack — this is the likely cause of the SIGSEGV in //! issue #675 on x86 (32-bit). //! //! Confirmed in the field by issue #699 (bugs.gentoo.org/973619): when //! Bear's own suite runs inside Gentoo's `emerge` sandbox, `libsandbox.so` //! strips `bear-lib` from `LD_PRELOAD` after an `env -i`, so the env-clear //! tests here fail. This is a packaging-side concern, not a Bear defect - //! see the "Preload conflicts with sandboxes" limitation in //! `docs/requirements/interception-preload-mechanism.md`. use crate::fixtures::constants::*; use crate::fixtures::infrastructure::*; use anyhow::Result; /// Config to enforce preload mode (same as intercept_posix tests). const CONFIG: &str = concat!( r#"schema: '4.2' intercept: mode: preload path: "#, env!("PRELOAD_LIBRARY_PATH"), r#" "# ); // ========================================================================= // Shell-level environment sabotage tests (HI-1 through HI-5) // // These all work because the preload library is loaded in the shell and // doctors the envp on every exec* call via `resolve_environment`. // ========================================================================= /// HI-1: Build script that unsets LD_PRELOAD before compiling. #[test] #[cfg(has_preload_library)] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn hardened_unset_ld_preload() -> Result<()> { let env = TestEnvironment::new("hardened_unset_ld_preload")?; env.create_source_files(&[("test.c", "int main() { return 0; }")])?; let build_commands = ["unset LD_PRELOAD".to_string(), format!("{} -c test.c -o test.o", filename_of(COMPILER_C_PATH))] .join("\n"); let script_path = env.create_shell_script("build.sh", &build_commands)?; env.run_bear_success(&[ "--output", "compile_commands.json", "--", SHELL_PATH, script_path.to_str().unwrap(), ])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; db.assert_contains(&compilation_entry!( file: "test.c".to_string(), directory: env.test_dir().to_str().unwrap().to_string(), arguments: vec![ COMPILER_C_PATH.to_string(), "-c".to_string(), "test.c".to_string(), "-o".to_string(), "test.o".to_string(), ] ))?; Ok(()) } /// HI-2: Build script that overwrites BEAR_INTERCEPT with garbage. #[test] #[cfg(has_preload_library)] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn hardened_overwrite_bear_intercept() -> Result<()> { let env = TestEnvironment::new("hardened_overwrite_bear_intercept")?; env.create_source_files(&[("test.c", "int main() { return 0; }")])?; let build_commands = [ "export BEAR_INTERCEPT=garbage".to_string(), format!("{} -c test.c -o test.o", filename_of(COMPILER_C_PATH)), ] .join("\n"); let script_path = env.create_shell_script("build.sh", &build_commands)?; env.run_bear_success(&[ "--output", "compile_commands.json", "--", SHELL_PATH, script_path.to_str().unwrap(), ])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; db.assert_contains(&compilation_entry!( file: "test.c".to_string(), directory: env.test_dir().to_str().unwrap().to_string(), arguments: vec![ COMPILER_C_PATH.to_string(), "-c".to_string(), "test.c".to_string(), "-o".to_string(), "test.o".to_string(), ] ))?; Ok(()) } /// HI-3: Build script that clears the entire environment with `env -i`. // Requirements: interception-preload-mechanism #[test] #[cfg(has_preload_library)] #[cfg(all(has_executable_compiler_c, has_executable_shell, has_executable_env))] fn hardened_env_clear() -> Result<()> { let env = TestEnvironment::new("hardened_env_clear")?; env.create_source_files(&[("test.c", "int main() { return 0; }")])?; let build_commands = format!( "{env} -i PATH=/usr/bin:/bin {compiler} -c test.c -o test.o", env = ENV_PATH, compiler = COMPILER_C_PATH, ); let script_path = env.create_shell_script("build.sh", &build_commands)?; env.run_bear_success(&[ "--output", "compile_commands.json", "--", SHELL_PATH, script_path.to_str().unwrap(), ])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; db.assert_contains(&compilation_entry!( file: "test.c".to_string(), directory: env.test_dir().to_str().unwrap().to_string(), arguments: vec![ COMPILER_C_PATH.to_string(), "-c".to_string(), "test.c".to_string(), "-o".to_string(), "test.o".to_string(), ] ))?; Ok(()) } /// HI-4: Nested make invocations where the inner make clears the environment. #[test] #[cfg(has_preload_library)] #[cfg(all(has_executable_compiler_c, has_executable_shell, has_executable_make))] fn hardened_nested_make_env_cleared() -> Result<()> { let env = TestEnvironment::new("hardened_nested_make")?; env.create_source_files(&[ ("outer.c", "int outer() { return 1; }"), ("inner/inner.c", "int inner() { return 2; }"), ])?; let inner_makefile = format!("all:\n\t{compiler} -c inner.c -o inner.o\n", compiler = COMPILER_C_PATH); env.create_makefile("inner/Makefile", &inner_makefile)?; // NOTE: `inner` must be .PHONY because `inner/` is a real directory; // without this, make considers the target up-to-date and skips the recipe. let outer_makefile = format!( concat!( ".PHONY: inner\n", "all: outer.o inner\n", "outer.o:\n", "\t{compiler} -c outer.c -o outer.o\n", "inner:\n", "\tunset LD_PRELOAD && $(MAKE) -C inner\n", ), compiler = COMPILER_C_PATH, ); env.create_makefile("Makefile", &outer_makefile)?; env.run_bear_success(&["--output", "compile_commands.json", "--", MAKE_PATH])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_contains(&compilation_entry!( file: "outer.c".to_string(), directory: env.test_dir().to_str().unwrap().to_string(), arguments: vec![ COMPILER_C_PATH.to_string(), "-c".to_string(), "outer.c".to_string(), "-o".to_string(), "outer.o".to_string(), ] ))?; db.assert_contains(&compilation_entry!( file: "inner.c".to_string(), directory: env.test_dir().join("inner").to_str().unwrap().to_string(), arguments: vec![ COMPILER_C_PATH.to_string(), "-c".to_string(), "inner.c".to_string(), "-o".to_string(), "inner.o".to_string(), ] ))?; Ok(()) } /// HI-5: Wrapper script that does `exec env -i gcc "$@"`. #[test] #[cfg(has_preload_library)] #[cfg(all(has_executable_compiler_c, has_executable_shell, has_executable_env))] fn hardened_wrapper_exec_env_clear() -> Result<()> { let env = TestEnvironment::new("hardened_wrapper_exec_env_clear")?; env.create_source_files(&[("test.c", "int main() { return 0; }")])?; let wrapper_commands = format!( "exec {env} -i PATH=/usr/bin:/bin {compiler} \"$@\"", env = ENV_PATH, compiler = COMPILER_C_PATH, ); let wrapper_path = env.create_shell_script("cc-wrapper.sh", &wrapper_commands)?; let build_commands = format!("{} -c test.c -o test.o", wrapper_path.to_str().unwrap()); let script_path = env.create_shell_script("build.sh", &build_commands)?; env.run_bear_success(&[ "--output", "compile_commands.json", "--", SHELL_PATH, script_path.to_str().unwrap(), ])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; db.assert_contains(&compilation_entry!( file: "test.c".to_string(), directory: env.test_dir().to_str().unwrap().to_string(), arguments: vec![ COMPILER_C_PATH.to_string(), "-c".to_string(), "test.c".to_string(), "-o".to_string(), "test.o".to_string(), ] ))?; Ok(()) } // ========================================================================= // system()/popen() after environ modification (simulates #660) // // See module-level docs for why these pass on glibc ≥ 2.34 and when // they would fail. // ========================================================================= /// HI-6: C program that unsets LD_PRELOAD+BEAR_INTERCEPT, then uses system() to compile. /// /// Simulates issue #660: a build orchestrator strips Bear's environment /// variables from its own `environ`, then spawns compilation via `system()`. /// Bear's interposed `system()` spawns the shell itself with a doctored /// envp, so the call is recorded regardless of libc internals. #[test] #[cfg(has_preload_library)] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn hardened_system_after_unsetenv() -> Result<()> { let env = TestEnvironment::new("hardened_system_after_unsetenv")?; env.create_source_files(&[("hello.c", "int main() { return 0; }")])?; let c_program = format!( r#"#include <stdlib.h> int main() {{ /* Strip Bear's environment variables — simulates a build tool that sanitizes the environment before spawning sub-processes. */ unsetenv("LD_PRELOAD"); unsetenv("BEAR_INTERCEPT"); /* Now invoke the compiler via system(). system() uses the process's environ internally. */ return system("{compiler} -c hello.c -o hello.o"); }}"#, compiler = COMPILER_C_PATH, ); env.create_source_files(&[("orchestrator.c", &c_program)])?; env.run_c_compiler("orchestrator", &["orchestrator.c"])?; env.create_config(CONFIG)?; env.run_bear_success(&[ "--config", "config.yml", "--output", "compile_commands.json", "--", "./orchestrator", ])?; let db = env.load_compilation_database("compile_commands.json")?; // The compiler invocation inside system() must be captured. Pin the // compiled file rather than the argv: where /bin/sh cannot load the // preload library (a static busybox in the nix sandbox), the first // recorded exec is the wrapper script's interpreter execing the real // compiler - with the distribution's injected flags - not the wrapper // invocation itself. db.assert_contains( &CompilationEntryMatcher::new() .file("hello.c".to_string()) .directory(env.test_dir().to_str().unwrap().to_string()) .output("hello.o".to_string()), )?; Ok(()) } /// HI-7: C program that unsets LD_PRELOAD+BEAR_INTERCEPT, then uses popen() to compile. /// /// Same mechanism as HI-6 but through `popen()`. #[test] #[cfg(has_preload_library)] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn hardened_popen_after_unsetenv() -> Result<()> { let env = TestEnvironment::new("hardened_popen_after_unsetenv")?; env.create_source_files(&[("hello.c", "int main() { return 0; }")])?; env.create_config(CONFIG)?; let c_program = format!( r#"#include <stdlib.h> #include <stdio.h> int main() {{ /* Strip Bear's environment variables */ unsetenv("LD_PRELOAD"); unsetenv("BEAR_INTERCEPT"); /* Use popen to invoke the compiler */ FILE *fp = popen("{compiler} -c hello.c -o hello.o 2>&1", "r"); if (!fp) return 1; /* Drain output */ char buf[256]; while (fgets(buf, sizeof(buf), fp)) {{}} return pclose(fp) == 0 ? 0 : 1; }}"#, compiler = COMPILER_C_PATH, ); env.create_source_files(&[("orchestrator.c", &c_program)])?; env.run_c_compiler("orchestrator", &["orchestrator.c"])?; env.run_bear_success(&[ "--config", "config.yml", "--output", "compile_commands.json", "--", "./orchestrator", ])?; let db = env.load_compilation_database("compile_commands.json")?; // The compiler invocation inside popen() must be captured; see HI-6 for // why the assertion pins the compiled file rather than the argv. db.assert_contains( &CompilationEntryMatcher::new() .file("hello.c".to_string()) .directory(env.test_dir().to_str().unwrap().to_string()) .output("hello.o".to_string()), )?; Ok(()) } // ========================================================================= // Competing LD_PRELOAD library (simulates #675) // ========================================================================= /// HI-8: Another LD_PRELOAD library coexists with Bear's. /// /// Simulates issue #675 where Gentoo's `libsandbox.so` is already in /// LD_PRELOAD when Bear starts. We build a trivial shared library that also /// overrides `execve` (just delegates to the real one via RTLD_NEXT), append /// it to LD_PRELOAD, and verify that interception still works. /// /// This works because LD_PRELOAD libraries form a chain: our `execve` calls /// `dlsym(RTLD_NEXT, "execve")` which resolves to the competing library's /// `execve`, which in turn calls `dlsym(RTLD_NEXT, "execve")` → libc. /// /// The real #675 issue involves additional complexity: /// - `libsandbox.so` may modify the environment or refuse certain exec calls. /// - On i686 (32-bit), chained variadic function overrides (`execle`) can /// corrupt the stack — likely the cause of the SIGSEGV reported in #675. /// This test covers the basic "chain of RTLD_NEXT" scenario only. /// /// See also HI-9 which tests the harder case: competing library + `env -i`. // Requirements: interception-preload-mechanism #[test] #[cfg(has_preload_library)] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn hardened_competing_ld_preload() -> Result<()> { let env = TestEnvironment::new("hardened_competing_ld_preload")?; env.create_source_files(&[("test.c", "int main() { return 0; }")])?; // Build a trivial shared library that overrides execve. // It just calls the real execve via dlsym(RTLD_NEXT, ...). // This simulates what libsandbox.so does. let competing_lib_source = r#" #define _GNU_SOURCE #include <dlfcn.h> #include <unistd.h> typedef int (*execve_func_t)(const char *, char *const[], char *const[]); int execve(const char *path, char *const argv[], char *const envp[]) { execve_func_t real_execve = (execve_func_t)dlsym(RTLD_NEXT, "execve"); if (!real_execve) return -1; return real_execve(path, argv, envp); } "#; env.create_source_files(&[("competing_lib.c", competing_lib_source)])?; // Compile the competing shared library let compile_result = { let mut cmd = std::process::Command::new(COMPILER_C_PATH); cmd.current_dir(env.test_dir()).args([ "-shared", "-fPIC", "-o", "libcompeting.so", "competing_lib.c", "-ldl", ]); cmd.output()? }; if !compile_result.status.success() { anyhow::bail!( "Failed to compile competing library: {}", String::from_utf8_lossy(&compile_result.stderr) ); } // Build script that compiles with the competing library in LD_PRELOAD let build_commands = format!( "export LD_PRELOAD=\"$LD_PRELOAD:{}\" && {} -c test.c -o test.o", env.test_dir().join("libcompeting.so").to_str().unwrap(), filename_of(COMPILER_C_PATH), ); let script_path = env.create_shell_script("build.sh", &build_commands)?; env.run_bear_success(&[ "--output", "compile_commands.json", "--", SHELL_PATH, script_path.to_str().unwrap(), ])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; db.assert_contains(&compilation_entry!( file: "test.c".to_string(), directory: env.test_dir().to_str().unwrap().to_string(), arguments: vec![ COMPILER_C_PATH.to_string(), "-c".to_string(), "test.c".to_string(), "-o".to_string(), "test.o".to_string(), ] ))?; Ok(()) } /// HI-9: Competing LD_PRELOAD library survives exec with empty envp. /// /// Simulates the core #675 failure: a competing LD_PRELOAD library (like /// Gentoo's `libsandbox.so`) coexists with Bear's, and a program calls /// `execve` with a completely empty envp. When Bear's doctoring restores /// `LD_PRELOAD`, it must include the competing library — not just its own. /// /// The competing library here **enforces** its presence: its `execve` /// override checks that its path appears in the envp's `LD_PRELOAD` and /// rejects the exec with `EPERM` if missing. This simulates sandbox's /// self-protection. /// /// We use a C program that calls `execve` directly (not `env -i`) because /// `env` uses `execvp` → `execvpe` which bypasses the competing library's /// `execve` override entirely. /// /// Without the fix (preserving the initial full LD_PRELOAD in INITIAL_PRELOAD), /// doctoring after an empty-envp exec would set `LD_PRELOAD=libexec.so` only, /// and the competing library's check would reject the exec. #[test] #[cfg(has_preload_library)] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn hardened_competing_ld_preload_survives_env_clear() -> Result<()> { let env = TestEnvironment::new("hardened_competing_env_clear")?; env.create_config(CONFIG)?; env.create_source_files(&[("test.c", "int main() { return 0; }")])?; // Build a competing library that enforces its own presence in LD_PRELOAD. // If a child exec's envp does NOT contain this library in LD_PRELOAD, // the library rejects the exec with EPERM. This simulates sandbox // self-protection behavior. let lib_path = env.test_dir().join("libcompeting.so"); let competing_lib_source = format!( r#" #define _GNU_SOURCE #include <dlfcn.h> #include <errno.h> #include <string.h> #include <unistd.h> #define MY_PATH "{lib_path}" typedef int (*execve_func_t)(const char *, char *const[], char *const[]); int execve(const char *path, char *const argv[], char *const envp[]) {{ execve_func_t real_execve = (execve_func_t)dlsym(RTLD_NEXT, "execve"); if (!real_execve) return -1; /* Check that our library is still in LD_PRELOAD. If not, reject the exec — simulating sandbox self-protection. */ int found = 0; for (char *const *e = envp; *e; e++) {{ if (strstr(*e, "LD_PRELOAD=") == *e) {{ if (strstr(*e, MY_PATH)) {{ found = 1; }} break; }} }} if (!found) {{ errno = EPERM; return -1; }} return real_execve(path, argv, envp); }} "#, lib_path = lib_path.to_str().unwrap(), ); env.create_source_files(&[("competing_lib.c", &competing_lib_source)])?; let compile_result = { let mut cmd = std::process::Command::new(COMPILER_C_PATH); cmd.current_dir(env.test_dir()).args([ "-shared", "-fPIC", "-o", "libcompeting.so", "competing_lib.c", "-ldl", ]); cmd.output()? }; if !compile_result.status.success() { anyhow::bail!( "Failed to compile competing library: {}", String::from_utf8_lossy(&compile_result.stderr) ); } // Build a C program that calls execve with an empty envp. // This forces the call through execve (not execvp/execvpe), so the // competing library's execve override is in the RTLD_NEXT chain. let launcher_source = format!( r#"#include <unistd.h> #include <stdio.h> int main() {{ char *const argv[] = {{ "{compiler}", "-c", "test.c", "-o", "test.o", 0 }}; char *const envp[] = {{ "PATH=/usr/bin:/bin", 0 }}; execve("{compiler}", argv, envp); perror("execve failed"); return 1; }}"#, compiler = COMPILER_C_PATH, ); env.create_source_files(&[("launcher.c", &launcher_source)])?; env.run_c_compiler("launcher", &["launcher.c"])?; // Run bear with the competing library added to LD_PRELOAD. // Bear will prepend libexec.so, giving: libexec.so:libcompeting.so // The launcher then calls execve with an empty envp. Bear's doctoring // must restore LD_PRELOAD with BOTH libraries for the competing // library's self-check to pass. let build_commands = format!("export LD_PRELOAD=\"$LD_PRELOAD:{}\" && ./launcher", lib_path.to_str().unwrap(),); let script_path = env.create_shell_script("build.sh", &build_commands)?; env.run_bear_success(&[ "--config", "config.yml", "--output", "compile_commands.json", "--", SHELL_PATH, script_path.to_str().unwrap(), ])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_contains(&compilation_entry!( file: "test.c".to_string(), directory: env.test_dir().to_str().unwrap().to_string(), arguments: vec![ COMPILER_C_PATH.to_string(), "-c".to_string(), "test.c".to_string(), "-o".to_string(), "test.o".to_string(), ] ))?; Ok(()) } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/tests/cases/install.rs��������������������������������������0000664�0000000�0000000�00000005132�15271777203�0024555�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: GPL-3.0-or-later //! Tests for the installation layout that `scripts/install.sh` produces. use crate::fixtures::constants::*; use crate::fixtures::infrastructure::InstallEnvironment; #[cfg(all(has_preload_library, not(target_os = "macos"), has_executable_shell, has_executable_true))] use crate::fixtures::infrastructure::{EventMatcher, TestEnvironment}; use anyhow::Result; /// Distribution tooling (RPM's debuginfo extraction and strip step) only /// processes shared libraries that are executable, so packagers had to /// `chmod` the preload library after running the install script. #[test] fn preload_library_is_installed_executable() -> Result<()> { use std::os::unix::fs::PermissionsExt; let install = InstallEnvironment::new()?; let sut = install.install_dir().join("libexec/bear").join(INTERCEPT_LIBDIR).join(PRELOAD_LIBRARY); let mode = std::fs::metadata(&sut)?.permissions().mode(); assert_eq!(mode & 0o777, 0o755, "{} has mode {:o}", sut.display(), mode & 0o777); Ok(()) } /// Distributions without a `libexec` directory (Arch Linux) choose the /// parent of the `bear/` directory. The driver finds its siblings by /// relative path, so the relocated install still intercepts: the `true` /// that the shell runs is only reported when the preload library loads. #[test] #[cfg(all(has_preload_library, not(target_os = "macos"), has_executable_shell, has_executable_true))] fn libexecdir_relocates_the_helpers() -> Result<()> { let install = InstallEnvironment::with_libexecdir("lib")?; let prefix = install.install_dir().to_path_buf(); let env = TestEnvironment::with_install("libexecdir_relocates_the_helpers", install)?; env.run_bear_success(&["intercept", "--output", "events.json", "--", SHELL_PATH, "-c", TRUE_PATH])?; let sut = env.load_events_file("events.json")?; sut.assert_contains(&EventMatcher::new().executable_path(TRUE_PATH))?; assert!(prefix.join("lib/bear/bin/bear-driver").is_file(), "driver not under LIBEXECDIR"); assert!(!prefix.join("libexec").exists(), "default libexec directory was still created"); Ok(()) } /// A trailing slash on `LIBEXECDIR` must not reach the shipped entry /// script as a `//` in the driver path. #[test] fn libexecdir_trailing_slash_is_dropped() -> Result<()> { let install = InstallEnvironment::with_libexecdir("lib/")?; let expected = format!("exec {}/bear/bin/bear-driver ", install.install_dir().join("lib").display()); let sut = std::fs::read_to_string(install.path())?; assert!(sut.contains(&expected), "expected {expected:?} in the entry script:\n{sut}"); Ok(()) } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/tests/cases/intercept.rs������������������������������������0000664�0000000�0000000�00000135151�15271777203�0025111�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: GPL-3.0-or-later //! Intercept functionality tests for Bear integration //! //! These tests verify that Bear's command interception works correctly //! across different scenarios, ported from the Python/lit test suite. use crate::fixtures::constants::*; use crate::fixtures::infrastructure::*; use anyhow::Result; /// Test basic command interception with preload mechanism // Requirements: interception-preload-mechanism #[test] #[cfg(target_family = "unix")] #[cfg(has_executable_compiler_c)] fn basic_command_interception() -> Result<()> { let env = TestEnvironment::new("basic_intercept")?; env.create_source_files(&[("test.c", "int main() { return 0; }")])?; // Run intercept mode to capture commands env.run_bear_success(&["intercept", "--output", "events.json", "--", COMPILER_C_PATH, "-c", "test.c"])?; // Load and verify events let events = env.load_events_file("events.json")?; // Should have at least one event assert!(!events.events().is_empty()); // Should contain compiler execution event let compiler_matcher = event_matcher!(executable_path: COMPILER_C_PATH.to_string()); events.assert_contains(&compiler_matcher)?; Ok(()) } /// Test shell command interception. Running without any interception /// configuration on a Unix host also exercises the default mode /// selection: only the preload mechanism can observe the shell process. // Requirements: interception-preload-mechanism, interception-mechanism-selection #[test] #[cfg(has_preload_library)] #[cfg(all(has_executable_compiler_c, has_executable_echo, has_executable_shell))] fn shell_command_interception() -> Result<()> { let env = TestEnvironment::new("shell_intercept")?; env.create_source_files(&[("test.c", "int main() { return 0; }")])?; // Create shell script that runs compiler let build_commands = [ format!("{} \"Starting build...\"", ECHO_PATH), format!("{} -c test.c -o test.o", COMPILER_C_PATH), format!("{} \"Build complete\"", ECHO_PATH), ] .join("\n"); let script_path = env.create_shell_script("build.sh", &build_commands)?; // Intercept shell script execution env.run_bear_success(&[ "intercept", "--output", "events.json", "--", SHELL_PATH, script_path.to_str().unwrap(), ])?; // Load and verify events let events = env.load_events_file("events.json")?; // Should have captured events assert!(!events.events().is_empty()); // Should capture shell execution let shell_matcher = event_matcher!(executable_name: "sh".to_string()); events.assert_contains(&shell_matcher)?; // Should capture compiler execution let compiler_matcher = event_matcher!(executable_path: COMPILER_C_PATH.to_string()); events.assert_contains(&compiler_matcher)?; Ok(()) } /// Test shell commands without shebang #[test] #[cfg(target_family = "unix")] #[cfg(all(has_executable_compiler_c, has_executable_echo, has_executable_shell))] fn shell_commands_without_shebang() -> Result<()> { let env = TestEnvironment::new("shell_no_shebang")?; env.create_source_files(&[("test.c", "int main() { return 0; }")])?; // Create shell script WITHOUT shebang let shell_script = [ format!("{} \"Building without shebang...\"", ECHO_PATH), format!("{} -c test.c", COMPILER_C_PATH), format!("{} \"Done\"", ECHO_PATH), ] .join("\n"); let script_path = env.create_build_script("build_no_shebang.sh", &shell_script)?; let _output = env.run_bear_success(&[ "intercept", "--output", "events.json", "--", SHELL_PATH, script_path.to_str().unwrap(), ])?; // Load and verify events let events = env.load_events_file("events.json")?; // Should still capture commands even without shebang events.assert_min_count(1)?; Ok(()) } /// Test parallel command interception #[test] #[cfg(target_family = "unix")] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn parallel_command_interception() -> Result<()> { let env = TestEnvironment::new("parallel_intercept")?; // Create multiple source files for i in 1..=4 { env.create_source_files(&[( &format!("test_{}.c", i), &format!("int func_{}() {{ return {}; }}", i, i), )])?; } // Create parallel build script let build_commands = [ format!("{} -c test_1.c &", filename_of(COMPILER_C_PATH)), format!("{} -c test_2.c &", filename_of(COMPILER_C_PATH)), format!("{} -c test_3.c &", filename_of(COMPILER_C_PATH)), format!("{} -c test_4.c &", filename_of(COMPILER_C_PATH)), "wait".to_string(), ] .join("\n"); let script_path = env.create_shell_script("parallel_build.sh", &build_commands)?; env.run_bear_success(&[ "intercept", "--output", "events.json", "--", SHELL_PATH, script_path.to_str().unwrap(), ])?; // Load and verify events let events = env.load_events_file("events.json")?; // Each of the 4 parallel compiler invocations must be captured exactly // once. Match the full argument vector rather than the executable name: // where `gcc` is a wrapper script (nixpkgs, ccache), the wrapper's exec // of the real compiler shows up under the same name and doubles a // name-only count. for i in 1..=4 { let specific_compiler_matcher = EventMatcher::new().executable_name(filename_of(COMPILER_C_PATH)).arguments(vec![ filename_of(COMPILER_C_PATH).to_string(), "-c".to_string(), format!("test_{}.c", i), ]); events.assert_count_matching(&specific_compiler_matcher, 1)?; } Ok(()) } /// Test build stdout capture during interception #[test] #[cfg(all(has_executable_shell, has_executable_echo, has_executable_true))] fn build_stdout_capture() -> Result<()> { let env = TestEnvironment::new("stdout_capture")?; // Create script that outputs to stdout let script_commands = [ format!("\"{}\" \"This goes to stdout\"", ECHO_PATH), format!("\"{}\" \"This also goes to stdout\"", ECHO_PATH), format!("\"{}\"", TRUE_PATH), ] .join("\n"); let script_path = env.create_shell_script("stdout_test.sh", &script_commands)?; let output = env.run_bear_success(&[ "intercept", "--output", "events.json", "--", SHELL_PATH, script_path.to_str().unwrap(), ])?; // Stdout should be preserved assert!(output.stdout().contains("This goes to stdout")); assert!(output.stdout().contains("This also goes to stdout")); // Load and verify events let events = env.load_events_file("events.json")?; // Events should still be captured events.assert_min_count(1)?; Ok(()) } /// Test build stderr capture during interception #[test] #[cfg(all(has_executable_shell, has_executable_echo, has_executable_true))] fn build_stderr_capture() -> Result<()> { let env = TestEnvironment::new("stderr_capture")?; // Create script that outputs to stderr let script_commands = [ format!("\"{}\" \"This goes to stderr\" >&2", ECHO_PATH), format!("\"{}\" \"This also goes to stderr\" >&2", ECHO_PATH), format!("\"{}\"", TRUE_PATH), ] .join("\n"); let script_path = env.create_shell_script("stderr_test.sh", &script_commands)?; let output = env.run_bear_success(&[ "intercept", "--output", "events.json", "--", SHELL_PATH, script_path.to_str().unwrap(), ])?; // Stderr should be preserved in the stderr stream assert!(output.stderr().contains("This goes to stderr")); assert!(output.stderr().contains("This also goes to stderr")); // Load and verify events let events = env.load_events_file("events.json")?; // Events should still be captured events.assert_min_count(1)?; Ok(()) } /// Test interception with empty environment #[test] #[cfg(target_family = "unix")] #[cfg(all(has_executable_compiler_c, has_executable_env))] fn intercept_empty_environment() -> Result<()> { let env = TestEnvironment::new("empty_env_intercept")?; env.create_source_files(&[("test.c", "int main() { return 0; }")])?; // Run with minimal environment let _output = env.run_bear_success(&[ "intercept", "--output", "events.json", "--", ENV_PATH, "-i", "PATH=/usr/bin:/bin", COMPILER_C_PATH, "-c", "test.c", ])?; // Load and verify events let events = env.load_events_file("events.json")?; // Should still capture execution even with empty environment events.assert_min_count(1)?; Ok(()) } /// Test libtool command interception /// /// Note: This test might be fragile test, because libtool versions are different. /// eg.: MacOS CI was failing to complain about "unknown option character `-' in: --mode=compile". #[test] #[cfg(has_preload_library)] #[cfg(all(has_executable_libtool, has_executable_compiler_c))] fn libtool_command_interception() -> Result<()> { let env = TestEnvironment::new("libtool_intercept")?; env.create_source_files(&[("lib.c", "int lib_func() { return 42; }")])?; // Use libtool to compile a library let _output = env.run_bear_success(&[ "intercept", "--output", "events.json", "--", LIBTOOL_PATH, "--mode=compile", "--tag=CC", COMPILER_C_PATH, "-c", "lib.c", ])?; // Load and verify events let events = env.load_events_file("events.json")?; // Should capture libtool and compiler invocations events.assert_min_count(1)?; // Should have captured libtool execution let libtool_matcher = EventMatcher::new().executable_name("libtool".to_string()); events.assert_contains(&libtool_matcher)?; Ok(()) } /// In wrapper mode, Bear creates a deterministic `.bear/` directory in the /// working directory during the build and removes it automatically on exit. /// Verify both: the build observes `.bear/` while it runs, and after Bear /// returns the directory is gone. The config forces wrapper mode on a host /// whose default is preload, so the observed `.bear/` directory is also /// evidence that the configuration override selected the wrapper mechanism. // Requirements: interception-wrapper-mechanism, interception-mechanism-selection #[test] #[cfg(target_family = "unix")] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn wrapper_mode_creates_and_cleans_up_bear_directory() -> Result<()> { let env = TestEnvironment::new("wrapper_bear_dir_cleanup")?; env.create_source_files(&[("test.c", "int main() { return 0; }")])?; // Build script records whether `.bear/` was present during the build, // then invokes the compiler via $CC so the wrapper is exercised. let build = r#"if [ -d .bear ]; then echo present > bear_dir_status.txt; else echo missing > bear_dir_status.txt; fi $CC -c test.c -o test.o "#; let script = env.create_shell_script("build.sh", build)?; // Config forces wrapper mode and does not list compilers: Bear discovers // them via the CC env var and PATH scan (masquerade-aware, see // `interception-wrapper-recursion`). let config = r#" schema: "4.2" intercept: mode: wrapper "#; let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; let mut cmd = env.command_bear(); cmd.current_dir(env.test_dir()).env("CC", filename_of(COMPILER_C_PATH)).args([ "--config", config_path.to_str().unwrap(), "--output", "compile_commands.json", "--", SHELL_PATH, script.to_str().unwrap(), ]); let output = cmd.output()?; assert!( output.status.success(), "bear failed: stdout={}\nstderr={}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); let status = env.read_file("bear_dir_status.txt")?; assert_eq!(status.trim(), "present", "expected .bear/ to exist during the build, got {status:?}"); let bear_dir = env.test_dir().join(".bear"); assert!( !bear_dir.exists(), ".bear/ must be cleaned up after Bear exits but still present at {bear_dir:?}" ); Ok(()) } /// Two back-to-back wrapper-mode runs in the same working directory must each /// create `.bear/` (deterministic name, not a random temp dir) and clean it /// up. This matches the "deterministic directory" guarantee so that paths /// recorded during `./configure` survive when a follow-on step re-runs Bear. // Requirements: interception-wrapper-mechanism #[test] #[cfg(target_family = "unix")] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn wrapper_mode_bear_directory_is_deterministic_across_runs() -> Result<()> { let env = TestEnvironment::new("wrapper_bear_dir_deterministic")?; env.create_source_files(&[("test.c", "int main() { return 0; }")])?; let build = r#"ls -d .bear >> bear_dir_observed.txt 2>/dev/null || echo missing >> bear_dir_observed.txt $CC -c test.c -o test.o "#; let script = env.create_shell_script("build.sh", build)?; let config = r#" schema: "4.2" intercept: mode: wrapper "#; let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; for _ in 0..2 { let mut cmd = env.command_bear(); cmd.current_dir(env.test_dir()).env("CC", filename_of(COMPILER_C_PATH)).args([ "--config", config_path.to_str().unwrap(), "--output", "compile_commands.json", "--", SHELL_PATH, script.to_str().unwrap(), ]); let output = cmd.output()?; assert!( output.status.success(), "bear failed on iteration: stdout={}\nstderr={}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); assert!(!env.test_dir().join(".bear").exists(), ".bear/ must be cleaned up after each Bear run"); } // Each run logged a line with the observed directory. Both lines must be // the deterministic `.bear/` form (no random temp dir), confirming the // name is stable across invocations. let observed = env.read_file("bear_dir_observed.txt")?; let lines: Vec<&str> = observed.lines().collect(); assert_eq!(lines.len(), 2, "expected two observation lines, got {observed:?}"); for line in &lines { assert_eq!(*line, ".bear", "wrapper directory must be the deterministic `.bear`, got {line:?}"); } Ok(()) } // Requirements: interception-wrapper-mechanism #[test] #[cfg(target_family = "unix")] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn wrapper_mode_leaves_cargo_variables_alone() -> Result<()> { let env = TestEnvironment::new("wrapper_cargo_vars_untouched")?; // A compiler name as the value: were RUSTC rewritten, it would turn into // the wrapper path. let value = filename_of(COMPILER_C_PATH); let script = env.create_shell_script("build.sh", "printf '%s' \"$RUSTC\" > rustc_seen.txt\n")?; let config = "schema: \"4.2\"\n\nintercept:\n mode: wrapper\n"; let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; let mut cmd = env.command_bear(); cmd.current_dir(env.test_dir()).env("RUSTC", &value).args([ "--config", config_path.to_str().unwrap(), "--output", "compile_commands.json", "--", SHELL_PATH, script.to_str().unwrap(), ]); let output = cmd.output()?; assert!( output.status.success(), "bear failed: stdout={}\nstderr={}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); let sut = env.read_file("rustc_seen.txt")?; assert_eq!(sut, value, "RUSTC must reach the build unchanged"); Ok(()) } /// An explicitly configured compiler list REPLACES PATH-based discovery in /// wrapper mode; it does not extend it. When the user configures at least one /// compiler for interception, exactly those are wrapped and the PATH scan is /// skipped, so a compiler the build finds on PATH but the user did not /// configure is left alone. That is the contract, not an oversight: the /// configured list is how a user narrows interception to the toolchain they /// care about, and a scan that ran anyway would silently widen it again. /// /// One run cannot show this. "Nothing was intercepted" also holds when the /// build script is broken, so the test runs the SAME build script twice and /// varies only the configuration: with no `compilers:` entry the PATH scan /// wraps the compiler and the source appears in the database; with one entry /// naming an unrelated executable the scan is skipped and the source does not /// appear. A failure on the first case means discovery broke; a failure on /// the second means the scan is no longer being skipped, and interception /// now reaches beyond what the user configured. /// /// The build must call the compiler by its bare name, resolved through PATH. /// Compilers named in the compiler environment variables are registered in /// both cases, so a `$CC`-based script would be intercepted in the second run /// too; those variables are stripped from both children for the same reason. // Requirements: interception-wrapper-mechanism #[test] #[cfg(target_family = "unix")] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn wrapper_mode_configured_compilers_replace_path_discovery() -> Result<()> { // Every `program_env` key. Bear registers a wrapper for these regardless // of the configured list, so one left set by the host would wrap the // compiler in the configured run and mask the contrast. const COMPILER_ENV_VARS: [&str; 27] = [ "CC", "CXX", "CPP", "FC", "AR", "AS", "M2C", "PC", "LEX", "YACC", "LINT", "CUDACXX", "CUDAHOSTCXX", "HIPCXX", "HIPHOSTCXX", "ISPC", "OBJC", "OBJCXX", "RC", "SWIFTC", "SYCLCXX", "ASM", "ASM_NASM", "ASM_MASM", "ASM_MARMASM", "ASM_POASM", "ASM-ATT", ]; let env = TestEnvironment::new("wrapper_configured_compilers")?; env.create_source_files(&[("test.c", "int main() { return 0; }")])?; // Bare name, resolved by the shell through PATH -- the only invocation // shape that distinguishes the two configurations. let build = format!("{} -c test.c -o test.o\n", filename_of(COMPILER_C_PATH)); let script = env.create_shell_script("build.sh", &build)?; // A configured entry only has to exist on disk: the validator checks the // path, and configured executables are registered without asking whether // they are compilers. The build never invokes this one; its only job is // to make the configured list non-empty. let decoy = env.create_shell_script("decoy-cc", "exit 1")?; let control_config = r#" schema: "4.2" intercept: mode: wrapper "# .to_string(); let configured_config = format!( r#" schema: "4.2" intercept: mode: wrapper compilers: - path: '{decoy}' "#, decoy = decoy.display() ); let cases = [ ("control: no configured compilers, PATH scan runs", control_config, "control.json", true), ("configured compiler list, PATH scan skipped", configured_config, "configured.json", false), ]; for (case, config, output, expected) in cases { let config_path = env.test_dir().join(format!("{output}.config.yaml")); std::fs::write(&config_path, &config)?; let mut cmd = env.command_bear(); cmd.current_dir(env.test_dir()).args([ "--config", config_path.to_str().unwrap(), "--output", output, "--", SHELL_PATH, script.to_str().unwrap(), ]); for key in COMPILER_ENV_VARS { cmd.env_remove(key); } let run = cmd.output()?; assert!( run.status.success(), "[{case}] bear failed, so neither outcome can be trusted:\nstdout={}\nstderr={}", String::from_utf8_lossy(&run.stdout), String::from_utf8_lossy(&run.stderr) ); let sut = env.load_compilation_database(output)?; let intercepted = sut .entries() .iter() .any(|entry| entry.get("file").and_then(|v| v.as_str()).is_some_and(|f| f.ends_with("test.c"))); assert_eq!( intercepted, expected, "[{case}] expected test.c to be {} in the database. {}\nentries: {:#?}", if expected { "present" } else { "absent" }, if expected { "An empty configured list must fall back to the PATH scan, which wraps the compiler." } else { "A non-empty configured list replaces the PATH scan; wrapping anything else means \ interception now reaches beyond what the user configured." }, sut.entries() ); } Ok(()) } /// Test wrapper-based interception // Requirements: interception-wrapper-mechanism #[test] #[cfg(target_family = "unix")] #[cfg(all(has_executable_compiler_c, has_executable_echo))] fn wrapper_based_interception() -> Result<()> { let env = TestEnvironment::new("wrapper_intercept")?; env.create_source_files(&[("test.c", "int main() { return 0; }")])?; // Create a wrapper script let wrapper_commands = format!( r#"{} "Wrapper called with: $@" exec {} "$@""#, ECHO_PATH, COMPILER_C_PATH ); let wrapper_path = env.create_shell_script("cc-wrapper", &wrapper_commands)?; // Test with wrapper-based interception (when preload isn't available) let _output = env.run_bear_success(&[ "intercept", "--output", "events.json", "--", wrapper_path.to_str().unwrap(), "-c", "test.c", ])?; // Load and verify events let events = env.load_events_file("events.json")?; // Should capture wrapper execution events.assert_min_count(1)?; // Should contain wrapper execution let wrapper_matcher = EventMatcher::new().executable_name("cc-wrapper".to_string()); events.assert_contains(&wrapper_matcher)?; Ok(()) } /// Test Unicode handling in shell commands #[test] #[cfg(has_preload_library)] #[cfg(all(has_executable_shell, has_executable_echo, has_executable_true))] fn unicode_shell_commands() -> Result<()> { let env = TestEnvironment::new("unicode_intercept")?; // Create script with Unicode content let unicode_commands = [ format!("\"{}\" \"Testing Unicode: αβγδε 中文 🚀\"", ECHO_PATH), format!("\"{}\" \"Файл с русскими именами\"", ECHO_PATH), format!("\"{}\"", TRUE_PATH), ] .join("\n"); let script_path = env.create_shell_script("unicode_test.sh", &unicode_commands)?; let output = env.run_bear_success(&[ "intercept", "--output", "events.json", "--", SHELL_PATH, script_path.to_str().unwrap(), ])?; // Should handle Unicode properly assert!(output.stdout().contains("αβγδε")); assert!(output.stdout().contains("中文")); assert!(output.stdout().contains("🚀")); // Load and verify events let events = env.load_events_file("events.json")?; // Events should be captured properly despite Unicode content events.assert_min_count(2)?; Ok(()) } /// Test interception with ISO-8859-2 encoding /// /// This test verifies that Bear can properly intercept commands from shell scripts /// that are encoded in ISO-8859-2 (Latin-2) character encoding, which is commonly /// used in Central and Eastern European languages. /// /// The test: /// 1. Creates a shell script with Polish characters (ąęłńóśźż) that exist in ISO-8859-2 /// 2. Ensures the script file is actually written with ISO-8859-2 encoding (not UTF-8) /// 3. Verifies that Bear can intercept commands from such encoded scripts /// 4. Confirms that the encoding doesn't interfere with event capture /// /// This addresses real-world scenarios where legacy build systems or scripts /// may use non-UTF-8 encodings, particularly in European development environments. #[test] #[cfg(has_preload_library)] #[cfg(all(has_executable_shell, has_executable_echo, has_executable_true))] fn iso8859_2_encoding() -> Result<()> { let env = TestEnvironment::new("iso8859_2")?; // Create script with ISO-8859-2 characters (Polish characters that exist in ISO-8859-2) // These specific characters (ąęłńóśźż) are chosen because they: // - Are valid in ISO-8859-2 encoding // - Would be encoded differently in UTF-8 // - Represent common characters in Polish and other Central European languages let script_commands = [format!("\"{}\" 'Testing ISO-8859-2: ąęłńóśźż'", ECHO_PATH), format!("\"{}\"", TRUE_PATH)] .join("\n"); // Create the script with ISO-8859-2 encoding (not UTF-8) let script_path = env.create_shell_script_with_encoding("iso_test.sh", &script_commands, encoding_rs::ISO_8859_2)?; // Verify the script file is actually encoded in ISO-8859-2 // This is the key improvement: we now actually verify the encoding is correct assert!( env.verify_file_encoding(&script_path, encoding_rs::ISO_8859_2)?, "Script file should be encoded in ISO-8859-2" ); let _output = env.run_bear_success(&[ "intercept", "--output", "events.json", "--", SHELL_PATH, script_path.to_str().unwrap(), ])?; // Load and verify events let events = env.load_events_file("events.json")?; // Should handle encoding properly - Bear should intercept commands successfully // regardless of the script's character encoding events.assert_min_count(2)?; Ok(()) } /// Test Valgrind integration #[test] #[cfg(target_family = "unix")] #[cfg(all(has_executable_valgrind, has_executable_compiler_c))] fn valgrind_integration() -> Result<()> { let env = TestEnvironment::new("valgrind_intercept")?; env.create_source_files(&[("test.c", "int main() { return 0; }")])?; let _output = env.run_bear_success(&[ "intercept", "--output", "events.json", "--", VALGRIND_PATH, "--tool=memcheck", COMPILER_C_PATH, "-c", "test.c", ])?; // Load and verify events let events = env.load_events_file("events.json")?; // Should capture valgrind execution events.assert_min_count(1)?; // Should contain valgrind execution let valgrind_matcher = EventMatcher::new().executable_name("valgrind".to_string()); events.assert_contains(&valgrind_matcher)?; Ok(()) } /// Test fakeroot integration #[test] #[cfg(target_family = "unix")] #[cfg(all(has_executable_fakeroot, has_executable_compiler_c))] fn fakeroot_integration() -> Result<()> { let env = TestEnvironment::new("fakeroot_intercept")?; env.create_source_files(&[("test.c", "int main() { return 0; }")])?; let _output = env.run_bear_success(&[ "intercept", "--output", "events.json", "--", FAKEROOT_PATH, COMPILER_C_PATH, "-c", "test.c", ])?; // Load and verify events let events = env.load_events_file("events.json")?; // Should capture fakeroot execution events.assert_min_count(1)?; // Should contain fakeroot execution let fakeroot_matcher = EventMatcher::new().executable_name("fakeroot".to_string()); events.assert_contains(&fakeroot_matcher)?; Ok(()) } /// Test that wrapper mode resolves bare compiler names from CC env var via PATH. /// /// Covers the PATH resolution part of issue #686: when CC is set to a bare name /// (e.g. "gcc" instead of "/usr/bin/gcc"), Bear's wrapper mode must resolve it /// through PATH before registering wrapper targets. On ccache-equipped hosts /// the bare-name resolution also exercises `interception-wrapper-recursion`, /// since Bear must step past the ccache masquerade directory to reach the /// real compiler. // Requirements: interception-wrapper-mechanism, interception-wrapper-recursion #[test] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn wrapper_mode_resolves_cc_bare_name_via_path() -> Result<()> { let env = TestEnvironment::new("wrapper_cc_bare_name")?; env.create_source_files(&[("test.c", "int main() { return 0; }")])?; let compiler_filename = filename_of(COMPILER_C_PATH); // Build script uses $CC so the wrapper intercepts the call. let build_commands = "$CC -c test.c".to_string(); let script_path = env.create_shell_script("build.sh", &build_commands)?; // Config forces wrapper mode. No `compilers:` entry -- Bear discovers the // compiler via CC (masquerade-aware resolution). let config = r#" schema: "4.2" intercept: mode: wrapper "#; let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; // Run the full bear pipeline with CC set to the bare compiler name (no path). // Bear must resolve "gcc" via PATH before creating wrapper symlinks. let mut cmd = env.command_bear(); cmd.current_dir(env.test_dir()) .env("RUST_LOG", "debug") .env("RUST_BACKTRACE", "1") .env("CC", &compiler_filename) .args([ "--config", config_path.to_str().unwrap(), "--output", "compile_commands.json", "--", SHELL_PATH, script_path.to_str().unwrap(), ]); let output = cmd.output()?; let stderr = String::from_utf8_lossy(&output.stderr); // Verify Bear resolved the bare CC name: the CC env var should be // overridden to point to a wrapper in the .bear directory. Without // PATH resolution, this would fail with "Executable not found". assert!( !stderr.contains("Skipping compiler env var CC="), "Bear should resolve CC={} via PATH, but it was skipped:\n{}", compiler_filename, stderr, ); // Build should succeed and produce a compilation database entry. assert!(output.status.success(), "bear failed:\n{}", stderr); let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; Ok(()) } /// Reporting failures must not affect the build. In wrapper mode the /// collector address reaches the agents through the wrapper configuration /// file (`.bear/wrappers.cfg`), which every wrapper invocation re-reads -- /// so the build script can repoint it at a closed port (port 1 refuses /// connections) before invoking the compiler. The wrapper's report then /// fails, yet the real compiler still runs and produces its object file, /// and Bear's exit code reflects the build alone. /// /// (The preload mechanism carries the same "reporting failures do not /// affect the build" contract, but its endpoint cannot be made unreachable /// end-to-end here: the collector lives inside the Bear process, and the /// preload library actively restores the session state in child /// environments -- see the restoration policy in /// `crates/intercept-preload/src/session.rs`.) // Requirements: interception-wrapper-mechanism #[test] #[cfg(target_family = "unix")] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn wrapper_reporting_failure_does_not_affect_build() -> Result<()> { let env = TestEnvironment::new("wrapper_reporting_failure")?; env.create_source_files(&[("test.c", "int main() { return 0; }")])?; // Rewrite the collector address to a port nothing listens on, then // compile via $CC so the wrapper is exercised with the dead endpoint. // The rewrite handles both loopback forms the collector may bind. let build = r#"sed -e 's/127\.0\.0\.1:[0-9]*/127.0.0.1:1/' -e 's/::1\]:[0-9]*/::1]:1/' .bear/wrappers.cfg > .bear/wrappers.cfg.tmp mv .bear/wrappers.cfg.tmp .bear/wrappers.cfg $CC -c test.c -o test.o "#; let script = env.create_shell_script("build.sh", build)?; let config = r#" schema: "4.2" intercept: mode: wrapper "#; let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; let mut cmd = env.command_bear(); cmd.current_dir(env.test_dir()).env("CC", filename_of(COMPILER_C_PATH)).args([ "--config", config_path.to_str().unwrap(), "--output", "compile_commands.json", "--", SHELL_PATH, script.to_str().unwrap(), ]); let output = cmd.output()?; // The build must succeed end-to-end despite the reporting failure. assert!( output.status.success(), "bear must exit with the build's success despite the unreachable endpoint:\nstdout={}\nstderr={}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); assert!(env.file_exists("test.o"), "the real compiler must have run and produced its object file"); // The dead endpoint was really in effect: the compile was never // reported, so the database holds no entry for it. let db = env.load_compilation_database("compile_commands.json")?; let has_entry = db.entries().iter().any(|entry| entry.get("file").and_then(|v| v.as_str()) == Some("test.c")); assert!(!has_entry, "the compile must not have reached the collector through the dead endpoint"); Ok(()) } /// Test that wrapper mode handles a compiler env var that carries trailing /// flags (e.g. `CC="gcc -std=c11"`). Bear must extract the program token, /// register a wrapper for it, and rewrite the env var so the build command /// still receives the flags. // Requirements: interception-cc-env-var-flags #[test] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn wrapper_mode_handles_cc_with_trailing_flags() -> Result<()> { let env = TestEnvironment::new("wrapper_cc_with_flags")?; env.create_source_files(&[("test.c", "int main() { return 0; }")])?; let compiler_filename = filename_of(COMPILER_C_PATH); // Build script uses `$CC` so the shell word-splits the env var value // into compiler + flags when invoking the wrapper, which is the shape // this test is about. let build_commands = "$CC -c test.c".to_string(); let script_path = env.create_shell_script("build.sh", &build_commands)?; let config = r#" schema: "4.2" intercept: mode: wrapper "#; let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; let cc_value = format!("{} -DBEAR_TEST_FLAG=1", compiler_filename); let mut cmd = env.command_bear(); cmd.current_dir(env.test_dir()) .env("RUST_LOG", "debug") .env("RUST_BACKTRACE", "1") .env("CC", &cc_value) .args([ "--config", config_path.to_str().unwrap(), "--output", "compile_commands.json", "--", SHELL_PATH, script_path.to_str().unwrap(), ]); let output = cmd.output()?; let stderr = String::from_utf8_lossy(&output.stderr); // Bear must not skip CC: the whole point of this requirement is that // values with trailing flags reach wrapper registration. assert!( !stderr.contains("Skipping compiler env var CC="), "Bear should accept CC={:?}, but it was skipped:\n{}", cc_value, stderr, ); assert!(output.status.success(), "bear failed:\n{}", stderr); let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; // The -D flag from the env var must survive interception and appear in // the recorded arguments. let entry = db.entries().first().expect("exactly one entry"); let arguments = entry.get("arguments").and_then(|v| v.as_array()).expect("entry must have arguments array"); let found = arguments.iter().any(|v| v.as_str() == Some("-DBEAR_TEST_FLAG=1")); assert!(found, "arguments must contain the -D flag from CC, got: {:?}", arguments,); Ok(()) } /// An empty and a whitespace-only `CC` are treated the same way: Bear /// warns and leaves the variable alone. // Requirements: interception-cc-env-var-flags #[test] #[cfg(has_executable_shell)] fn wrapper_mode_warns_on_empty_or_whitespace_only_cc() -> Result<()> { for (name, cc_value) in [("wrapper_cc_empty", ""), ("wrapper_cc_whitespace", " ")] { let env = TestEnvironment::new(name)?; let script_path = env.create_shell_script("build.sh", "true")?; let config = r#" schema: "4.2" intercept: mode: wrapper "#; let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; let mut cmd = env.command_bear(); cmd.current_dir(env.test_dir()).env("RUST_LOG", "warn").env("CC", cc_value).args([ "--config", config_path.to_str().unwrap(), "--output", "compile_commands.json", "--", SHELL_PATH, script_path.to_str().unwrap(), ]); let output = cmd.output()?; let stderr = String::from_utf8_lossy(&output.stderr); assert!(output.status.success(), "bear failed for CC={:?}:\n{}", cc_value, stderr); assert!( stderr.contains("Skipping compiler env var CC="), "Bear should warn about CC={:?}, got:\n{}", cc_value, stderr, ); } Ok(()) } /// When a ccache masquerade directory is first on PATH (the default on /// Fedora/Arch/Gentoo and on the CI Ubuntu job after `apt-get install /// ccache`), wrapper mode must resolve past it to the real compiler. Before /// the fix for `interception-wrapper-recursion`, Bear's wrapper config held /// the ccache symlink as the "real compiler"; ccache's PATH search then /// picked up Bear's hard-linked wrapper in `.bear/` and the two looped /// forever. /// /// The test prepends the masquerade dir into its own child PATH so the /// scenario runs regardless of the host's default PATH. Other integration /// tests keep the host's PATH so ccache does not leak into their event /// counts. // Requirements: interception-wrapper-recursion #[test] #[cfg(target_family = "unix")] #[cfg(host_has_ccache_masquerade)] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn wrapper_mode_survives_masquerade_wrapper_in_path() -> Result<()> { let env = TestEnvironment::new("wrapper_ccache_masquerade")?; env.create_source_files(&[("test.c", "int main() { return 0; }")])?; let compiler_filename = filename_of(COMPILER_C_PATH); let build = "$CC -c test.c -o test.o\n".to_string(); let script = env.create_shell_script("build.sh", &build)?; let config = r#" schema: "4.2" intercept: mode: wrapper "#; let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; // Prepend the ccache masquerade directory to the child's PATH so the // recursion scenario is exercised. Without the masquerade fix the // build hangs and the test harness timeout will surface it. let masquerade_dir = env!("CCACHE_MASQUERADE_DIR"); let host_path = std::env::var("PATH").unwrap_or_default(); let child_path = std::env::join_paths( std::iter::once(std::path::PathBuf::from(masquerade_dir)).chain(std::env::split_paths(&host_path)), ) .expect("join PATH failed"); let mut cmd = env.command_bear(); cmd.current_dir(env.test_dir()) .env("RUST_LOG", "debug") .env("RUST_BACKTRACE", "1") .env("CC", &compiler_filename) .env("PATH", &child_path) .args([ "--config", config_path.to_str().unwrap(), "--output", "compile_commands.json", "--", SHELL_PATH, script.to_str().unwrap(), ]); let output = cmd.output()?; let stderr = String::from_utf8_lossy(&output.stderr); assert!(output.status.success(), "bear failed: {stderr}"); let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; // Recorded compiler must be (a) an absolute path, (b) not the ccache // symlink, and (c) not Bear's own wrapper in `.bear/`. `starts_with` on // the exact wrapper dir is the tight check -- a substring match would // false-positive on any path that happens to contain `.bear`. let wrapper_dir = env.test_dir().join(".bear"); for entry in db.entries() { let argv = entry.get("arguments").and_then(|v| v.as_array()); let compiler = argv .and_then(|a| a.first()) .and_then(|v| v.as_str()) .expect("compilation db entry must have argv[0]"); let compiler_path = std::path::Path::new(compiler); assert!(compiler_path.is_absolute(), "compilation db compiler path must be absolute: {compiler}",); assert!(!compiler.contains("ccache"), "compilation db must not reference ccache: {compiler}"); assert!( !compiler_path.starts_with(&wrapper_dir), "compilation db must not reference Bear wrapper at {}: {compiler}", wrapper_dir.display(), ); } Ok(()) } /// Test that wrapper mode handles CC without .exe extension on Windows. /// /// Reproduces the exact user scenario from issue #686: CC=cl (no extension, /// no path) when cl.exe exists on PATH. On Windows, the OS may resolve the /// wrapper executable name with different casing (e.g. "cl.EXE"), so the /// wrapper config lookup must be case-insensitive and extension-agnostic. #[test] #[cfg(target_family = "windows")] #[cfg(all(has_executable_compiler_c, has_executable_shell))] fn wrapper_mode_resolves_cc_without_exe_extension_on_windows() -> Result<()> { let env = TestEnvironment::new("wrapper_cc_no_exe_ext")?; env.create_source_files(&[("test.c", "int main() { return 0; }")])?; let compiler_filename = filename_of(COMPILER_C_PATH); // Strip .exe extension to mimic CC=cl (the exact user scenario) let bare_name = compiler_filename .strip_suffix(".exe") .or_else(|| compiler_filename.strip_suffix(".EXE")) .unwrap_or(&compiler_filename); // Build script uses %CC% on Windows let build_commands = "%CC% -c test.c".to_string(); let script_path = env.test_dir().join("build.bat"); std::fs::write(&script_path, &build_commands)?; let config = r#" schema: "4.2" intercept: mode: wrapper "#; let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; // CC is set to bare name WITHOUT .exe (e.g. "cl" or "gcc") let mut cmd = env.command_bear(); cmd.current_dir(env.test_dir()) .env("RUST_LOG", "debug") .env("RUST_BACKTRACE", "1") .env("CC", bare_name) .args([ "--config", config_path.to_str().unwrap(), "--output", "compile_commands.json", "--", "cmd", "/c", script_path.to_str().unwrap(), ]); let output = cmd.output()?; let stderr = String::from_utf8_lossy(&output.stderr); assert!(output.status.success(), "bear failed:\n{}", stderr); let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; Ok(()) } /// Regression guard for #506: 3.x serialized argv via protobuf, which rejected /// non-UTF-8 bytes and crashed the supervisor. 4.x replaced the wire format /// with JSON; `as_string` in the preload library rejects non-UTF-8 argv and /// silently drops only the offending event (see /// `intercept-preload/src/implementation.rs::report`). This test locks in /// "no crash + the rest of the build is still captured + events file remains /// valid JSON Lines" so a future change cannot reintroduce the pipeline-break. /// /// Gated on `has_preload_library`: the test exercises the preload library's /// UTF-8 fallback path. On macOS the integration build disables preload /// (`is_preload_supported_at_runtime` in `integration-tests/build.rs`), /// and Windows has no preload mechanism at all; both fall back to wrapper /// mode, where argv handling lives in a different code path with its own /// behaviour that is not the contract under test here. // Requirements: interception-preload-mechanism #[test] #[cfg(all(has_preload_library, has_executable_compiler_c, has_executable_shell, has_executable_true))] fn non_utf8_argv_does_not_break_interception() -> Result<()> { let env = TestEnvironment::new("non_utf8_argv")?; env.create_source_files(&[("test.c", "int main() { return 0; }")])?; // Two commands in one build: // 1. /bin/true invoked with an argv slot containing raw non-UTF-8 bytes // (0xC3 0x28 -- a malformed UTF-8 lead-byte sequence). The shell's // command substitution preserves raw bytes verbatim. Whether bear // records this event is an implementation choice; the contract is // that bear must not crash and must not corrupt the events file. // 2. A normal C compile. This event MUST survive: the regression // scenario is a non-UTF-8 byte breaking the whole capture pipeline. let build_commands = [ format!("{} \"$(printf '\\xc3\\x28')\"", TRUE_PATH), format!("{} -c test.c -o test.o", COMPILER_C_PATH), ] .join("\n"); let script_path = env.create_shell_script("build.sh", &build_commands)?; env.run_bear_success(&[ "intercept", "--output", "events.json", "--", SHELL_PATH, script_path.to_str().unwrap(), ])?; // Events file must parse cleanly. `load_events_file` reads via // `read_to_string`, which fails on any non-UTF-8 byte in the file -- // so the parse itself is the regression assertion for "no corrupted // output". let events = env.load_events_file("events.json")?; // The normal compile must still be in the events file. A regression // that crashes the collector on the first non-UTF-8 event would drop // every subsequent event too. let compiler_matcher = event_matcher!(executable_path: COMPILER_C_PATH.to_string()); events.assert_contains(&compiler_matcher)?; Ok(()) } /// `bear intercept` runs the build, so its own stdout shares the stream with /// the events output: `--output -` must be rejected with a clear error /// instead of silently creating a file literally named `-`. // Requirements: interception-events-format #[test] #[cfg(has_executable_true)] fn intercept_output_dash_is_rejected() -> Result<()> { let env = TestEnvironment::new("intercept_output_dash_is_rejected")?; let result = env.run_bear(&["intercept", "--output", "-", "--", TRUE_PATH])?; result.assert_failure()?; assert!( result.stderr().contains("cannot write events to stdout"), "stderr did not explain the rejection: {}", result.stderr() ); assert!(!env.file_exists("-"), "must not create a file literally named `-`"); Ok(()) } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/tests/cases/intercept_posix.rs������������������������������0000664�0000000�0000000�00000064445�15271777203�0026342�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: GPL-3.0-or-later //! POSIX system call interception tests for Bear integration //! //! The idea is that we write C programs which are calling the specific function //! and verify if the interception get these calls recorded. This only works with //! the preload mode, since it does not involve any compiler. //! //! Requirements: interception-preload-mechanism use crate::fixtures::constants::*; use crate::fixtures::infrastructure::*; use anyhow::Result; /// Create a minimal config to enforce the preload mode. /// /// This might be the default mode on many platform, but that's not always the case. /// The preload might work on MacOS, but not set to default. Here we can enforce it. const CONFIG: &str = concat!( r#"schema: '4.2' intercept: mode: preload path: "#, env!("PRELOAD_LIBRARY_PATH"), r#" "# ); /// PATH payload for the empty-environment phases of the PATH-searching /// (`*p`) tests below. With PATH unset, the real exec falls back to libc's /// built-in default search path, which provides no echo in a nix sandbox. #[cfg(has_executable_echo)] fn path_with_echo() -> String { format!("PATH={}", std::path::Path::new(ECHO_PATH).parent().unwrap().display()) } /// Test execl system call interception with multiple arguments /// /// execl(path, arg0, arg1, ..., NULL) - variadic, no PATH search, inherited env #[test] #[cfg(has_symbol_execl)] #[cfg(all(has_executable_compiler_c, has_executable_echo))] fn execl_interception() -> Result<()> { let env = TestEnvironment::new("execl_intercept")?; env.create_config(CONFIG)?; // Test with multiple variadic arguments to catch truncation bugs let c_program = format!( r#"#include <unistd.h> int main() {{ return execl("{}", "{}", "arg1", "arg2", "arg3", (char *)0); }}"#, ECHO_PATH, ECHO_PATH ); env.create_source_files(&[("test_execl.c", &c_program)])?; env.run_c_compiler("test_execl", &["test_execl.c"])?; // Run intercept on the compiled program { env.run_bear_success(&[ "--config", "config.yml", "intercept", "--output", "events.json", "--", "./test_execl", ])?; // Verify intercepted events - check BOTH executable AND arguments let events = env.load_events_file("events.json")?; events.assert_contains(&EventMatcher::new().executable_name("echo").arguments(vec![ ECHO_PATH.to_string(), "arg1".to_string(), "arg2".to_string(), "arg3".to_string(), ]))?; } // Run intercept on the compiled program in empty environment if cfg!(has_symbol_execve) { env.run_bear_success(&[ "--config", "config.yml", "intercept", "--output", "events.json", "--", ENV_PATH, "-", "./test_execl", ])?; // Verify intercepted events - check BOTH executable AND arguments let events = env.load_events_file("events.json")?; events.assert_contains(&EventMatcher::new().executable_name("echo").arguments(vec![ ECHO_PATH.to_string(), "arg1".to_string(), "arg2".to_string(), "arg3".to_string(), ]))?; } Ok(()) } /// Test execlp system call interception (searches PATH) /// /// execlp(file, arg0, arg1, ..., NULL) - variadic, PATH search, inherited env #[test] #[cfg(has_symbol_execlp)] #[cfg(all(has_executable_compiler_c, has_executable_echo))] fn execlp_interception() -> Result<()> { let env = TestEnvironment::new("execlp_intercept")?; env.create_config(CONFIG)?; // execlp searches PATH, so we use just "echo" instead of full path let c_program = r#"#include <unistd.h> int main() { return execlp("echo", "echo", "arg1", "arg2", "arg3", (char *)0); }"#; env.create_source_files(&[("test_execlp.c", c_program)])?; env.run_c_compiler("test_execlp", &["test_execlp.c"])?; { env.run_bear_success(&[ "--config", "config.yml", "intercept", "--output", "events.json", "--", "./test_execlp", ])?; // For execlp, we verify the arguments but not the full path since it's resolved via PATH let events = env.load_events_file("events.json")?; events.assert_contains(&EventMatcher::new().executable_name("echo").arguments(vec![ "echo".to_string(), "arg1".to_string(), "arg2".to_string(), "arg3".to_string(), ]))?; } // Run intercept on the compiled program in empty environment if cfg!(has_symbol_execvpe) { env.run_bear_success(&[ "--config", "config.yml", "intercept", "--output", "events.json", "--", ENV_PATH, "-", &path_with_echo(), "./test_execlp", ])?; // For execlp, we verify the arguments but not the full path since it's resolved via PATH let events = env.load_events_file("events.json")?; events.assert_contains(&EventMatcher::new().executable_name("echo").arguments(vec![ "echo".to_string(), "arg1".to_string(), "arg2".to_string(), "arg3".to_string(), ]))?; } Ok(()) } /// Test execle system call interception (with explicit environment) /// /// execle(path, arg0, arg1, ..., NULL, envp) - variadic, no PATH search, explicit env #[test] #[cfg(has_symbol_execle)] #[cfg(all(has_executable_compiler_c, has_executable_echo))] fn execle_interception() -> Result<()> { let env = TestEnvironment::new("execle_intercept")?; env.create_config(CONFIG)?; // execle takes environment as final argument after NULL let c_program = format!( r#"#include <unistd.h> int main() {{ char *const envp[] = {{ "MY_VAR=test_value", "ANOTHER=123", 0 }}; return execle("{}", "{}", "arg1", "arg2", "arg3", (char *)0, envp); }}"#, ECHO_PATH, ECHO_PATH ); env.create_source_files(&[("test_execle.c", &c_program)])?; env.run_c_compiler("test_execle", &["test_execle.c"])?; { env.run_bear_success(&[ "--config", "config.yml", "intercept", "--output", "events.json", "--", "./test_execle", ])?; // Verify arguments were captured correctly let events = env.load_events_file("events.json")?; events.assert_contains(&EventMatcher::new().executable_name("echo").arguments(vec![ ECHO_PATH.to_string(), "arg1".to_string(), "arg2".to_string(), "arg3".to_string(), ]))?; } // Run intercept on the compiled program in empty environment if cfg!(has_symbol_execve) { env.run_bear_success(&[ "--config", "config.yml", "intercept", "--output", "events.json", "--", ENV_PATH, "-", "./test_execle", ])?; // Verify arguments were captured correctly let events = env.load_events_file("events.json")?; events.assert_contains(&EventMatcher::new().executable_name("echo").arguments(vec![ ECHO_PATH.to_string(), "arg1".to_string(), "arg2".to_string(), "arg3".to_string(), ]))?; } Ok(()) } /// Test execv system call interception /// /// execv(path, argv) - array, no PATH search, inherited env #[test] #[cfg(has_symbol_execv)] #[cfg(all(has_executable_compiler_c, has_executable_echo))] fn execv_interception() -> Result<()> { let env = TestEnvironment::new("execv_intercept")?; env.create_config(CONFIG)?; let c_program = format!( r#"#include <unistd.h> int main() {{ char *const argv[] = {{ "{}", "arg1", "arg2", "arg3", 0 }}; return execv("{}", argv); }}"#, ECHO_PATH, ECHO_PATH ); env.create_source_files(&[("test_execv.c", &c_program)])?; env.run_c_compiler("test_execv", &["test_execv.c"])?; { env.run_bear_success(&[ "--config", "config.yml", "intercept", "--output", "events.json", "--", "./test_execv", ])?; let events = env.load_events_file("events.json")?; events.assert_contains(&EventMatcher::new().executable_name("echo").arguments(vec![ ECHO_PATH.to_string(), "arg1".to_string(), "arg2".to_string(), "arg3".to_string(), ]))?; } // Run intercept on the compiled program in empty environment if cfg!(has_symbol_execve) { env.run_bear_success(&[ "--config", "config.yml", "intercept", "--output", "events.json", "--", ENV_PATH, "-", "./test_execv", ])?; let events = env.load_events_file("events.json")?; events.assert_contains(&EventMatcher::new().executable_name("echo").arguments(vec![ ECHO_PATH.to_string(), "arg1".to_string(), "arg2".to_string(), "arg3".to_string(), ]))?; } Ok(()) } /// Test execve system call interception /// /// execve(path, argv, envp) - array, no PATH search, explicit env #[test] #[cfg(has_symbol_execve)] #[cfg(all(has_executable_compiler_c, has_executable_echo))] fn execve_interception() -> Result<()> { let env = TestEnvironment::new("execve_intercept")?; env.create_config(CONFIG)?; let c_program = format!( r#"#include <unistd.h> int main() {{ char *const argv[] = {{ "{}", "arg1", "arg2", "arg3", 0 }}; char *const envp[] = {{ "TEST_VAR=test_value", 0 }}; return execve("{}", argv, envp); }}"#, ECHO_PATH, ECHO_PATH ); env.create_source_files(&[("test_execve.c", &c_program)])?; env.run_c_compiler("test_execve", &["test_execve.c"])?; { env.run_bear_success(&[ "--config", "config.yml", "intercept", "--output", "events.json", "--", "./test_execve", ])?; let events = env.load_events_file("events.json")?; events.assert_contains(&EventMatcher::new().executable_name("echo").arguments(vec![ ECHO_PATH.to_string(), "arg1".to_string(), "arg2".to_string(), "arg3".to_string(), ]))?; } // Run intercept on the compiled program in empty environment { env.run_bear_success(&[ "--config", "config.yml", "intercept", "--output", "events.json", "--", ENV_PATH, "-", "./test_execve", ])?; let events = env.load_events_file("events.json")?; events.assert_contains(&EventMatcher::new().executable_name("echo").arguments(vec![ ECHO_PATH.to_string(), "arg1".to_string(), "arg2".to_string(), "arg3".to_string(), ]))?; } Ok(()) } /// Test execvp system call interception (searches PATH) /// /// execvp(file, argv) - array, PATH search, inherited env #[test] #[cfg(has_symbol_execvp)] #[cfg(all(has_executable_compiler_c, has_executable_echo))] fn execvp_interception() -> Result<()> { let env = TestEnvironment::new("execvp_intercept")?; env.create_config(CONFIG)?; let c_program = r#"#include <unistd.h> int main() { char *const argv[] = {"echo", "arg1", "arg2", "arg3", 0}; return execvp("echo", argv); }"#; env.create_source_files(&[("test_execvp.c", c_program)])?; env.run_c_compiler("test_execvp", &["test_execvp.c"])?; { env.run_bear_success(&[ "--config", "config.yml", "intercept", "--output", "events.json", "--", "./test_execvp", ])?; let events = env.load_events_file("events.json")?; events.assert_contains(&EventMatcher::new().executable_name("echo").arguments(vec![ "echo".to_string(), "arg1".to_string(), "arg2".to_string(), "arg3".to_string(), ]))?; } // Run intercept on the compiled program in empty environment if cfg!(has_symbol_execvpe) { env.run_bear_success(&[ "--config", "config.yml", "intercept", "--output", "events.json", "--", ENV_PATH, "-", &path_with_echo(), "./test_execvp", ])?; let events = env.load_events_file("events.json")?; events.assert_contains(&EventMatcher::new().executable_name("echo").arguments(vec![ "echo".to_string(), "arg1".to_string(), "arg2".to_string(), "arg3".to_string(), ]))?; } Ok(()) } /// Test execvpe system call (GNU extension - searches PATH with explicit env) /// /// execvpe(file, argv, envp) - array, PATH search, explicit env #[test] #[cfg(has_symbol_execvpe)] #[cfg(all(has_executable_compiler_c, has_executable_echo))] fn execvpe_interception() -> Result<()> { let env = TestEnvironment::new("execvpe_intercept")?; env.create_config(CONFIG)?; // execvpe is a GNU extension, may not be available on all systems let c_program = r#"#define _GNU_SOURCE #include <unistd.h> int main() { char *const argv[] = {"echo", "arg1", "arg2", "arg3", 0}; char *const envp[] = {"TEST=execvpe", 0}; return execvpe("echo", argv, envp); }"#; env.create_source_files(&[("test_execvpe.c", c_program)])?; env.run_c_compiler("test_execvpe", &["test_execvpe.c"])?; { env.run_bear_success(&[ "--config", "config.yml", "intercept", "--output", "events.json", "--", "./test_execvpe", ])?; let events = env.load_events_file("events.json")?; events.assert_contains(&EventMatcher::new().executable_name("echo").arguments(vec![ "echo".to_string(), "arg1".to_string(), "arg2".to_string(), "arg3".to_string(), ]))?; } // Run intercept on the compiled program in empty environment { env.run_bear_success(&[ "--config", "config.yml", "intercept", "--output", "events.json", "--", ENV_PATH, "-", &path_with_echo(), "./test_execvpe", ])?; let events = env.load_events_file("events.json")?; events.assert_contains(&EventMatcher::new().executable_name("echo").arguments(vec![ "echo".to_string(), "arg1".to_string(), "arg2".to_string(), "arg3".to_string(), ]))?; } Ok(()) } /// Test posix_spawn interception #[test] #[cfg(has_symbol_posix_spawn)] #[cfg(all(has_executable_compiler_c, has_executable_echo))] fn posix_spawn_interception() -> Result<()> { let env = TestEnvironment::new("posix_spawn_intercept")?; env.create_config(CONFIG)?; let c_program = format!( r#"#include <spawn.h> #include <sys/wait.h> #include <unistd.h> int main() {{ pid_t pid; char *const argv[] = {{ "{}", "arg1", "arg2", "arg3", 0 }}; char *const envp[] = {{ "SPAWN_TEST=1", 0 }}; int result = posix_spawn(&pid, "{}", NULL, NULL, argv, envp); if (result == 0) {{ int status; waitpid(pid, &status, 0); return WEXITSTATUS(status); }} return result; }}"#, ECHO_PATH, ECHO_PATH ); env.create_source_files(&[("test_posix_spawn.c", &c_program)])?; env.run_c_compiler("test_posix_spawn", &["test_posix_spawn.c"])?; { env.run_bear_success(&[ "--config", "config.yml", "intercept", "--output", "events.json", "--", "./test_posix_spawn", ])?; let events = env.load_events_file("events.json")?; events.assert_contains(&EventMatcher::new().executable_name("echo").arguments(vec![ ECHO_PATH.to_string(), "arg1".to_string(), "arg2".to_string(), "arg3".to_string(), ]))?; } // Run intercept on the compiled program in empty environment { env.run_bear_success(&[ "--config", "config.yml", "intercept", "--output", "events.json", "--", ENV_PATH, "-", "./test_posix_spawn", ])?; let events = env.load_events_file("events.json")?; events.assert_contains(&EventMatcher::new().executable_name("echo").arguments(vec![ ECHO_PATH.to_string(), "arg1".to_string(), "arg2".to_string(), "arg3".to_string(), ]))?; } Ok(()) } /// Test posix_spawnp interception (searches PATH) #[test] #[cfg(has_symbol_posix_spawnp)] #[cfg(all(has_executable_compiler_c, has_executable_echo))] fn posix_spawnp_interception() -> Result<()> { let env = TestEnvironment::new("posix_spawnp_intercept")?; env.create_config(CONFIG)?; let c_program = r#"#include <spawn.h> #include <sys/wait.h> #include <unistd.h> int main() { pid_t pid; char *const argv[] = {"echo", "arg1", "arg2", "arg3", 0}; char *const envp[] = {"TEST=1", 0}; int result = posix_spawnp(&pid, "echo", NULL, NULL, argv, envp); if (result == 0) { int status; waitpid(pid, &status, 0); return WEXITSTATUS(status); } return result; }"#; env.create_source_files(&[("test_posix_spawnp.c", c_program)])?; env.run_c_compiler("test_posix_spawnp", &["test_posix_spawnp.c"])?; { env.run_bear_success(&[ "--config", "config.yml", "intercept", "--output", "events.json", "--", "./test_posix_spawnp", ])?; let events = env.load_events_file("events.json")?; events.assert_contains(&EventMatcher::new().executable_name("echo").arguments(vec![ "echo".to_string(), "arg1".to_string(), "arg2".to_string(), "arg3".to_string(), ]))?; } // Run intercept on the compiled program in empty environment { env.run_bear_success(&[ "--config", "config.yml", "intercept", "--output", "events.json", "--", ENV_PATH, "-", &path_with_echo(), "./test_posix_spawnp", ])?; let events = env.load_events_file("events.json")?; events.assert_contains(&EventMatcher::new().executable_name("echo").arguments(vec![ "echo".to_string(), "arg1".to_string(), "arg2".to_string(), "arg3".to_string(), ]))?; } Ok(()) } /// Test popen system call interception #[test] #[cfg(all(has_symbol_popen, not(target_os = "android")))] #[cfg(all(has_executable_compiler_c, has_executable_cat))] fn popen_interception() -> Result<()> { let env = TestEnvironment::new("popen_intercept")?; env.create_config(CONFIG)?; let c_program = format!( r#"#include <stdio.h> #include <stdlib.h> void write_data(FILE *stream) {{ int i; for (i = 0; i < 10; i++) {{ fprintf(stream, "%d\n", i); }} if (ferror(stream)) {{ fprintf(stderr, "Output to stream failed.\n"); exit(EXIT_FAILURE); }} }} int main(void) {{ FILE *output; output = popen("{}", "w"); if (!output) {{ fprintf(stderr, "Could not run cat.\n"); return EXIT_FAILURE; }} write_data(output); if (pclose(output) != 0) {{ fprintf(stderr, "Could not run cat or other error.\n"); }} return EXIT_SUCCESS; }}"#, CAT_PATH ); env.create_source_files(&[("test_popen.c", &c_program)])?; env.run_c_compiler("test_popen", &["test_popen.c"])?; env.run_bear_success(&[ "--config", "config.yml", "intercept", "--output", "events.json", "--", "./test_popen", ])?; let events = env.load_events_file("events.json")?; // Bear's interposed popen reports the `sh -c` execution itself, so this // holds even where /bin/sh cannot load the preload library (a static // busybox in the nix sandbox) and the grandchild goes unrecorded. events.assert_contains(&EventMatcher::new().executable_name("sh").arguments(vec![ "/bin/sh".to_string(), "-c".to_string(), CAT_PATH.to_string(), ]))?; Ok(()) } /// Test system() call interception #[test] #[cfg(all(has_symbol_system, not(target_os = "android")))] #[cfg(all(has_executable_compiler_c, has_executable_echo))] fn system_interception() -> Result<()> { let env = TestEnvironment::new("system_intercept")?; env.create_config(CONFIG)?; let c_program = format!( r#"#include <stdlib.h> int main() {{ return system("{} arg1 arg2 arg3"); }}"#, ECHO_PATH ); env.create_source_files(&[("test_system.c", &c_program)])?; env.run_c_compiler("test_system", &["test_system.c"])?; env.run_bear_success(&[ "--config", "config.yml", "intercept", "--output", "events.json", "--", "./test_system", ])?; let events = env.load_events_file("events.json")?; // Same contract as popen_interception: assert the `sh -c` execution that // the interposed system() reports, not the grandchild a static /bin/sh // would never let the preload library see. events.assert_contains(&EventMatcher::new().executable_name("sh").arguments(vec![ "/bin/sh".to_string(), "-c".to_string(), format!("{} arg1 arg2 arg3", ECHO_PATH), ]))?; Ok(()) } /// Test errno handling with failed exec calls #[test] #[cfg(has_symbol_execve)] #[cfg(has_executable_compiler_c)] fn test_failed_exec_errno_handling() -> Result<()> { let env = TestEnvironment::new("failed_exec_errno")?; env.create_config(CONFIG)?; let c_program = r#"#include <unistd.h> #include <stdio.h> #include <errno.h> #include <string.h> int main() { char *const argv[] = {"nonexistent_program", 0}; char *const envp[] = {0}; int result = execve("/nonexistent/path/program", argv, envp); // This should only execute if execve fails printf("execve failed: %s\n", strerror(errno)); return result; }"#; env.create_source_files(&[("test_failed_exec.c", c_program)])?; env.run_c_compiler("test_failed_exec", &["test_failed_exec.c"])?; // Run intercept on the compiled program let intercept_output = env.run_bear(&[ "--config", "config.yml", "intercept", "--output", "events.json", "--", "./test_failed_exec", ])?; // The program should fail (non-zero exit) but intercept should still work intercept_output.assert_failure()?; let output = intercept_output.stdout(); assert!(output.contains("execve failed: No such file or directory")); // Should still be able to load events file let events = env.load_events_file("events.json")?; let _ = events.events(); Ok(()) } /// Test that programs with no exec calls don't generate spurious events #[test] #[cfg(has_executable_compiler_c)] fn test_no_exec_calls() -> Result<()> { let env = TestEnvironment::new("no_exec")?; env.create_config(CONFIG)?; let c_program = r#"#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main() { printf("This program does not call any exec functions\n"); printf("Process ID: %d\n", getpid()); return EXIT_SUCCESS; }"#; env.create_source_files(&[("test_no_exec.c", c_program)])?; env.run_c_compiler("test_no_exec", &["test_no_exec.c"])?; env.run_bear_success(&[ "--config", "config.yml", "intercept", "--output", "events.json", "--", "./test_no_exec", ])?; // For programs that don't call exec functions, we expect minimal events. // Note: The test program itself may be captured during startup, so we // just verify we don't see unexpected child process executions. let events = env.load_events_file("events.json")?; let event_count = events.events().len(); // We should have at most 1 event (the test program itself) // and no events for echo, sh, or other child processes assert!( event_count <= 1, "Programs without exec calls should generate at most 1 event (got {})", event_count ); Ok(()) } /// The preload mechanism only works if the dynamic linker can see the /// interposer symbols. rustc adds its own version script to the cdylib /// link, so a linker or build-script change can silently stop exporting /// the C shim's wrappers (see docs/rationale/preload-linker-selection.md). /// Inspect the built library's dynamic symbol table directly. // Requirements: interception-preload-mechanism #[test] #[cfg(all(not(target_os = "macos"), has_executable_nm))] fn preload_library_exports_the_interposer_symbols() -> Result<()> { // arrange: the POSIX-portable interposers plus the version marker; // extension symbols (execvpe, execvP, exect) are host-dependent and // covered by the behavioural tests above. let library = env!("PRELOAD_LIBRARY_PATH"); let expected_symbols = [ "execl", "execle", "execlp", "execv", "execve", "execvp", "posix_spawn", "posix_spawnp", "popen", "pclose", "system", "LIBEXEC_VERSION", ]; // act let output = std::process::Command::new(env!("NM_PATH")) .args(["--dynamic", "--defined-only", library]) .output()?; assert!(output.status.success(), "nm failed on {}: {}", library, String::from_utf8_lossy(&output.stderr)); let sut: Vec<&str> = std::str::from_utf8(&output.stdout)? .lines() .filter_map(|line| line.split_whitespace().last()) .collect(); // assert for symbol in expected_symbols { assert!( sut.contains(&symbol), "symbol {} is missing from the dynamic symbol table of {}", symbol, library ); } Ok(()) } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/tests/cases/mod.rs������������������������������������������0000664�0000000�0000000�00000004130�15271777203�0023663�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: GPL-3.0-or-later //! Integration test cases for Bear //! //! This module contains the actual integration tests that verify Bear's functionality //! across different scenarios and use cases. //! //! ## Platform-Specific Test Behavior //! //! Bear's integration tests are designed to work across different platforms, but some //! tests require specific interception mechanisms: //! //! ### Preload-based Tests (Linux, FreeBSD, NetBSD, OpenBSD, DragonFly) //! - `intercept_posix`: All POSIX system call interception tests (execve, popen, etc.) //! - `config`: Configuration tests that use preload-specific settings //! - Some tests in `intercept`: Tests that require direct system call hooking //! //! ### Wrapper-based Tests (macOS, iOS, Windows, and all other platforms) //! - `wrapper_based_interception`: Tests designed for wrapper-based interception //! - General intercept tests that work with process monitoring //! //! ### Universal Tests (All platforms) //! - `compilation_output`: End-to-end compilation database generation //! - `exit_codes`: Command exit code handling //! - Platform-agnostic functionality tests //! //! Tests use conditional compilation attributes (`#[cfg(target_os = "...")]`) to ensure //! they only run on platforms where the required interception mechanism is available. pub mod compilation_output; pub mod config; pub mod diagnostics; #[cfg(all(has_preload_library, has_executable_compiler_c, has_executable_shell))] pub mod environment_arguments; pub mod exit_codes; #[cfg(has_preload_library)] pub mod hardened_intercept; #[cfg(unix)] pub mod install; pub mod intercept; #[cfg(has_preload_library)] pub mod intercept_posix; // parse-sh parses POSIX `sh` dry-run output; its end-to-end tests feed shell // commands and native compiler paths through that parser, which treats `\` as // an escape. On Windows those paths (e.g. `C:\mingw64\bin\gcc.exe`) mangle, so // the feature and its integration tests are Unix-only. #[cfg(unix)] pub mod parse_sh; #[cfg(all(has_preload_library, has_executable_compiler_c, has_executable_shell))] pub mod response_files; pub mod semantic; ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/tests/cases/parse_sh.rs�������������������������������������0000664�0000000�0000000�00000055771�15271777203�0024731�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: GPL-3.0-or-later //! Integration tests for the `bear parse-sh` subcommand: dry-run shell text //! in, compilation database out. The wiring of the pure lex/interpret stages //! to the semantic analysis and output pipeline (stdin/file input, the //! default and named database destinations, stdout streaming, config, //! append, the `--directory` override, and the exit-code policy). //! //! See `docs/requirements/interception-shell-text-parsing.md` for the //! contract; the crate-level unit tests in //! `crates/bear/src/parse_sh/interpreter.rs` cover the parsing rules //! themselves, so these tests focus on the mode's observable behavior. use crate::fixtures::infrastructure::{CompilationEntryMatcher, compilation_entry}; use crate::fixtures::*; use anyhow::{Context, Result}; use serde_json::{Value, json}; /// A real `make -n` capture of zlib 1.3.1's build (77 lines; 34 compile /// commands among the gcc/ar/mv/mkdir/ln/subshell/redirect noise). See /// `tests/integration/tests/fixtures/data/zlib.make-n.sh`. const ZLIB_SH: &str = include_str!("../fixtures/data/zlib.make-n.sh"); /// The same zlib 1.3.1 build, captured with `make -n -w`: byte-identical to /// `ZLIB_SH` except for a leading `make: Entering directory '/tmp/build'` /// and a trailing `make: Leaving directory '/tmp/build'` marker. const ZLIB_SH_W: &str = include_str!("../fixtures/data/zlib.make-n-w.sh"); // Requirements: interception-shell-text-parsing // // The headline usage: shell text on standard input, nothing else named, // and the database appears under the ecosystem-contracted default name. #[test] #[cfg(has_executable_compiler_c)] fn parse_sh_stdin_default_writes_default_database() -> Result<()> { let env = TestEnvironment::new("parse_sh_stdin_default")?; let temp_dir = env.test_dir().to_str().unwrap(); let result = env.run_bear_with_stdin(&["parse-sh"], b"gcc -c foo.c\n")?; result.assert_success()?; assert!( result.stdout().trim().is_empty(), "the database goes to the file, not stdout: {}", result.stdout() ); let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; db.assert_contains(&compilation_entry!( file: "foo.c".to_string(), directory: temp_dir.to_string(), arguments: vec!["gcc".to_string(), "-c".to_string(), "foo.c".to_string()] ))?; Ok(()) } // Requirements: interception-shell-text-parsing #[test] #[cfg(has_executable_compiler_c)] fn parse_sh_file_input_and_output() -> Result<()> { let env = TestEnvironment::new("parse_sh_file_io")?; env.create_source_files(&[("build.sh", "gcc -c foo.c\n")])?; let result = env.run_bear(&["parse-sh", "-i", "build.sh", "-o", "db.json"])?; result.assert_success()?; assert!(!env.file_exists("compile_commands.json"), "a named output must not also write the default"); let db = env.load_compilation_database("db.json")?; db.assert_count(1)?; db.assert_contains(&CompilationEntryMatcher::new().file("foo.c".to_string()))?; Ok(()) } // Requirements: interception-shell-text-parsing, output-json-compilation-database // // The database itself may stream to standard output: parse-sh runs no // build, so stdout is free. Diagnostics stay on stderr, and no file is // created anywhere. #[test] #[cfg(has_executable_compiler_c)] fn parse_sh_stdout_streaming_emits_database() -> Result<()> { let env = TestEnvironment::new("parse_sh_stdout_streaming")?; let result = env.run_bear_with_stdin(&["parse-sh", "--output", "-"], b"gcc -c foo.c\n")?; result.assert_success()?; let entries: Vec<Value> = serde_json::from_str(&result.stdout()).context("stdout must be a valid JSON compilation database")?; assert_eq!(entries.len(), 1, "expected exactly one compilation entry: {entries:?}"); assert_eq!(entries[0]["file"], "foo.c"); assert_eq!(entries[0]["arguments"], json!(["gcc", "-c", "foo.c"])); assert!(!env.file_exists("-"), "must not create a file literally named `-`"); assert!(!env.file_exists("compile_commands.json"), "streaming must not also write the default file"); Ok(()) } // Requirements: interception-shell-text-parsing, output-json-compilation-database // // A streamed database cannot be combined with appending; the combination // is rejected before any parsing happens. #[test] fn parse_sh_stdout_streaming_rejects_append() -> Result<()> { let env = TestEnvironment::new("parse_sh_stdout_append")?; let result = env.run_bear_with_stdin(&["parse-sh", "--output", "-", "--append"], b"gcc -c foo.c\n")?; result.assert_failure()?; assert!( result.stderr().contains("append"), "stderr must explain the append/stdout conflict: {}", result.stderr() ); Ok(()) } // Requirements: interception-shell-text-parsing, output-append #[test] #[cfg(has_executable_compiler_c)] fn parse_sh_append_merges_databases() -> Result<()> { let env = TestEnvironment::new("parse_sh_append")?; let first = env.run_bear_with_stdin(&["parse-sh"], b"gcc -c a.c\n")?; first.assert_success()?; let second = env.run_bear_with_stdin(&["parse-sh", "--append"], b"gcc -c b.c\n")?; second.assert_success()?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(2)?; db.assert_contains(&CompilationEntryMatcher::new().file("a.c".to_string()))?; db.assert_contains(&CompilationEntryMatcher::new().file("b.c".to_string()))?; Ok(()) } // Requirements: interception-shell-text-parsing // // Configuration applies to parse-sh exactly as to the other // database-producing modes: with `arguments` added to the duplicate key, // the plain and `-fPIC` compiles of each zlib source no longer collapse, // so all 34 compile commands survive as distinct entries (the default // key yields 17; see `parse_sh_zlib_capture_writes_default_deduped_database`). #[test] #[cfg(has_executable_compiler_c)] fn parse_sh_config_widens_duplicate_detection() -> Result<()> { const CONFIG: &str = r#"schema: '4.2' duplicates: match_on: - directory - file - arguments "#; let env = TestEnvironment::new("parse_sh_config_dedup")?; let config_path = env.create_config(CONFIG)?; let config_path = config_path.to_str().context("config path is not valid UTF-8")?; let result = env.run_bear_with_stdin(&["-c", config_path, "parse-sh"], ZLIB_SH.as_bytes())?; result.assert_success()?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(34)?; Ok(()) } // Requirements: interception-shell-text-parsing // // parse-sh now consumes configuration, so a malformed config in a default // search location (`bear.yml` in the working directory) aborts the run // like it aborts every other config-consuming mode. This inverts the // pre-4.2.0 filter design, where parse-sh loaded no config at all. #[test] fn parse_sh_malformed_default_location_config_fails() -> Result<()> { let env = TestEnvironment::new("parse_sh_malformed_default_config")?; // An unclosed flow sequence is a hard YAML parse error. std::fs::write(env.test_dir().join("bear.yml"), "format:\n paths: [unclosed\n")?; let result = env.run_bear_with_stdin(&["parse-sh"], b"gcc -c foo.c\n")?; result.assert_failure()?; assert!( !result.stderr().trim().is_empty(), "the config load failure must be reported on stderr, not silent" ); Ok(()) } // Requirements: interception-shell-text-parsing, cli-exit-codes #[test] fn parse_sh_all_skipped_input_exits_non_zero() -> Result<()> { let env = TestEnvironment::new("parse_sh_all_skipped")?; // A subshell group is outside the supported shell subset: the whole // line must be skipped, not guessed at. let result = env.run_bear_with_stdin(&["parse-sh"], b"(ranlib libz.a || true) >/dev/null 2>&1\n")?; result.assert_failure()?; assert!(result.stderr().contains("skipped"), "stderr must report the skip: {}", result.stderr()); Ok(()) } // Requirements: interception-shell-text-parsing, cli-exit-codes // // A clean parse with no recognized compiler is not an error: non-compiler // commands are valid candidates that simply produce no entries, silently. // This is distinct from the all-skipped failure above, where the parser // could not handle the input at all. Runs at the default log level, where // a nothing-skipped run keeps stderr quiet (the harness's `RUST_LOG=info` // default would surface the zero-skip summary line). #[test] fn parse_sh_non_compiler_only_input_writes_empty_database() -> Result<()> { let env = TestEnvironment::new("parse_sh_non_compiler_only")?; let result = env.run_bear_with_stdin_default_log(&["parse-sh"], b"mv objs/a.o a.lo\n")?; result.assert_success()?; assert!( !result.stderr().contains("skipped"), "a parsed non-compiler line is not a skip and must not be reported as one: {}", result.stderr() ); let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(0)?; Ok(()) } // Requirements: interception-shell-text-parsing // // Skip reports must reach stderr by default, without the caller having to // opt in via `RUST_LOG`. Runs with `RUST_LOG` explicitly unset (the harness's // other helpers default it to `info`, which would hide a regression in // bear-driver's own built-in default log level). #[test] fn parse_sh_default_log_level_reports_skips_without_rust_log() -> Result<()> { let env = TestEnvironment::new("parse_sh_default_log_level")?; let script = b"(subshell command) >/dev/null\ngcc -c foo.c\n"; let result = env.run_bear_with_stdin_default_log(&["parse-sh"], script)?; result.assert_success()?; let stderr = result.stderr(); assert!(stderr.contains("skipped"), "stderr must report the skip without RUST_LOG set: {stderr}"); assert!(stderr.contains("line 1"), "stderr must cite the skipped line number: {stderr}"); assert!(stderr.contains("subshell"), "stderr must name the subshell as the skip reason: {stderr}"); Ok(()) } // Requirements: cli-diagnostic-format // // Without `RUST_LOG`, diagnostics use the UNIX user format: each line is // prefixed with the emitting process (`bear`) and warnings carry a // `warning:` qualifier. The developer tag `bear[<pid>]` must be absent, so // this pins the user view rather than merely "some text on stderr". #[test] fn parse_sh_user_format_prefixes_program_and_severity() -> Result<()> { let env = TestEnvironment::new("parse_sh_user_format")?; let script = b"(subshell command) >/dev/null\ngcc -c foo.c\n"; let result = env.run_bear_with_stdin_default_log(&["parse-sh"], script)?; result.assert_success()?; let stderr = result.stderr(); assert!( stderr.contains("bear: warning:"), "user format prefixes the process and qualifies the severity: {stderr}" ); assert!( !stderr.contains("bear["), "the developer process[pid] tag must not appear without RUST_LOG: {stderr}" ); Ok(()) } // Requirements: cli-diagnostic-format // // With `RUST_LOG` set (the harness defaults it to `info`), diagnostics use // the developer format: every line carries the process identity and pid as // `bear[<pid>]`, distinguishing Bear's processes when they interleave. #[test] fn parse_sh_developer_format_tags_process_identity() -> Result<()> { let env = TestEnvironment::new("parse_sh_developer_format")?; let script = b"(subshell command) >/dev/null\ngcc -c foo.c\n"; let result = env.run_bear_with_stdin(&["parse-sh"], script)?; result.assert_success()?; let stderr = result.stderr(); assert!( stderr.contains("bear["), "developer format tags each line with the process identity and pid: {stderr}" ); assert!( !stderr.contains("bear: warning:"), "the user-format prefix must not appear once RUST_LOG selects the developer view: {stderr}" ); Ok(()) } // Requirements: interception-shell-text-parsing // // When any line is skipped, a stderr summary reports both counts, and the // skip count is physical lines, not command segments: the `for` line below // holds three segments but is one skipped line. The per-line warnings also // say "skipped", so this pins the summary text itself, which no other // assertion covers -- the summary could vanish or regress to counting // segments without any other test failing. #[test] fn parse_sh_skip_summary_counts_lines_not_segments() -> Result<()> { let env = TestEnvironment::new("parse_sh_skip_summary")?; let script = b"gcc -c foo.c\nfor f in *.c; do gcc -c $f; done\n"; let result = env.run_bear_with_stdin(&["parse-sh"], script)?; result.assert_success()?; let stderr = result.stderr(); assert!( stderr.contains("1 command(s) parsed, 1 line(s) skipped"), "the summary must count lines, not segments: {stderr}" ); Ok(()) } // Requirements: interception-shell-text-parsing, output-env-derived-flags // // A leading `VAR=value` assignment is that command's environment, not its // executable, and it participates in semantic analysis like an intercepted // execution's environment: `CPATH` folds into the entry as explicit include // flags (the default; see output-env-derived-flags). The redirection is // stripped from the recorded arguments. #[test] #[cfg(has_executable_compiler_c)] fn parse_sh_leading_assignment_reaches_semantic_as_environment() -> Result<()> { let env = TestEnvironment::new("parse_sh_environment")?; let script = b"CPATH=/opt/include gcc -c foo.c -o foo.o >/dev/null 2>&1\n"; let result = env.run_bear_with_stdin(&["parse-sh"], script)?; result.assert_success()?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; let entry = &db.entries()[0]; let arguments: Vec<&str> = entry["arguments"] .as_array() .context("arguments must be an array")? .iter() .filter_map(Value::as_str) .collect(); assert_eq!(arguments[0], "gcc", "the assignment must not be taken as the executable: {arguments:?}"); let include_at = arguments.iter().position(|arg| *arg == "-I"); assert!( include_at.is_some_and(|at| arguments.get(at + 1) == Some(&"/opt/include")), "CPATH must fold into explicit include flags: {arguments:?}" ); assert!( !arguments.iter().any(|arg| arg.contains('>') || *arg == "/dev/null" || *arg == "2>&1"), "redirection words must not reach the entry: {arguments:?}" ); Ok(()) } // Requirements: interception-shell-text-parsing, cli-exit-codes // // An assignment-only line sets shell state but executes nothing, so it is // neither a command nor a skip; input made only of such lines is the empty // case, not the all-skipped failure. #[test] fn parse_sh_assignment_only_input_exits_zero_as_no_commands() -> Result<()> { let env = TestEnvironment::new("parse_sh_assignment_only")?; let result = env.run_bear_with_stdin(&["parse-sh"], b"FOO=bar\n")?; result.assert_success()?; assert!( result.stderr().contains("no commands found"), "stderr must carry the empty-input notice: {}", result.stderr() ); let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(0)?; Ok(()) } // Requirements: interception-shell-text-parsing, cli-exit-codes #[test] fn parse_sh_non_utf8_input_fails_with_clear_error() -> Result<()> { let env = TestEnvironment::new("parse_sh_non_utf8")?; let result = env.run_bear_with_stdin(&["parse-sh"], b"gcc \xff\xfe -c foo.c\n")?; result.assert_failure()?; assert!(result.stderr().contains("UTF-8"), "stderr must name the encoding problem: {}", result.stderr()); Ok(()) } // Requirements: interception-shell-text-parsing, cli-exit-codes #[test] fn parse_sh_missing_input_file_fails() -> Result<()> { let env = TestEnvironment::new("parse_sh_missing_input")?; let result = env.run_bear(&["parse-sh", "--input", "missing.sh"])?; result.assert_failure()?; assert!( result.stderr().contains("cannot read missing.sh: "), "stderr must name the missing input file: {}", result.stderr() ); Ok(()) } // Requirements: interception-shell-text-parsing, cli-exit-codes #[test] fn parse_sh_unwritable_output_file_fails() -> Result<()> { let env = TestEnvironment::new("parse_sh_unwritable_output")?; let result = env.run_bear_with_stdin(&["parse-sh", "--output", "no_such_dir/db.json"], b"gcc -c foo.c\n")?; result.assert_failure()?; assert!( !result.stderr().trim().is_empty(), "the unwritable output destination must be reported on stderr, not silent" ); Ok(()) } // Requirements: interception-shell-text-parsing, cli-exit-codes #[test] fn parse_sh_empty_input_exits_zero_with_warning() -> Result<()> { let env = TestEnvironment::new("parse_sh_empty_input")?; let result = env.run_bear_with_stdin(&["parse-sh"], b"")?; result.assert_success()?; assert!( result.stderr().contains("no commands found"), "stderr must warn that no commands were found: {}", result.stderr() ); let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(0)?; Ok(()) } // Requirements: interception-shell-text-parsing #[test] #[cfg(has_executable_compiler_c)] fn parse_sh_directory_flag_sets_entry_directory() -> Result<()> { let env = TestEnvironment::new("parse_sh_directory_flag")?; // The default path format records paths as-is without touching the // filesystem, so a directory that only existed where the log was // captured is fine. let result = env.run_bear_with_stdin(&["parse-sh", "--directory", "/custom/build"], b"gcc -c foo.c\n")?; result.assert_success()?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; assert_eq!(db.entries()[0]["directory"], "/custom/build"); Ok(()) } /// The 17 distinct source files the zlib fixture compiles, in the order /// `make -n` first mentions them (the default duplicate key collapses each /// source's plain and `-fPIC` compile into one entry, keeping the first /// occurrence). const ZLIB_EXPECTED_FILES: [&str; 17] = [ "../zlib-1.3.1/test/example.c", "../zlib-1.3.1/adler32.c", "../zlib-1.3.1/crc32.c", "../zlib-1.3.1/deflate.c", "../zlib-1.3.1/infback.c", "../zlib-1.3.1/inffast.c", "../zlib-1.3.1/inflate.c", "../zlib-1.3.1/inftrees.c", "../zlib-1.3.1/trees.c", "../zlib-1.3.1/zutil.c", "../zlib-1.3.1/compress.c", "../zlib-1.3.1/uncompr.c", "../zlib-1.3.1/gzclose.c", "../zlib-1.3.1/gzlib.c", "../zlib-1.3.1/gzread.c", "../zlib-1.3.1/gzwrite.c", "../zlib-1.3.1/test/minigzip.c", ]; /// Asserts that every entry in `db` records the fixture's bare `gcc` token /// verbatim in `arguments[0]`: semantic analysis never rewrites the /// executable, so the spelling from the `make -n` capture survives as-is. fn assert_all_entries_use_gcc(db: &CompilationDatabase) -> Result<()> { for entry in db.entries() { let arg0 = entry["arguments"][0].as_str().with_context(|| format!("entry missing arguments[0]: {entry}"))?; assert_eq!(arg0, "gcc", "arguments[0] must be the observed bare gcc (entry: {entry})"); } Ok(()) } // Requirements: interception-shell-text-parsing // // End-to-end coverage over a REAL `make -n` capture (zlib 1.3.1), in one // step. The default duplicate key (directory+file) collapses each source's // plain and `-fPIC` compile into one entry (first occurrence - the plain // compile - wins), so 34 compile commands over 17 distinct sources yield // 17 database entries; the ar/mv/mkdir/ln noise contributes nothing, and // the unsupported lines are reported as skips without failing the run. #[test] #[cfg(has_executable_compiler_c)] fn parse_sh_zlib_capture_writes_default_deduped_database() -> Result<()> { let env = TestEnvironment::new("parse_sh_zlib_default")?; let result = env.run_bear_with_stdin(&["parse-sh"], ZLIB_SH.as_bytes())?; result.assert_success()?; let stderr = result.stderr(); assert!(stderr.contains("skipped"), "stderr must report the skip: {stderr}"); assert!(stderr.contains("line 18"), "stderr must cite the skipped line number: {stderr}"); assert!(stderr.contains("subshell"), "stderr must name the subshell as the skip reason: {stderr}"); let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(17)?; for file in ZLIB_EXPECTED_FILES { db.assert_contains(&CompilationEntryMatcher::new().file(file.to_string())) .with_context(|| format!("missing expected file entry: {file}"))?; } assert_all_entries_use_gcc(&db)?; Ok(()) } // Requirements: interception-shell-text-parsing // // Same real zlib capture as above, but recorded with `make -n -w`, which // wraps it in a top-level `make: Entering directory '/tmp/build'` / // `Leaving directory '/tmp/build'` pair. Proves real GNU make `-w` directory // markers drive the recorded `directory` end to end, on a real capture: the // markers add no entries of their own (still 17, via the default dedup), but // every entry's `directory` must come from the announced `/tmp/build`. #[test] #[cfg(has_executable_compiler_c)] fn parse_sh_zlib_make_n_w_capture_records_marker_directory() -> Result<()> { let env = TestEnvironment::new("parse_sh_zlib_make_n_w")?; let result = env.run_bear_with_stdin(&["parse-sh"], ZLIB_SH_W.as_bytes())?; result.assert_success()?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(17)?; for entry in db.entries() { assert_eq!( entry["directory"], "/tmp/build", "entry directory must come from the 'Entering directory' marker: {entry}" ); } assert_all_entries_use_gcc(&db)?; Ok(()) } // Requirements: interception-shell-text-parsing // // Synthetic recursive-make directory tracking, end to end: `make[1]: // Entering directory` / `Leaving directory` markers must drive the working // directory, and that working directory must reach each entry's recorded // `directory`. `a.c` is compiled inside the announced subdirectory; `b.c` // is compiled after the matching `Leaving directory`, back in parse-sh's // own cwd (the test dir). #[test] #[cfg(has_executable_compiler_c)] fn parse_sh_recursive_make_markers_drive_entry_directory() -> Result<()> { const SCRIPT: &str = "\ make[1]: Entering directory '/build/sub' gcc -c a.c make[1]: Leaving directory '/build/sub' gcc -c b.c "; let env = TestEnvironment::new("parse_sh_recursive_make_directory")?; let result = env.run_bear_with_stdin(&["parse-sh"], SCRIPT.as_bytes())?; result.assert_success()?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(2)?; let a_entry = db.entries().iter().find(|entry| entry["file"] == "a.c").context("expected an entry for a.c")?; assert_eq!(a_entry["directory"], "/build/sub"); let b_entry = db.entries().iter().find(|entry| entry["file"] == "b.c").context("expected an entry for b.c")?; assert_ne!( b_entry["directory"], "/build/sub", "b.c must be recorded in parse-sh's own cwd, not the entered subdirectory" ); Ok(()) } �������rizsotto-Bear-0c30b27/tests/integration/tests/cases/response_files.rs�������������������������������0000664�0000000�0000000�00000017433�15271777203�0026136�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: GPL-3.0-or-later //! Response-file (`@file`) inlining integration tests. //! //! These exercise the opt-in `format.arguments.from_response_files` behaviour //! end to end: a build that passes flags via an `@file` reference, analysed by //! Bear with inlining enabled or disabled. //! //! The whole module is gated (at its declaration in `cases/mod.rs`) on a //! preload library plus a C compiler and shell, since every test needs all //! three; that keeps the shared helpers below from tripping the dead-code lint //! where the tests are compiled out. use crate::fixtures::constants::*; use crate::fixtures::infrastructure::*; use anyhow::Result; const SRC: &str = "int main() { return 0; }"; /// Minimal config that toggles response-file inlining. fn config(from_response_files: bool) -> String { format!( r#" schema: "4.2" intercept: mode: preload path: "{}" format: paths: directory: as-is file: as-is arguments: from_response_files: {} "#, PRELOAD_LIBRARY_PATH, from_response_files ) } fn run(env: &TestEnvironment, config_yaml: &str, script: &std::path::Path) -> Result<BearOutput> { env.run_bear_success(&[ "--output", "compile_commands.json", "--config", env.create_config(config_yaml)?.to_str().unwrap(), "--", SHELL_PATH, script.to_str().unwrap(), ]) } // Requirements: output-response-file-inlining #[test] fn inlines_response_file_when_enabled() -> Result<()> { let env = TestEnvironment::new("rsp_enabled")?; env.create_source_files(&[("src.c", SRC), ("flags.resp", "-I/opt/include -DEXTRA=2")])?; let build = format!("{} -DBASE=1 @flags.resp -c src.c -o src.o", filename_of(COMPILER_C_PATH)); let script = env.create_shell_script("build.sh", &build)?; run(&env, &config(true), &script)?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; let expected = vec![ COMPILER_C_PATH.to_string(), "-DBASE=1".to_string(), "-I/opt/include".to_string(), "-DEXTRA=2".to_string(), "-c".to_string(), "src.c".to_string(), "-o".to_string(), "src.o".to_string(), ]; db.assert_contains(&CompilationEntryMatcher::new().file("src.c").arguments(expected))?; Ok(()) } // Requirements: output-response-file-inlining #[test] fn keeps_response_file_literal_when_disabled() -> Result<()> { let env = TestEnvironment::new("rsp_disabled")?; env.create_source_files(&[("src.c", SRC), ("flags.resp", "-I/opt/include -DEXTRA=2")])?; let build = format!("{} -DBASE=1 @flags.resp -c src.c -o src.o", filename_of(COMPILER_C_PATH)); let script = env.create_shell_script("build.sh", &build)?; run(&env, &config(false), &script)?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; let expected = vec![ COMPILER_C_PATH.to_string(), "-DBASE=1".to_string(), "@flags.resp".to_string(), "-c".to_string(), "src.c".to_string(), "-o".to_string(), "src.o".to_string(), ]; db.assert_contains(&CompilationEntryMatcher::new().file("src.c").arguments(expected))?; Ok(()) } // Requirements: output-response-file-inlining #[test] fn inlining_splits_quoted_tokens() -> Result<()> { let env = TestEnvironment::new("rsp_quoted")?; env.create_source_files(&[("src.c", SRC), ("flags.resp", "'-DGREETING=hello' -DCOUNT=3")])?; let build = format!("{} @flags.resp -c src.c -o src.o", filename_of(COMPILER_C_PATH)); let script = env.create_shell_script("build.sh", &build)?; run(&env, &config(true), &script)?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; let expected = vec![ COMPILER_C_PATH.to_string(), "-DGREETING=hello".to_string(), "-DCOUNT=3".to_string(), "-c".to_string(), "src.c".to_string(), "-o".to_string(), "src.o".to_string(), ]; db.assert_contains(&CompilationEntryMatcher::new().file("src.c").arguments(expected))?; Ok(()) } // Requirements: output-response-file-inlining #[test] fn inlining_expands_nested_references() -> Result<()> { let env = TestEnvironment::new("rsp_nested")?; env.create_source_files(&[ ("src.c", SRC), ("outer.resp", "-DOUTER @inner.resp"), ("inner.resp", "-DINNER"), ])?; let build = format!("{} @outer.resp -c src.c -o src.o", filename_of(COMPILER_C_PATH)); let script = env.create_shell_script("build.sh", &build)?; run(&env, &config(true), &script)?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; let expected = vec![ COMPILER_C_PATH.to_string(), "-DOUTER".to_string(), "-DINNER".to_string(), "-c".to_string(), "src.c".to_string(), "-o".to_string(), "src.o".to_string(), ]; db.assert_contains(&CompilationEntryMatcher::new().file("src.c").arguments(expected))?; Ok(()) } // Requirements: output-response-file-inlining // // Inlined tokens participate in entry shaping: a response file that names an // additional source makes the database gain an entry for that source, exactly // as if it had been written on the command line. #[test] fn inlining_adds_entry_for_source_named_in_response_file() -> Result<()> { let env = TestEnvironment::new("rsp_extra_source")?; env.create_source_files(&[("src.c", SRC), ("extra.c", SRC), ("srcs.resp", "extra.c")])?; let build = format!("{} -c src.c @srcs.resp", filename_of(COMPILER_C_PATH)); let script = env.create_shell_script("build.sh", &build)?; run(&env, &config(true), &script)?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(2)?; db.assert_contains(&CompilationEntryMatcher::new().file("src.c").arguments(vec![ COMPILER_C_PATH.to_string(), "-c".to_string(), "src.c".to_string(), ]))?; db.assert_contains(&CompilationEntryMatcher::new().file("extra.c").arguments(vec![ COMPILER_C_PATH.to_string(), "-c".to_string(), "extra.c".to_string(), ]))?; Ok(()) } // Requirements: output-response-file-inlining #[test] fn missing_response_file_at_analysis_kept_literal_with_warning() -> Result<()> { // Two-phase flow: intercept while the response file is present (the build // succeeds), then remove it before the separate semantic analysis -- the // stale-build-artefact scenario the requirement describes. let env = TestEnvironment::new("rsp_missing")?; env.create_source_files(&[("src.c", SRC), ("flags.resp", "-DPRESENT=1")])?; let build = format!("{} @flags.resp -c src.c -o src.o", filename_of(COMPILER_C_PATH)); let script = env.create_shell_script("build.sh", &build)?; let config_path = env.create_config(&config(true))?; env.run_bear_success(&["intercept", "-o", "events.json", "--", SHELL_PATH, script.to_str().unwrap()])?; std::fs::remove_file(env.test_dir().join("flags.resp"))?; let output = env.run_bear_success(&[ "--config", config_path.to_str().unwrap(), "semantic", "-i", "events.json", "-o", "compile_commands.json", ])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; let expected = vec![ COMPILER_C_PATH.to_string(), "@flags.resp".to_string(), "-c".to_string(), "src.c".to_string(), "-o".to_string(), "src.o".to_string(), ]; db.assert_contains(&CompilationEntryMatcher::new().file("src.c").arguments(expected))?; let stderr = output.stderr(); anyhow::ensure!( stderr.contains("flags.resp"), "expected a warning naming the missing response file, got stderr: {stderr}" ); Ok(()) } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/tests/cases/semantic.rs�������������������������������������0000664�0000000�0000000�00000317632�15271777203�0024725�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use crate::fixtures::infrastructure::compilation_entry; use crate::fixtures::*; use anyhow::{Context, Result}; use serde_json::{Value, json}; #[test] #[cfg(has_executable_compiler_c)] fn basic_semantic_conversion() -> Result<()> { let env = TestEnvironment::new("basic_semantic")?; let temp_dir = env.test_dir().to_str().unwrap(); // Create sample events file with compilation events using new format // Use proper JSON serialization to handle Windows paths with backslashes let event1 = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "test.c"], "working_dir": temp_dir, "environment": {} }); let event2 = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "test.c"], "working_dir": temp_dir, "environment": {} }); let events_content = format!("{}\n{}", event1, event2); env.create_source_files(&[("events.json", &events_content), ("test.c", "int main() { return 0; }")])?; // Run semantic to convert events to compilation database let _output = env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; // Verify compilation database was created let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; // Verify the compilation entry matches expected format db.assert_contains(&compilation_entry!( file: "test.c".to_string(), directory: temp_dir.to_string(), arguments: vec![COMPILER_C_PATH.to_string(), "-c".to_string(), "test.c".to_string()] ))?; Ok(()) } #[test] #[cfg(all(has_executable_compiler_c, has_executable_compiler_cxx))] fn semantic_multiple_entries() -> Result<()> { let env = TestEnvironment::new("semantic_multiple")?; let temp_dir = env.test_dir().to_str().unwrap(); // Create events file with multiple compilation events using new format let event1 = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "test1.c"], "working_dir": temp_dir, "environment": {} }); let event2 = json!({ "executable": COMPILER_CXX_PATH, "arguments": [COMPILER_CXX_PATH, "-c", "test2.cpp"], "working_dir": temp_dir, "environment": {} }); let event3 = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "test3.c", "-o", "test3.o"], "working_dir": temp_dir, "environment": {} }); let events_content = format!("{}\n{}\n{}", event1, event2, event3); env.create_source_files(&[ ("events.json", &events_content), ("test1.c", "int func1() { return 1; }"), ("test2.cpp", "int func2() { return 2; }"), ("test3.c", "int func3() { return 3; }"), ])?; let _output = env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(3)?; // Verify all compilation entries db.assert_contains(&compilation_entry!( file: "test1.c".to_string(), directory: temp_dir.to_string(), arguments: vec![COMPILER_C_PATH.to_string(), "-c".to_string(), "test1.c".to_string()] ))?; db.assert_contains(&compilation_entry!( file: "test2.cpp".to_string(), directory: temp_dir.to_string(), arguments: vec![COMPILER_CXX_PATH.to_string(), "-c".to_string(), "test2.cpp".to_string()] ))?; db.assert_contains(&compilation_entry!( file: "test3.c".to_string(), directory: temp_dir.to_string(), arguments: vec![COMPILER_C_PATH.to_string(), "-c".to_string(), "test3.c".to_string(), "-o".to_string(), "test3.o".to_string()] ))?; Ok(()) } #[test] #[cfg(has_executable_compiler_c)] fn semantic_format_conversion() -> Result<()> { let env = TestEnvironment::new("semantic_format")?; let temp_dir = env.test_dir().to_str().unwrap(); // Create events with compiler flags let event1 = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "-Wall", "test.c"], "working_dir": temp_dir, "environment": {} }); let events_content = event1.to_string(); env.create_source_files(&[ ("events.json", &events_content), ("test.c", "#include <stdio.h>\nint main() { printf(\"Hello\\n\"); return 0; }"), ])?; let _output = env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; // Verify the compilation entry preserves compiler flags db.assert_contains(&compilation_entry!( file: "test.c".to_string(), directory: temp_dir.to_string(), arguments: vec![ COMPILER_C_PATH.to_string(), "-c".to_string(), "-Wall".to_string(), "test.c".to_string() ] ))?; Ok(()) } #[test] #[cfg(has_executable_compiler_c)] fn semantic_relative_paths() -> Result<()> { let env = TestEnvironment::new("semantic_relative_paths")?; let temp_dir = env.test_dir().to_str().unwrap(); // Create events with relative paths let event1 = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "./src/main.c"], "working_dir": temp_dir, "environment": {} }); let events_content = event1.to_string(); env.create_source_files(&[("events.json", &events_content), ("src/main.c", "int main() { return 0; }")])?; let _output = env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; // Verify relative paths are handled correctly db.assert_contains(&compilation_entry!( file: "./src/main.c".to_string(), directory: temp_dir.to_string(), arguments: vec![ COMPILER_C_PATH.to_string(), "-c".to_string(), "./src/main.c".to_string() ] ))?; Ok(()) } #[test] #[cfg(has_executable_compiler_c)] fn semantic_wrapper_flags() -> Result<()> { let env = TestEnvironment::new("semantic_wrapper")?; let temp_dir = env.test_dir().to_str().unwrap(); // Create events with wrapper that adds flags let event1 = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-DWRAPPER_FLAG", "-c", "test.c"], "working_dir": temp_dir, "environment": {} }); let events_content = event1.to_string(); env.create_source_files(&[ ("events.json", &events_content), ("test.c", "#ifdef WRAPPER_FLAG\nint main() { return 0; }\n#endif"), ])?; let _output = env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; // Verify wrapper flags are preserved db.assert_contains(&compilation_entry!( file: "test.c".to_string(), directory: temp_dir.to_string(), arguments: vec![ COMPILER_C_PATH.to_string(), "-DWRAPPER_FLAG".to_string(), "-c".to_string(), "test.c".to_string() ] ))?; Ok(()) } #[test] #[cfg(has_executable_compiler_c)] fn semantic_clang_plugins() -> Result<()> { let env = TestEnvironment::new("semantic_clang_plugins")?; let temp_dir = env.test_dir().to_str().unwrap(); // Create events with clang plugin flags let event1 = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-fplugin=libexample.so", "-c", "test.c"], "working_dir": temp_dir, "environment": {} }); let events_content = event1.to_string(); env.create_source_files(&[("events.json", &events_content), ("test.c", "int main() { return 0; }")])?; let _output = env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; // Verify plugin flags are preserved db.assert_contains(&compilation_entry!( file: "test.c".to_string(), directory: temp_dir.to_string(), arguments: vec![ COMPILER_C_PATH.to_string(), "-fplugin=libexample.so".to_string(), "-c".to_string(), "test.c".to_string() ] ))?; Ok(()) } #[test] #[cfg(all(has_executable_compiler_c, has_executable_ls))] fn semantic_with_filtering() -> Result<()> { let env = TestEnvironment::new("semantic_filtering")?; let temp_dir = env.test_dir().to_str().unwrap(); // Create events with both compilation and non-compilation commands let event1 = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "test.c"], "working_dir": temp_dir, "environment": {} }); let event2 = json!({ "executable": LS_PATH, "arguments": [LS_PATH, "-la"], "working_dir": temp_dir, "environment": {} }); let event3 = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "test2.c"], "working_dir": temp_dir, "environment": {} }); let events_content = format!("{}\n{}\n{}", event1, event2, event3); env.create_source_files(&[ ("events.json", &events_content), ("test.c", "int main() { return 0; }"), ("test2.c", "int func() { return 1; }"), ])?; let _output = env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; // Should only contain the 2 compilation commands, not the ls command db.assert_count(2)?; // Verify only compilation entries are included db.assert_contains(&compilation_entry!( file: "test.c".to_string(), directory: temp_dir.to_string(), arguments: vec![COMPILER_C_PATH.to_string(), "-c".to_string(), "test.c".to_string()] ))?; db.assert_contains(&compilation_entry!( file: "test2.c".to_string(), directory: temp_dir.to_string(), arguments: vec![COMPILER_C_PATH.to_string(), "-c".to_string(), "test2.c".to_string()] ))?; Ok(()) } // Requirements: cli-exit-codes #[test] fn semantic_empty_events() -> Result<()> { let env = TestEnvironment::new("semantic_empty")?; env.create_source_files(&[("events.json", "")])?; let _output = env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(0)?; Ok(()) } // Requirements: interception-events-format, cli-exit-codes // // Every one of these three lines is rejected (an unterminated object, a // valid-but-incomplete object missing required fields, and another // unterminated object): non-empty input from which nothing parsed must // exit non-zero rather than silently succeed with an empty database. #[test] fn semantic_malformed_events() -> Result<()> { let env = TestEnvironment::new("semantic_malformed")?; env.create_source_files(&[( "events.json", r#"{"invalid": "json" {} {malformed json"#, )])?; let result = env.run_bear(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; result.assert_failure()?; Ok(()) } // Requirements: interception-events-format // // The same set of events in any order must yield a database with the same // set of entries. Three distinct compilations consumed forward and reversed // must produce identical entry sets; only their order in the file may // differ. The bare `gcc` executable with an empty environment keeps the // events host-independent: recognition is by name and the default path // format never touches the filesystem. #[test] fn semantic_database_entry_set_is_order_independent() -> Result<()> { let env = TestEnvironment::new("semantic_order_independent")?; let temp_dir = env.test_dir().to_str().unwrap(); let events: Vec<String> = ["a.c", "b.c", "c.c"] .iter() .map(|source| { json!({ "executable": "gcc", "arguments": ["gcc", "-c", source], "working_dir": temp_dir, "environment": {} }) .to_string() }) .collect(); let forward = events.join("\n") + "\n"; let reverse = events.iter().rev().cloned().collect::<Vec<_>>().join("\n") + "\n"; env.create_source_files(&[("forward.json", &forward), ("reverse.json", &reverse)])?; env.run_bear_success(&["semantic", "--input", "forward.json", "--output", "forward_db.json"])?; env.run_bear_success(&["semantic", "--input", "reverse.json", "--output", "reverse_db.json"])?; let mut sut_forward = env.load_compilation_database("forward_db.json")?.entries().to_vec(); let mut sut_reverse = env.load_compilation_database("reverse_db.json")?.entries().to_vec(); sut_forward.sort_by_key(|entry| entry["file"].as_str().map(String::from)); sut_reverse.sort_by_key(|entry| entry["file"].as_str().map(String::from)); assert_eq!(sut_forward.len(), 3, "each distinct compilation must yield an entry"); assert_eq!(sut_forward, sut_reverse, "entry sets must match regardless of event order"); Ok(()) } // Requirements: output-env-derived-flags, output-json-compilation-database // // MSVC reads `CL` as leading and `_CL_` as trailing options. The leading // words go after the compiler, so `arguments[0]` stays the compiler. A bare // `cl` event is recognized by name, so this runs on any host. #[test] fn semantic_msvc_environment_words_keep_compiler_first() -> Result<()> { let env = TestEnvironment::new("semantic_msvc_env_words")?; let temp_dir = env.test_dir().to_str().unwrap(); let event = json!({ "executable": "cl", "arguments": ["cl", "/c", "foo.c"], "working_dir": temp_dir, "environment": { "CL": "/O2 /W4", "_CL_": "/DX" } }); env.create_source_files(&[("events.json", &format!("{event}\n"))])?; env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let sut = env.load_compilation_database("compile_commands.json")?; sut.assert_count(1)?; sut.assert_contains(&compilation_entry!( file: "foo.c".to_string(), directory: temp_dir.to_string(), arguments: ["cl", "/O2", "/W4", "/c", "foo.c", "/DX"].map(String::from).to_vec() ))?; Ok(()) } // Requirements: output-env-derived-flags // // MSVC splits `CL` with Windows command-line rules, so a backslash in an // unquoted path is kept, the same way the family's response files are read. #[test] fn semantic_msvc_environment_words_keep_backslashes() -> Result<()> { let env = TestEnvironment::new("semantic_msvc_env_backslashes")?; let temp_dir = env.test_dir().to_str().unwrap(); let event = json!({ "executable": "cl", "arguments": ["cl", "/c", "foo.c"], "working_dir": temp_dir, "environment": { "CL": r"/IC:\foo /W4" } }); env.create_source_files(&[("events.json", &format!("{event}\n"))])?; env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let sut = env.load_compilation_database("compile_commands.json")?; sut.assert_count(1)?; sut.assert_contains(&compilation_entry!( file: "foo.c".to_string(), directory: temp_dir.to_string(), arguments: ["cl", r"/IC:\foo", "/W4", "/c", "foo.c"].map(String::from).to_vec() ))?; Ok(()) } // Requirements: output-json-compilation-database // // The compiler is written into `arguments[0]` exactly as the event observed // it: a bare `gcc` stays `gcc`. Semantic analysis never resolves the // executable against PATH (an earlier resolver leaked absolute -- even // ccache-masquerade -- paths into the database and broke clangd's // --query-driver allowlists). The event's PATH names a directory that // really contains a resolvable `gcc`, so this test fails if resolution is // ever reintroduced and leaks into the output. #[test] fn semantic_bare_compiler_name_stays_bare_in_output() -> Result<()> { let env = TestEnvironment::new("semantic_bare_name_stays_bare")?; let temp_dir = env.test_dir().to_str().unwrap(); std::fs::create_dir_all(env.test_dir().join("tools"))?; env.create_build_script("tools/gcc", "#!/bin/sh\nexit 0\n")?; let event = json!({ "executable": "gcc", "arguments": ["gcc", "-c", "test.c"], "working_dir": temp_dir, "environment": { "PATH": env.test_dir().join("tools").to_str().unwrap() } }); env.create_source_files(&[ ("events.json", &event.to_string()), ("test.c", "int main(void) { return 0; }"), ])?; env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; db.assert_contains(&compilation_entry!( file: "test.c".to_string(), directory: temp_dir.to_string(), arguments: vec!["gcc".to_string(), "-c".to_string(), "test.c".to_string()] ))?; Ok(()) } // Requirements: recognition-ambiguous-name-probe // // A masquerade link under an ambiguous name (a `cc` symlink into a ccache // farm) is probed as invoked: ccache passes `--version` through to the // underlying compiler, so the event classifies and yields an entry with // the compiler recorded as observed. The test builds its own masquerade // link from the host's ccache (located via the detected farm), so it does // not depend on which names the host farm carries. #[test] #[cfg(all(unix, host_has_ccache_masquerade))] fn masquerade_cc_event_yields_entry_with_observed_path() -> Result<()> { let env = TestEnvironment::new("masquerade_cc_event")?; let temp_dir = env.test_dir().to_str().unwrap().to_string(); let farm = std::path::Path::new(env!("CCACHE_MASQUERADE_DIR")); let ccache = ["gcc", "cc", "g++", "c++", "clang", "clang++"] .iter() .find_map(|name| std::fs::canonicalize(farm.join(name)).ok()) .context("no masquerade entry found in CCACHE_MASQUERADE_DIR")?; let masq_dir = env.test_dir().join("masq"); std::fs::create_dir_all(&masq_dir)?; std::os::unix::fs::symlink(&ccache, masq_dir.join("cc"))?; let masq_cc = masq_dir.join("cc").to_str().unwrap().to_string(); let event = json!({ "executable": masq_cc, "arguments": [masq_cc, "-c", "hello.c"], "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[ ("events.json", &event.to_string()), ("hello.c", "int main(void) { return 0; }"), ])?; env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; db.assert_contains(&compilation_entry!( file: "hello.c".to_string(), directory: temp_dir.clone(), arguments: vec![masq_cc.clone(), "-c".to_string(), "hello.c".to_string()] ))?; Ok(()) } // Requirements: recognition-compiler-launchers // // A ccache masquerade link under a full compiler name (a `gcc` symlink // into a ccache farm), invoked as `gcc -c main.c`, is treated as that // compiler's compilation: the entry records the compiler as observed (the // symlink path) with the invocation's own arguments. The underlying binary // being ccache must not leak into the entry. Modeled on the ambiguous-name // variant above (`masquerade_cc_event_yields_entry_with_observed_path`); // here the name `gcc` is unambiguous, so recognition is by name alone. #[test] #[cfg(all(unix, host_has_ccache_masquerade))] fn masquerade_gcc_event_yields_entry_with_observed_path() -> Result<()> { let env = TestEnvironment::new("masquerade_gcc_event")?; let temp_dir = env.test_dir().to_str().unwrap().to_string(); let farm = std::path::Path::new(env!("CCACHE_MASQUERADE_DIR")); let ccache = ["gcc", "cc", "g++", "c++", "clang", "clang++"] .iter() .find_map(|name| std::fs::canonicalize(farm.join(name)).ok()) .context("no masquerade entry found in CCACHE_MASQUERADE_DIR")?; let masq_dir = env.test_dir().join("masq"); std::fs::create_dir_all(&masq_dir)?; std::os::unix::fs::symlink(&ccache, masq_dir.join("gcc"))?; let masq_gcc = masq_dir.join("gcc").to_str().unwrap().to_string(); let event = json!({ "executable": masq_gcc, "arguments": [masq_gcc, "-c", "main.c"], "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[ ("events.json", &event.to_string()), ("main.c", "int main(void) { return 0; }"), ])?; env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; db.assert_contains(&compilation_entry!( file: "main.c".to_string(), directory: temp_dir.clone(), arguments: vec![masq_gcc.clone(), "-c".to_string(), "main.c".to_string()] ))?; Ok(()) } // Requirements: output-compilation-entries #[test] #[cfg(all(has_executable_echo, has_executable_mkdir, has_executable_rm))] fn semantic_non_compilation_events() -> Result<()> { let env = TestEnvironment::new("semantic_non_compilation")?; let temp_dir = env.test_dir().to_str().unwrap(); // Create events with only non-compilation commands let event1 = json!({ "executable": ECHO_PATH, "arguments": ["hello"], "working_dir": temp_dir, "environment": {} }); let event2 = json!({ "executable": MKDIR_PATH, "arguments": ["-p", "build"], "working_dir": temp_dir, "environment": {} }); let event3 = json!({ "executable": RM_PATH, "arguments": ["-f", "temp.txt"], "working_dir": temp_dir, "environment": {} }); let events_content = format!("{}\n{}\n{}", event1, event2, event3); env.create_source_files(&[("events.json", &events_content)])?; let _output = env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; // Should contain no entries since none are compilation commands db.assert_count(0)?; Ok(()) } #[test] #[cfg(has_executable_compiler_c)] fn semantic_output_format() -> Result<()> { let env = TestEnvironment::new("semantic_output_format")?; let temp_dir = env.test_dir().to_str().unwrap(); let event1 = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "test.c"], "working_dir": temp_dir, "environment": {} }); let events_content = event1.to_string(); env.create_source_files(&[("events.json", &events_content), ("test.c", "int main() { return 0; }")])?; let _output = env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; // Verify the entry has the expected format with defines db.assert_contains(&compilation_entry!( file: "test.c".to_string(), directory: temp_dir.to_string(), arguments: vec![ COMPILER_C_PATH.to_string(), "-c".to_string(), "test.c".to_string() ] ))?; Ok(()) } /// Regression test: all MSVC per-warning options documented on /// <https://learn.microsoft.com/en-us/cpp/build/reference/compiler-option-warning-level> /// accept their numeric value either glued (`/wd4995`) or separated by whitespace /// (`/wd 4995`). Both forms are emitted by real `cl.exe` invocations and by /// Makefiles in the wild (e.g. `CFLAGS = /wd 4995 /wd 4996 ...`). The separated /// form must survive semantic analysis intact; dropping the number silently would /// corrupt compile_commands.json and break downstream tools such as clangd /// (emits `drv_invalid_int_value` per translation unit). /// /// Covers `/w1`, `/w2`, `/w3`, `/w4` (set warning level for a specific warning) /// and `/wd`, `/we`, `/wo` (disable / as-error / report-once). /// /// This test is platform-independent: it exercises the `semantic` subcommand on /// a hand-crafted events file and does not require a real `cl.exe` to be present. #[test] fn msvc_per_warning_options_preserve_separated_value() -> Result<()> { let env = TestEnvironment::new("msvc_per_warning_options_separated")?; let temp_dir = env.test_dir().to_str().unwrap(); // Use a bare "cl.exe" -- the recognizer matches on the filename stem only, so we // do not need the file to exist on disk. Keeps the test hermetic across platforms. let cl = "cl.exe"; let event = json!({ "executable": cl, "arguments": [ cl, "/w1", "4100", "/w2", "4101", "/w3", "4102", "/w4", "4103", "/wd", "4995", "/we", "4996", "/wo", "4819", "/c", "test.c", ], "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[ ("events.json", &event.to_string()), ("test.c", "int main(void) { return 0; }"), ])?; env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; // Each flag/value pair must round-trip with its numeric value intact. Before // the fix, these flags matched a prefix-only pattern, so the standalone // numeric token following each flag was reclassified as a source file and // dropped from the output. db.assert_contains(&compilation_entry!( file: "test.c".to_string(), directory: temp_dir.to_string(), arguments: vec![ cl.to_string(), "/w1".to_string(), "4100".to_string(), "/w2".to_string(), "4101".to_string(), "/w3".to_string(), "4102".to_string(), "/w4".to_string(), "4103".to_string(), "/wd".to_string(), "4995".to_string(), "/we".to_string(), "4996".to_string(), "/wo".to_string(), "4819".to_string(), "/c".to_string(), "test.c".to_string(), ] ))?; Ok(()) } /// Regression test: `/Wv[:version]` has an optional value (cl uses the current /// compiler version when omitted). Both forms -- bare `/Wv` and `/Wv:17` -- must /// round-trip through semantic analysis without losing tokens or dropping the /// entry. #[test] fn msvc_wv_optional_version_is_preserved() -> Result<()> { let env = TestEnvironment::new("msvc_wv_optional_version")?; let temp_dir = env.test_dir().to_str().unwrap(); let cl = "cl.exe"; // Two translation units, one per /Wv form, so the test exercises both paths // in a single run. let event_bare = json!({ "executable": cl, "arguments": [cl, "/Wv", "/c", "bare.c"], "working_dir": temp_dir, "environment": {} }); let event_with_version = json!({ "executable": cl, "arguments": [cl, "/Wv:17", "/c", "versioned.c"], "working_dir": temp_dir, "environment": {} }); let events = format!("{}\n{}", event_bare, event_with_version); env.create_source_files(&[ ("events.json", &events), ("bare.c", "int main(void) { return 0; }"), ("versioned.c", "int main(void) { return 0; }"), ])?; env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(2)?; db.assert_contains(&compilation_entry!( file: "bare.c".to_string(), directory: temp_dir.to_string(), arguments: vec![cl.to_string(), "/Wv".to_string(), "/c".to_string(), "bare.c".to_string()] ))?; db.assert_contains(&compilation_entry!( file: "versioned.c".to_string(), directory: temp_dir.to_string(), arguments: vec![cl.to_string(), "/Wv:17".to_string(), "/c".to_string(), "versioned.c".to_string()] ))?; Ok(()) } /// Companion to `msvc_per_warning_options_preserve_separated_value`: the fix /// switched /wd, /we, /wo from `FlagPattern::Prefix` to /// `FlagPattern::ExactlyWithGluedOrSep`, so the glued path now runs through /// different generated code than before. Lock the glued form behavior so a /// future refactor of the pattern types cannot silently regress the common /// cl.exe spelling (/wd4995, /w34326). #[test] fn msvc_per_warning_options_preserve_glued_value() -> Result<()> { let env = TestEnvironment::new("msvc_per_warning_options_glued")?; let temp_dir = env.test_dir().to_str().unwrap(); let cl = "cl.exe"; let event = json!({ "executable": cl, "arguments": [ cl, "/w14100", "/w24101", "/w34102", "/w44103", "/wd4995", "/we4996", "/wo4819", "/c", "test.c", ], "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[ ("events.json", &event.to_string()), ("test.c", "int main(void) { return 0; }"), ])?; env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; db.assert_contains(&compilation_entry!( file: "test.c".to_string(), directory: temp_dir.to_string(), arguments: vec![ cl.to_string(), "/w14100".to_string(), "/w24101".to_string(), "/w34102".to_string(), "/w44103".to_string(), "/wd4995".to_string(), "/we4996".to_string(), "/wo4819".to_string(), "/c".to_string(), "test.c".to_string(), ] ))?; Ok(()) } /// `clang_cl.yaml` inherits the per-warning rules via `extends: msvc`. The /// bear-codegen snapshot proves the generated flag array is correct, but does /// not exercise the runtime matcher. This test drives the `semantic` subcommand /// with a clang-cl executable and a mix of glued / separated / colon forms to /// confirm the inheritance is effective end-to-end. #[test] fn clang_cl_inherits_msvc_per_warning_options() -> Result<()> { let env = TestEnvironment::new("clang_cl_inherits_msvc_per_warning")?; let temp_dir = env.test_dir().to_str().unwrap(); let cl = "clang-cl.exe"; let event = json!({ "executable": cl, "arguments": [ cl, "/wd4995", "/we", "4996", "/w3", "4102", "/w44103", "/Wv:17", "/c", "test.c", ], "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[ ("events.json", &event.to_string()), ("test.c", "int main(void) { return 0; }"), ])?; env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; db.assert_contains(&compilation_entry!( file: "test.c".to_string(), directory: temp_dir.to_string(), arguments: vec![ cl.to_string(), "/wd4995".to_string(), "/we".to_string(), "4996".to_string(), "/w3".to_string(), "4102".to_string(), "/w44103".to_string(), "/Wv:17".to_string(), "/c".to_string(), "test.c".to_string(), ] ))?; Ok(()) } /// The `format:` config that turns on the optional per-entry `output` field /// and keeps every path exactly as the build spelled it, so an MSVC-style /// Windows path survives a Unix test host unchanged. fn output_field_config() -> &'static str { r#" schema: "4.2" format: paths: directory: as-is file: as-is entries: use_array_format: true include_output_field: true "# } // Requirements: output-compilation-entries // // Regression test: the MSVC `/F`-family options spell their value glued to the // option (`/FoCMakeFiles\x.dir\a.cpp.obj`), which is what CMake's // `Modules/Platform/Windows-MSVC.cmake` compile template and Meson both emit. // Bear used to spell these as "value after a colon, or a separate argument", // so a glued `/Fo` matched neither; it fell through to `/F` (the stack-size // option, classified as a link-stage flag) and was stripped from the entry. // The object path then vanished from `arguments` and no `output` field was // produced. The command line below is a real one, from a CMake + MSVC 2022 // build of jackhumbert/scc_mod. #[test] fn msvc_glued_output_flags_survive_and_set_output_field() -> Result<()> { // arrange let env = TestEnvironment::new("msvc_glued_output_flags")?; let temp_dir = env.test_dir().to_str().unwrap(); let cl = "cl.exe"; let object = r"CMakeFiles\Detours.dir\deps\detours\src\image.cpp.obj"; let pdb_dir = r"/FdCMakeFiles\Detours.dir\"; let source = r"D:\Code\scc_mod\deps\detours\src\image.cpp"; let event = json!({ "executable": cl, "arguments": [ cl, "/nologo", "/TP", "-DUNICODE", r"-ID:\Code\scc_mod\deps\detours\src", "/DWIN32", "/EHsc", "/Zi", "/Od", "-std:c++20", "-MDd", "/Zc:__cplusplus", format!("/Fo{}", object), pdb_dir, "/FS", "-c", source, ], "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[("events.json", &event.to_string())])?; let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, output_field_config())?; // act env.run_bear_success(&[ "--config", config_path.to_str().unwrap(), "semantic", "--input", "events.json", "--output", "compile_commands.json", ])?; // assert let sut = env.load_compilation_database("compile_commands.json")?; sut.assert_count(1)?; // `/Fo` is an output flag and keeps the glued spelling the build used; // `/Fd` is an ordinary compile-stage flag and keeps its glued spelling too. sut.assert_contains(&compilation_entry!( file: source.to_string(), directory: temp_dir.to_string(), arguments: vec![ cl.to_string(), "/nologo".to_string(), "/TP".to_string(), "-DUNICODE".to_string(), r"-ID:\Code\scc_mod\deps\detours\src".to_string(), "/DWIN32".to_string(), "/EHsc".to_string(), "/Zi".to_string(), "/Od".to_string(), "-std:c++20".to_string(), "-MDd".to_string(), "/Zc:__cplusplus".to_string(), format!("/Fo{}", object), pdb_dir.to_string(), "/FS".to_string(), "-c".to_string(), source.to_string(), ] ))?; let entry = &sut.entries()[0]; assert_eq!( entry.get("output").and_then(Value::as_str), Some(object), "the glued /Fo value must become the entry's output field, got {:?}", entry.get("output") ); Ok(()) } // Requirements: output-compilation-entries // // Regression test: `/Yc` creates a precompiled header and its filename is // optional (`/Yc` or `/Ycstdafx.h`). Bear used to accept the value as a // separate argument, so a bare `/Yc` swallowed the source file that followed // it. The invocation then had no recognized source and produced an EMPTY // database instead of one entry. #[test] fn msvc_bare_precompiled_header_flag_does_not_consume_the_source() -> Result<()> { // arrange let env = TestEnvironment::new("msvc_bare_pch_flag")?; let temp_dir = env.test_dir().to_str().unwrap(); let cl = "cl.exe"; // Both /Yc and /Yu have an optional filename; cover the bare spelling of // each, plus the glued spelling that must keep its exact token. let event_create = json!({ "executable": cl, "arguments": [cl, "/c", "/Yc", "create.cpp"], "working_dir": temp_dir, "environment": {} }); let event_use = json!({ "executable": cl, "arguments": [cl, "/c", "/Yu", "/Fpstdafx.pch", "use.cpp"], "working_dir": temp_dir, "environment": {} }); let event_glued = json!({ "executable": cl, "arguments": [cl, "/c", "/Ycstdafx.h", "glued.cpp"], "working_dir": temp_dir, "environment": {} }); let events = format!("{}\n{}\n{}", event_create, event_use, event_glued); env.create_source_files(&[ ("events.json", &events), ("create.cpp", "int f(void) { return 0; }"), ("use.cpp", "int g(void) { return 0; }"), ("glued.cpp", "int h(void) { return 0; }"), ])?; // act env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; // assert let sut = env.load_compilation_database("compile_commands.json")?; sut.assert_count(3)?; sut.assert_contains(&compilation_entry!( file: "create.cpp".to_string(), directory: temp_dir.to_string(), arguments: vec![cl.to_string(), "/c".to_string(), "/Yc".to_string(), "create.cpp".to_string()] ))?; sut.assert_contains(&compilation_entry!( file: "use.cpp".to_string(), directory: temp_dir.to_string(), arguments: vec![ cl.to_string(), "/c".to_string(), "/Yu".to_string(), "/Fpstdafx.pch".to_string(), "use.cpp".to_string(), ] ))?; sut.assert_contains(&compilation_entry!( file: "glued.cpp".to_string(), directory: temp_dir.to_string(), arguments: vec![ cl.to_string(), "/c".to_string(), "/Ycstdafx.h".to_string(), "glued.cpp".to_string(), ] ))?; Ok(()) } // Requirements: output-compilation-entries // // Regression test: CMake's MSVC assembly rule is // `cl /FoNUL /FAs /Fa<listing> ... -c <source>`. `/FA[c][s][u]` selects the // listing contents and takes no path at all, and had no rule of its own, so // `/FAs` fell through to `/F` and was stripped along with `/FoNUL` and the // glued `/Fa`. All three must survive, and the match must be case sensitive // so that `/FAs` and `/Fa<path>` stay distinct options. #[test] fn msvc_assembly_listing_flags_survive() -> Result<()> { // arrange let env = TestEnvironment::new("msvc_assembly_listing_flags")?; let temp_dir = env.test_dir().to_str().unwrap(); let cl = "cl.exe"; let event = json!({ "executable": cl, "arguments": [cl, "/nologo", "/FoNUL", "/FAs", r"/FaCMakeFiles\x.dir\a.cpp.s", "-c", "a.cpp"], "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[("events.json", &event.to_string()), ("a.cpp", "int main() { return 0; }")])?; // act env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; // assert let sut = env.load_compilation_database("compile_commands.json")?; sut.assert_count(1)?; sut.assert_contains(&compilation_entry!( file: "a.cpp".to_string(), directory: temp_dir.to_string(), arguments: vec![ cl.to_string(), "/nologo".to_string(), "/FoNUL".to_string(), "/FAs".to_string(), r"/FaCMakeFiles\x.dir\a.cpp.s".to_string(), "-c".to_string(), "a.cpp".to_string(), ] ))?; Ok(()) } // Requirements: output-compilation-entries // // MSVC writes an output option four ways, and every one has to survive into // the entry as the build spelled it, with the path reaching the output field: // // /Fo no value, so it names no file // /Fomain.obj glued, the spelling CMake and Meson emit // /Fo:main.obj after a colon // /Fo: main.obj colon, value in the next argument // // The bare form is the one that has to name no output at all. Reading it as // an output with an empty path made the entry claim the working directory // once paths were resolved, and the separated colon form used to lose its // value entirely: unclaimed, it was read as an object file input and dropped. #[test] fn msvc_output_flag_spellings_are_recorded_as_written() -> Result<()> { // arrange let env = TestEnvironment::new("msvc_output_spellings")?; let temp_dir = env.test_dir().to_str().unwrap(); let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, output_field_config())?; let cases: Vec<(Vec<&str>, Option<&str>)> = vec![ (vec!["/c", "/Fo", "main.cpp"], None), (vec!["/c", "/Fomain.obj", "main.cpp"], Some("main.obj")), (vec!["/c", "/Fo:main.obj", "main.cpp"], Some("main.obj")), (vec!["/c", "/Fo:", "main.obj", "main.cpp"], Some("main.obj")), (vec!["/Fe", "main.cpp"], None), (vec!["/Feapp.exe", "main.cpp"], Some("app.exe")), ]; for (arguments, expected_output) in cases { let mut argv = vec!["cl.exe"]; argv.extend(arguments.iter().copied()); let event = json!({ "executable": "cl.exe", "arguments": argv, "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[ ("events.json", &event.to_string()), ("main.cpp", "int main() { return 0; }"), ])?; // act env.run_bear_success(&[ "--config", config_path.to_str().unwrap(), "semantic", "--input", "events.json", "--output", "compile_commands.json", ])?; // assert let sut = env.load_compilation_database("compile_commands.json")?; sut.assert_count(1).with_context(|| format!("case {arguments:?}"))?; sut.assert_contains(&compilation_entry!( file: "main.cpp".to_string(), directory: temp_dir.to_string(), arguments: argv.iter().map(|a| a.to_string()).collect::<Vec<_>>() )) .with_context(|| format!("case {arguments:?} must round-trip its arguments"))?; let entry = &sut.entries()[0]; assert_eq!( entry.get("output").and_then(Value::as_str), expected_output, "case {arguments:?} output field" ); } Ok(()) } // Requirements: output-compilation-entries // // `/F` sets the stack size and spells its value as a separate argument // (`/F 1024`), which is a link-stage setting and is therefore stripped from a // compile entry together with its value. It must not double as a prefix for // the glued `/F`-family options: `/Fofoo.obj` is an object file name, not a // stack size, and must survive. #[test] fn msvc_stack_size_option_takes_a_separate_value() -> Result<()> { // arrange let env = TestEnvironment::new("msvc_stack_size_option")?; let temp_dir = env.test_dir().to_str().unwrap(); let cl = "cl.exe"; let event = json!({ "executable": cl, "arguments": [cl, "/F", "1024", "/Fofoo.obj", "/c", "a.cpp"], "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[("events.json", &event.to_string()), ("a.cpp", "int main() { return 0; }")])?; // act env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; // assert let sut = env.load_compilation_database("compile_commands.json")?; sut.assert_count(1)?; // `/F 1024` is consumed as one link-stage option (both tokens gone), while // `/Fofoo.obj` is recognized as the object file name and kept. sut.assert_contains(&compilation_entry!( file: "a.cpp".to_string(), directory: temp_dir.to_string(), arguments: vec![ cl.to_string(), "/Fofoo.obj".to_string(), "/c".to_string(), "a.cpp".to_string(), ] ))?; Ok(()) } // Requirements: output-compilation-entries, recognition-compiler-names // // `clang_cl.yaml` inherits the `/F`-family and precompiled-header rules via // `extends: msvc`, and clang-cl accepts the same glued spellings. Drive the // runtime matcher through a clang-cl invocation to confirm the fix reaches // the inheriting family end-to-end, not just the snapshot of the generated // flag array. #[test] fn clang_cl_inherits_msvc_glued_output_flags() -> Result<()> { // arrange let env = TestEnvironment::new("clang_cl_glued_output_flags")?; let temp_dir = env.test_dir().to_str().unwrap(); let cl = "clang-cl.exe"; let object = "CMakeFiles/x.dir/a.cpp.obj"; let event = json!({ "executable": cl, "arguments": [ cl, "/nologo", format!("/Fo{}", object), "/FdCMakeFiles/x.dir/", "/Yc", "-c", "a.cpp", ], "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[("events.json", &event.to_string()), ("a.cpp", "int main() { return 0; }")])?; let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, output_field_config())?; // act env.run_bear_success(&[ "--config", config_path.to_str().unwrap(), "semantic", "--input", "events.json", "--output", "compile_commands.json", ])?; // assert let sut = env.load_compilation_database("compile_commands.json")?; sut.assert_count(1)?; sut.assert_contains(&compilation_entry!( file: "a.cpp".to_string(), directory: temp_dir.to_string(), arguments: vec![ cl.to_string(), "/nologo".to_string(), format!("/Fo{}", object), "/FdCMakeFiles/x.dir/".to_string(), "/Yc".to_string(), "-c".to_string(), "a.cpp".to_string(), ] ))?; let entry = &sut.entries()[0]; assert_eq!( entry.get("output").and_then(Value::as_str), Some(object), "clang-cl must inherit the MSVC /Fo rule, got output {:?}", entry.get("output") ); Ok(()) } // Requirements: output-compilation-entries // // An output flag and its value keep the joining the build used. Re-spelling // one form as another produces an entry the originating compiler rejects: // `cl.exe` documents that `/Fo` takes no space before its value, and TI's // long options glue their value after `=`. Only the separate spelling may // re-emit as two tokens. The `output` field carries the bare path either way. #[test] fn output_flag_keeps_the_spelling_the_build_used() -> Result<()> { // arrange let env = TestEnvironment::new("output_flag_spelling")?; let temp_dir = env.test_dir().to_str().unwrap(); // (compiler, arguments as invoked, source, expected output field). The // recorded arguments must equal the invoked ones: this whole test is that // the argument list is a faithful round trip. let cases: Vec<(&str, Vec<&str>, &str, &str)> = vec![ ("gcc", vec!["gcc", "-c", "-oglued.o", "glued.c"], "glued.c", "glued.o"), ("gcc", vec!["gcc", "-c", "-o", "separate.o", "separate.c"], "separate.c", "separate.o"), ("cl.exe", vec!["cl.exe", "/c", "/Fomsvcglued.obj", "msvcglued.c"], "msvcglued.c", "msvcglued.obj"), ("cl.exe", vec!["cl.exe", "/c", "/Fo:msvccolon.obj", "msvccolon.c"], "msvccolon.c", "msvccolon.obj"), ("armcl", vec!["armcl", "--output_file=ti.obj", "ti.c"], "ti.c", "ti.obj"), ]; let events = cases .iter() .map(|(compiler, arguments, _, _)| { json!({ "executable": compiler, "arguments": arguments, "working_dir": temp_dir, "environment": {} }) .to_string() }) .collect::<Vec<_>>() .join("\n"); env.create_source_files(&[("events.json", &events)])?; let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, output_field_config())?; // act env.run_bear_success(&[ "--config", config_path.to_str().unwrap(), "semantic", "--input", "events.json", "--output", "compile_commands.json", ])?; // assert let sut = env.load_compilation_database("compile_commands.json")?; sut.assert_count(cases.len())?; for (_, arguments, source, output) in &cases { sut.assert_contains(&compilation_entry!( file: source.to_string(), directory: temp_dir.to_string(), arguments: arguments.iter().map(|token| token.to_string()).collect::<Vec<_>>() )) .with_context(|| format!("invocation `{}`", arguments.join(" ")))?; // The `output` field is the bare path, never the joined token. let entry = sut .entries() .iter() .find(|entry| entry.get("file").and_then(Value::as_str) == Some(*source)) .with_context(|| format!("no entry for {source}"))?; assert_eq!( entry.get("output").and_then(Value::as_str), Some(*output), "the output field must carry the bare path for {source}, got {:?}", entry.get("output") ); } Ok(()) } // Requirements: output-compilation-entries // // A configured path format must reach the path *inside* a glued output token. // The flag and its value share one token (`-oglued.o`), so the token cannot be // treated as a path wholesale: doing so rewrites `-oglued.o` into // `<dir>/-oglued.o`, a nonsense argument. The flag spelling stays untouched // and only the path part is resolved. #[test] fn glued_output_path_is_rewritten_inside_the_token() -> Result<()> { // arrange let env = TestEnvironment::new("glued_output_path_format")?; let temp_dir = env.test_dir().to_str().unwrap(); let event = json!({ "executable": "gcc", "arguments": ["gcc", "-c", "-oglued.o", "glued.c"], "working_dir": temp_dir, "environment": {} }); let config = r#" schema: "4.2" format: paths: directory: absolute file: absolute entries: use_array_format: true include_output_field: true "#; env.create_source_files(&[("events.json", &event.to_string()), ("glued.c", "int main() { return 0; }")])?; let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; // act env.run_bear_success(&[ "--config", config_path.to_str().unwrap(), "semantic", "--input", "events.json", "--output", "compile_commands.json", ])?; // assert let sut = env.load_compilation_database("compile_commands.json")?; sut.assert_count(1)?; let source = env.test_dir().join("glued.c"); let object = env.test_dir().join("glued.o"); sut.assert_contains(&compilation_entry!( file: source.to_str().unwrap().to_string(), directory: temp_dir.to_string(), arguments: vec![ "gcc".to_string(), "-c".to_string(), format!("-o{}", object.display()), source.to_str().unwrap().to_string(), ] ))?; Ok(()) } // Requirements: output-path-format // // The configured `file` strategy reaches the source and the output path where // they reappear in `arguments`, so an entry cannot disagree with itself about // a path a consumer reads from either place. A path that is the payload of // some other flag (`-I../include`) is not individually addressable and is // carried through exactly as the build spelled it. #[test] fn configured_file_format_reaches_the_argument_paths_but_not_flag_payloads() -> Result<()> { // arrange let env = TestEnvironment::new("argument_path_format")?; let temp_dir = env.test_dir().to_str().unwrap(); let event = json!({ "executable": "gcc", "arguments": ["gcc", "-c", "-I../include", "src/main.c", "-o", "obj/main.o"], "working_dir": temp_dir, "environment": {} }); let config = r#" schema: "4.2" format: paths: directory: absolute file: absolute entries: use_array_format: true include_output_field: true "#; env.create_source_files(&[ ("events.json", &event.to_string()), ("src/main.c", "int main() { return 0; }"), ])?; let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; // act env.run_bear_success(&[ "--config", config_path.to_str().unwrap(), "semantic", "--input", "events.json", "--output", "compile_commands.json", ])?; // assert let sut = env.load_compilation_database("compile_commands.json")?; sut.assert_count(1)?; // Spell the expectations the way the code does, with `Path::join` and never // a literal separator, so the assertion holds on Windows too. let source = env.test_dir().join("src").join("main.c"); let object = env.test_dir().join("obj").join("main.o"); sut.assert_contains( &CompilationEntryMatcher::new() .file(source.to_str().unwrap()) .directory(temp_dir) .output(object.to_str().unwrap()) .arguments(vec![ "gcc".to_string(), "-c".to_string(), "-I../include".to_string(), source.to_str().unwrap().to_string(), "-o".to_string(), object.to_str().unwrap().to_string(), ]), )?; Ok(()) } // Requirements: output-header-entries // // A synthesized header entry clones the donor's arguments with the output // flag removed. That removal must recognize the glued spelling too: a // leftover `-omain.o` in a header entry names an object the header does not // produce. #[test] fn synthesized_header_entry_drops_a_glued_output_flag() -> Result<()> { // arrange let env = TestEnvironment::new("header_entry_glued_output")?; let temp_dir = env.test_dir().to_str().unwrap(); let event = json!({ "executable": "gcc", "arguments": ["gcc", "-c", "-omain.o", "main.c"], "working_dir": temp_dir, "environment": {} }); let config = r#" schema: "4.2" headers: enabled: true strategy: siblings format: paths: directory: as-is file: as-is "#; env.create_source_files(&[ ("events.json", &event.to_string()), ("main.c", "int main() { return 0; }"), ("util.h", "int util(void);"), ])?; let config_path = env.test_dir().join("config.yaml"); std::fs::write(&config_path, config)?; // act env.run_bear_success(&[ "--config", config_path.to_str().unwrap(), "semantic", "--input", "events.json", "--output", "compile_commands.json", ])?; // assert let sut = env.load_compilation_database("compile_commands.json")?; sut.assert_count(2)?; // The donor keeps its glued spelling ... sut.assert_contains(&compilation_entry!( file: "main.c".to_string(), directory: temp_dir.to_string(), arguments: vec![ "gcc".to_string(), "-c".to_string(), "-omain.o".to_string(), "main.c".to_string(), ] ))?; // ... and the synthesized entry drops it whole, not just its flag half. sut.assert_contains(&compilation_entry!( file: "util.h".to_string(), directory: temp_dir.to_string(), arguments: vec!["gcc".to_string(), "-c".to_string(), "util.h".to_string()] ))?; Ok(()) } // Requirements: output-compilation-entries // // Regression test for issue #715: Clang's `-F` (framework search path) is a // JoinedOrSeparate option, so `-F /Frameworks` and `-F/Frameworks` are the same // invocation. Bear only recognized the joined spelling, so the separated operand // fell through to source classification and was dropped, leaving a standalone // `-F` that no compiler can consume. SwiftPM emits the separated form on macOS, // which is how this surfaced. // // Both spellings are checked in one run, each on its own translation unit, so the // pair cannot drift apart. `bear semantic` runs the interpreter without executing // clang, so no toolchain is required and the executable name can be a bare `clang`. #[test] fn clang_framework_search_path_preserves_separated_and_joined_operand() -> Result<()> { let env = TestEnvironment::new("clang_framework_search_path")?; let temp_dir = env.test_dir().to_str().unwrap(); let clang = "clang"; let event_separated = json!({ "executable": clang, "arguments": [clang, "-F", "/Frameworks", "-c", "separated.cpp"], "working_dir": temp_dir, "environment": {} }); let event_joined = json!({ "executable": clang, "arguments": [clang, "-F/Frameworks", "-c", "joined.cpp"], "working_dir": temp_dir, "environment": {} }); let events = format!("{}\n{}", event_separated, event_joined); env.create_source_files(&[ ("events.json", &events), ("separated.cpp", "int main() { return 0; }"), ("joined.cpp", "int main() { return 0; }"), ])?; env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(2)?; db.assert_contains(&compilation_entry!( file: "separated.cpp".to_string(), directory: temp_dir.to_string(), arguments: vec![ clang.to_string(), "-F".to_string(), "/Frameworks".to_string(), "-c".to_string(), "separated.cpp".to_string(), ] ))?; db.assert_contains(&compilation_entry!( file: "joined.cpp".to_string(), directory: temp_dir.to_string(), arguments: vec![ clang.to_string(), "-F/Frameworks".to_string(), "-c".to_string(), "joined.cpp".to_string(), ] ))?; Ok(()) } // Requirements: output-compilation-entries // // Vala's `valac` is a transpiler-driver: it parses GNU-style (GOption) flags, // many of which take a separate-token value (`--pkg gio-2.0`, `--basedir ../src`). // Because Bear classifies any bare argument as a source file, an unrecognized // value-consuming flag turns its value into a phantom source -- so the single // strongest regression guard is that exactly ONE entry (for the `.vala` source) // is produced. `bear semantic` runs the interpreter without executing valac, so // no Vala toolchain is required and the executable name can be a bare `valac`. #[test] fn vala_transpile_mode_produces_single_entry() -> Result<()> { let env = TestEnvironment::new("vala_transpile_mode")?; let temp_dir = env.test_dir().to_str().unwrap(); let event = json!({ "executable": "valac", "arguments": [ "valac", "--pkg", "gio-2.0", "--define=FOO", "-X", "-lm", "--basedir", "../src", "-C", "foo.vala" ], "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[("events.json", &event.to_string()), ("foo.vala", "void main() { }")])?; env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; // Exactly one entry: the separate-token values gio-2.0 and ../src did NOT // leak in as their own source entries. db.assert_count(1)?; db.assert_contains(&compilation_entry!( file: "foo.vala".to_string(), directory: temp_dir.to_string(), arguments: vec![ "valac".to_string(), "--pkg".to_string(), "gio-2.0".to_string(), "--define=FOO".to_string(), "-X".to_string(), "-lm".to_string(), "--basedir".to_string(), "../src".to_string(), "-C".to_string(), "foo.vala".to_string(), ] ))?; Ok(()) } // Requirements: output-compilation-entries // // The default (non-`-C`) driver mode still yields one entry for the `.vala` // source. The internal C-compiler invocation that valac spawns at build time // is a separate process and is not part of a `semantic`-mode events file, so it // is out of scope here (see docs/rationale/vala-transpiler-database). #[test] fn vala_default_mode_produces_single_entry() -> Result<()> { let env = TestEnvironment::new("vala_default_mode")?; let temp_dir = env.test_dir().to_str().unwrap(); let event = json!({ "executable": "valac", "arguments": ["valac", "--pkg", "gio-2.0", "foo.vala"], "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[("events.json", &event.to_string()), ("foo.vala", "void main() { }")])?; env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; db.assert_contains(&compilation_entry!( file: "foo.vala".to_string(), directory: temp_dir.to_string(), arguments: vec![ "valac".to_string(), "--pkg".to_string(), "gio-2.0".to_string(), "foo.vala".to_string(), ] ))?; Ok(()) } // Requirements: output-compilation-entries // // valac compiles all of a target's `.vala` sources together as one translation // unit and produces one combined output. Bear must therefore emit exactly ONE // entry per `valac` invocation, not one per source, with `file` set to the first // source and every source retained in the command. This is the regression that // proves the single-translation-unit behaviour; the single-source tests above // are only N=1 guards. `bear semantic` runs the interpreter without executing // valac, so no Vala toolchain is required. #[test] fn vala_multiple_sources_produce_single_entry() -> Result<()> { let env = TestEnvironment::new("vala_multiple_sources")?; let temp_dir = env.test_dir().to_str().unwrap(); let event = json!({ "executable": "valac", "arguments": [ "valac", "--pkg", "gio-2.0", "--library", "foo", "a.vala", "b.vala", "c.vala" ], "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[ ("events.json", &event.to_string()), ("a.vala", "void a() { }"), ("b.vala", "void b() { }"), ("c.vala", "void main() { }"), ])?; env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; // Three sources, but valac is single-translation-unit: exactly one entry, // keyed on the first source, with all three sources retained. db.assert_count(1)?; db.assert_contains(&compilation_entry!( file: "a.vala".to_string(), directory: temp_dir.to_string(), arguments: vec![ "valac".to_string(), "--pkg".to_string(), "gio-2.0".to_string(), "--library".to_string(), "foo".to_string(), "a.vala".to_string(), "b.vala".to_string(), "c.vala".to_string(), ] ))?; Ok(()) } // Requirements: recognition-compiler-names // // An `mpicc -c hello.c` execution yields one entry, with the wrapper itself // (not the underlying compiler) as the recorded compiler. #[test] fn mpi_wrapper_execution_yields_single_entry() -> Result<()> { assert_driver_yields_single_entry("mpi_wrapper_execution", &["mpicc", "-c", "hello.c"]) } // Requirements: recognition-compiler-names // // Wrapper-info invocations (`-showme`, `-show`, `-compile_info`) print the // underlying compiler command and exit; none of them compile anything, so // none of them should yield a database entry. #[test] fn mpi_wrapper_info_flags_yield_no_entry() -> Result<()> { let env = TestEnvironment::new("mpi_wrapper_info_flags")?; let temp_dir = env.test_dir().to_str().unwrap(); for flag in ["-showme", "-show", "-compile_info"] { let event = json!({ "executable": "mpicc", "arguments": ["mpicc", flag], "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[("events.json", &event.to_string()), ("mpicc", "")])?; env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(0).with_context(|| format!("flag {flag} must yield no entry"))?; } Ok(()) } // Requirements: recognition-compiler-names // // MPICH's compiler-override flag `-cc=gcc` must survive as a single token // (not be split, not be expanded) and must not swallow the source file that // follows it. #[test] fn mpi_wrapper_compiler_override_flag_is_retained() -> Result<()> { assert_driver_yields_single_entry("mpi_wrapper_compiler_override", &["mpicc", "-cc=gcc", "-c", "hello.c"]) } // Requirements: recognition-compiler-names, output-duplicate-detection // // In preload mode the wrapper's child compiler exec is intercepted too, so a // single compilation can produce both an `mpicc` and a `gcc` event for the // same file. The default duplicate filter (directory+file) must collapse // them to one entry, and since the wrapper's event comes first in the event // stream, the surviving entry must record the wrapper invocation. #[test] fn mpi_wrapper_and_child_compiler_events_collapse_to_wrapper_entry() -> Result<()> { assert_duplicate_events_collapse_to_first( "mpi_wrapper_duplicate_collapse", &["mpicc", "-c", "hello.c"], &["gcc", "-c", "hello.c"], ) } // Requirements: recognition-compiler-names // // A `craycc -c hello.c` execution yields one entry, using the CCE C/C++ // compiler name directly (the same shape as the existing Cray Fortran // support for `crayftn`/`ftn`). #[test] fn cray_cc_execution_yields_single_entry() -> Result<()> { assert_driver_yields_single_entry("cray_cc_execution", &["craycc", "-c", "hello.c"]) } // Requirements: recognition-compiler-names // // A `hipcc -c hello.c` execution yields one entry, using the ROCm HIP // compiler driver name directly (parsed with Clang flag semantics). #[test] fn hipcc_execution_yields_single_entry() -> Result<()> { assert_driver_yields_single_entry("hipcc_execution", &["hipcc", "-c", "hello.c"]) } // Requirements: recognition-compiler-names // // A `qcc -c hello.c` execution yields one entry, using the QNX driver name // directly (parsed with GCC flag semantics -- QNX 8 is GCC-backed). #[test] fn qnx_qcc_execution_yields_single_entry() -> Result<()> { assert_driver_yields_single_entry("qnx_qcc_execution", &["qcc", "-c", "hello.c"]) } // Requirements: recognition-compiler-names // // QNX's attached-value variant selector (`-Vgcc_ntoaarch64le`) must be // treated as a driver option, never as an input file, and must be retained // verbatim in the recorded arguments. #[test] fn qnx_qcc_variant_selector_is_retained_as_driver_option() -> Result<()> { assert_driver_yields_single_entry( "qnx_qcc_variant_selector", &["qcc", "-Vgcc_ntoaarch64le", "-c", "hello.c"], ) } // Requirements: recognition-compiler-names // // A `tiarmclang -c hello.c` execution yields one entry (TI's clang-based // driver, parsed with Clang flag semantics). #[test] fn ti_tiarmclang_execution_yields_single_entry() -> Result<()> { assert_driver_yields_single_entry("ti_tiarmclang_execution", &["tiarmclang", "-c", "hello.c"]) } // Requirements: recognition-compiler-names // // Embarcadero C++Builder's Clang-based drivers share one family and are // parsed with Clang flag semantics. Each driver name has to be recognized // on its own. #[test] fn embarcadero_driver_names_yield_single_entry() -> Result<()> { for driver in ["bcc64x", "bcc64", "bcc32x"] { assert_driver_yields_single_entry( &format!("embarcadero_{driver}_execution"), &[driver, "-c", "hello.c"], )?; } Ok(()) } // Requirements: recognition-compiler-names // // TI's classic Code Generation Tools ship one driver per target family, // all speaking the same option dialect (`--compile_only` is TI's `-c`). // Each driver name has to be recognized on its own. #[test] fn ti_cgt_driver_names_yield_single_entry() -> Result<()> { for driver in ["armcl", "cl6x", "cl7x", "cl2000", "cl430", "clpru"] { assert_driver_yields_single_entry( &format!("ti_cgt_{driver}_execution"), &[driver, "--compile_only", "hello.c"], ) .with_context(|| format!("driver {driver}"))?; } Ok(()) } // Requirements: recognition-compiler-names // // The shape Code Composer Studio generates: TI long options glue their // value after `=`, so `--preproc_dependency`'s dependency file and // `--obj_directory`'s directory must be consumed as those flags' values // rather than read as further source files, and the invocation still // compiles because `--preproc_with_compile` resumes it. #[test] fn ti_cgt_code_composer_studio_command_yields_single_entry() -> Result<()> { assert_driver_yields_single_entry( "ti_cgt_ccs_command", &[ "armcl", "-mv7R4", "--code_state=32", "--float_support=VFPv3D16", "-me", "--include_path=include", "-g", "--diag_warning=225", "--abi=eabi", "--preproc_with_compile", "--preproc_dependency=hello.d_raw", "--obj_directory=obj", "hello.c", ], ) } // Requirements: recognition-compiler-names // // Intel's MSVC-compatible driver `icx-cl` takes `/` options and `-` options // alike; both spellings of "compile only" yield one entry. #[test] fn intel_icx_cl_execution_yields_single_entry() -> Result<()> { assert_driver_yields_single_entry("intel_icx_cl_dash_c", &["icx-cl", "-c", "hello.c"])?; assert_driver_yields_single_entry("intel_icx_cl_slash_c", &["icx-cl", "/c", "hello.c"])?; assert_driver_yields_single_entry( "intel_icx_cl_mixed_options", &["icx-cl", "-fsycl", "/Qiopenmp", "/c", "hello.c"], ) } // Requirements: output-compilation-entries // // Two TI invocations that compile nothing. `--preproc_only` writes // preprocessed output and stops, like `-E`. `-z` hands the rest of the // command line to the linker, which is how a Code Composer Studio // makefile spells its link step through the same driver; nothing after // it is a translation unit. #[test] fn ti_cgt_non_compiling_invocations_yield_no_entry() -> Result<()> { let env = TestEnvironment::new("ti_cgt_non_compiling")?; let temp_dir = env.test_dir().to_str().unwrap(); for arguments in [vec!["armcl", "--preproc_only", "hello.c"], vec!["armcl", "-z", "-o", "hello.out", "hello.c"]] { let event = json!({ "executable": "armcl", "arguments": arguments, "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[ ("events.json", &event.to_string()), ("armcl", ""), ("hello.c", "int main() { return 0; }"), ])?; env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(0).with_context(|| format!("arguments {arguments:?} must yield no entry"))?; } Ok(()) } // Requirements: recognition-compiler-names // // An `xc8-cc -c hello.c` execution yields one entry (Microchip's gcc-styled // XC8 driver, parsed with GCC flag semantics). #[test] fn microchip_xc8_cc_execution_yields_single_entry() -> Result<()> { assert_driver_yields_single_entry("microchip_xc8_cc_execution", &["xc8-cc", "-c", "hello.c"]) } // Requirements: recognition-compiler-names // // An `emcc -c hello.c` execution yields one entry, using the Emscripten // driver name directly (parsed with Clang flag semantics). #[test] fn emscripten_emcc_execution_yields_single_entry() -> Result<()> { assert_driver_yields_single_entry("emscripten_emcc_execution", &["emcc", "-c", "hello.c"]) } // Requirements: recognition-compiler-names, output-duplicate-detection // // In preload mode emcc's underlying clang child process is intercepted too, // so a single compilation can produce both an `emcc` and a `clang` event for // the same file. The default duplicate filter (directory+file) must collapse // them to one entry, and since the driver's event comes first in the event // stream, the surviving entry must record the emcc invocation. #[test] fn emscripten_driver_and_clang_child_events_collapse_to_driver_entry() -> Result<()> { assert_duplicate_events_collapse_to_first( "emscripten_duplicate_collapse", &["emcc", "-c", "hello.c"], &["clang", "-c", "hello.c"], ) } // Requirements: recognition-compiler-launchers // // An `icecc gcc -c hello.c` execution is recorded as the real compiler's // compilation: the icecc token is dropped and gcc's argv survives, the same // contract as ccache/distcc/sccache. #[test] fn icecc_launcher_execution_records_real_compiler() -> Result<()> { assert_launcher_execution_yields_entry( "icecc_launcher_execution", &["icecc", "gcc"], &["icecc", "gcc", "-c", "hello.c"], &["gcc", "-c", "hello.c"], ) } // Requirements: recognition-compiler-names // // A `gfortran -c hello.f90` execution yields one entry for the Fortran // source, with the invocation's arguments recorded verbatim -- one // representative row proving the Fortran compiler names follow the same // recognition pattern as the C family, at entry level. #[test] fn gfortran_execution_yields_single_entry() -> Result<()> { assert_driver_yields_single_entry_for_source( "gfortran_execution", &["gfortran", "-c", "hello.f90"], "hello.f90", "program hello\nend program hello\n", ) } // Requirements: recognition-compiler-names // // An `armflang -c hello.f90` execution yields one entry for the Fortran // source (Arm Compiler for Linux's Fortran driver, parsed with Flang // flag semantics), with the invocation's arguments recorded verbatim. #[test] fn arm_flang_execution_yields_single_entry() -> Result<()> { assert_driver_yields_single_entry_for_source( "arm_flang_execution", &["armflang", "-c", "hello.f90"], "hello.f90", "program hello\nend program hello\n", ) } // Requirements: recognition-compiler-names // // A `nasm -f elf64 -o hello.o hello.asm` execution yields one entry for // `hello.asm`, using the NASM driver name directly, with the invocation's // arguments recorded verbatim. #[test] fn nasm_execution_yields_single_entry() -> Result<()> { assert_driver_yields_single_entry_for_source( "nasm_execution", &["nasm", "-f", "elf64", "-o", "hello.o", "hello.asm"], "hello.asm", "section .text\nglobal _start\n_start:\n ret\n", ) } // Requirements: recognition-compiler-names // // A `fasm hello.asm` execution yields one entry for `hello.asm`, using the // flat assembler driver name directly. #[test] fn fasm_execution_yields_single_entry() -> Result<()> { assert_driver_yields_single_entry_for_source( "fasm_execution", &["fasm", "hello.asm"], "hello.asm", "org 0x100\nret\n", ) } // Requirements: output-compilation-entries // // NASM's `-M` and `-MG` emit Makefile dependencies and stop: nothing is // assembled, so a `nasm -M hello.asm` execution must yield no entry. Guards // the stops_at_preprocessing classification in nasm.yaml. #[test] fn nasm_dependency_only_invocation_yields_no_entry() -> Result<()> { let env = TestEnvironment::new("nasm_dependency_only")?; let temp_dir = env.test_dir().to_str().unwrap(); for flag in ["-M", "-MG"] { let event = json!({ "executable": "nasm", "arguments": ["nasm", flag, "hello.asm"], "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[ ("events.json", &event.to_string()), ("nasm", ""), ("hello.asm", "section .text\nglobal _start\n_start:\n ret\n"), ])?; env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(0).with_context(|| format!("flag {flag} must yield no entry"))?; } Ok(()) } // Requirements: output-compilation-entries // // NASM's `-MD` generates the dependency file as a side effect of a real // assembly step, so the invocation still yields its entry for `hello.asm`. #[test] fn nasm_dependency_side_effect_assembly_yields_entry() -> Result<()> { assert_driver_yields_single_entry_for_source( "nasm_dependency_side_effect", &["nasm", "-MD", "hello.d", "-f", "elf64", "-o", "hello.o", "hello.asm"], "hello.asm", "section .text\nglobal _start\n_start:\n ret\n", ) } // Requirements: recognition-compiler-names // // Direct assembly through a driver (`gcc -c foo.s`) is already recorded // today via the driver's own entry -- this is the actual fix for the // compile-then-assemble class of bug reported in issue #146, and it needs // no new recognition code (the standalone-assembler recognizer added by // this requirement is not involved). Locking this in guards against a // future regression that would make it invisible again. // // Unlike the synthetic embedded-toolchain names used elsewhere in this // file, `gcc` is a real, `PATH`-resolvable executable on any host that can // build Bear (its own host requirements list a `cc` toolchain), which // historically made `ExecutableResolver` rewrite the recorded compiler // token to gcc's absolute path. The recorded compiler now keeps the // observed spelling (see `semantic_bare_compiler_name_stays_bare_in_output`); // this test still asserts only the parts it cares about: one entry, for // `foo.s`, invoked with `-c foo.s`, as `gcc`. #[test] fn driver_compiled_assembly_yields_driver_entry() -> Result<()> { let env = TestEnvironment::new("driver_compiled_assembly")?; let temp_dir = env.test_dir().to_str().unwrap().to_string(); let event = json!({ "executable": "gcc", "arguments": ["gcc", "-c", "foo.s"], "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[ ("events.json", &event.to_string()), ("foo.s", ".text\n.globl main\nmain:\n ret\n"), ])?; env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; let entry = &db.entries()[0]; assert_eq!(entry["file"], "foo.s"); assert_eq!(entry["directory"], temp_dir); let arguments: Vec<String> = entry["arguments"] .as_array() .context("arguments must be an array")? .iter() .map(|v| v.as_str().unwrap().to_string()) .collect(); assert_eq!(&arguments[1..], &["-c".to_string(), "foo.s".to_string()]); let compiler_basename = std::path::Path::new(&arguments[0]).file_name().and_then(|n| n.to_str()); assert_eq!(compiler_basename, Some("gcc"), "recorded compiler must be gcc, got {:?}", arguments[0]); Ok(()) } // Requirements: recognition-compiler-names // // A `swiftc -c hello.swift` execution yields one entry, using the Swift // driver name directly, with the invocation's arguments recorded verbatim. #[test] fn swiftc_single_file_execution_yields_single_entry() -> Result<()> { assert_driver_yields_single_entry_for_source( "swiftc_single_file", &["swiftc", "-c", "hello.swift"], "hello.swift", "print(\"hello\")\n", ) } // Requirements: recognition-compiler-names // // Swift's whole-module compilation names several sources in one invocation, // but SourceKit-LSP looks up a compile command per file. Bear must therefore // emit one entry PER source (unlike valac's single combined entry), and every // one of those entries must carry the COMPLETE invocation -- all sources, not // just its own -- because each file's semantics depend on the whole module. // `bear semantic` runs the interpreter without executing swiftc, so no Swift // toolchain is required. #[test] fn swiftc_whole_module_invocation_yields_one_entry_per_source_with_full_arguments() -> Result<()> { let env = TestEnvironment::new("swiftc_whole_module")?; let temp_dir = env.test_dir().to_str().unwrap(); let event = json!({ "executable": "swiftc", "arguments": ["swiftc", "-module-name", "App", "-emit-object", "a.swift", "b.swift"], "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[ ("events.json", &event.to_string()), ("a.swift", "func a() {}\n"), ("b.swift", "func main() {}\n"), ("swiftc", ""), ])?; env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; // Two sources, whole-module: exactly two entries (one per source), each // retaining every source and flag from the original invocation. db.assert_count(2)?; let full_arguments = vec![ "swiftc".to_string(), "-module-name".to_string(), "App".to_string(), "-emit-object".to_string(), "a.swift".to_string(), "b.swift".to_string(), ]; db.assert_contains(&compilation_entry!( file: "a.swift".to_string(), directory: temp_dir.to_string(), arguments: full_arguments.clone() ))?; db.assert_contains(&compilation_entry!( file: "b.swift".to_string(), directory: temp_dir.to_string(), arguments: full_arguments ))?; Ok(()) } // Requirements: recognition-compiler-names // // swiftc spawns per-file `swift-frontend` jobs the way gcc spawns `cc1`; // the internal frontend executable must yield no database entry. #[test] fn swift_frontend_executable_execution_yields_no_entry() -> Result<()> { let env = TestEnvironment::new("swift_frontend_executable")?; let temp_dir = env.test_dir().to_str().unwrap(); let event = json!({ "executable": "swift-frontend", "arguments": ["swift-frontend", "-frontend", "-c", "hello.swift"], "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[ ("events.json", &event.to_string()), ("hello.swift", "print(\"hello\")\n"), ("swift-frontend", ""), ])?; env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(0)?; Ok(()) } // Requirements: recognition-compiler-names // // A legacy toolchain that re-invokes itself as `swiftc -frontend` must also // yield no entry, via the `ignore_when.flags` filter (mirrors clang's -cc1). #[test] fn swiftc_frontend_flag_execution_yields_no_entry() -> Result<()> { let env = TestEnvironment::new("swiftc_frontend_flag")?; let temp_dir = env.test_dir().to_str().unwrap(); let event = json!({ "executable": "swiftc", "arguments": ["swiftc", "-frontend", "-c", "hello.swift"], "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[ ("events.json", &event.to_string()), ("hello.swift", "print(\"hello\")\n"), ("swiftc", ""), ])?; env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(0)?; Ok(()) } // Requirements: recognition-compiler-names // // `swiftc --version` prints version information and exits; it yields no // database entry. #[test] fn swiftc_version_flag_yields_no_entry() -> Result<()> { let env = TestEnvironment::new("swiftc_version_flag")?; let temp_dir = env.test_dir().to_str().unwrap(); let event = json!({ "executable": "swiftc", "arguments": ["swiftc", "--version"], "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[("events.json", &event.to_string()), ("swiftc", "")])?; env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(0)?; Ok(()) } // Requirements: recognition-cpp20-modules // // A C++20 module-interface compile (`clang++ --precompile ... -o foo.pcm`) // must produce exactly one entry for the interface source (`foo.cppm`), with // `--precompile` and the input/output paths preserved verbatim. The `.pcm` // output is consumed by the `-o` flag and must never itself surface as a // `file` or a standalone source argument. #[test] fn clang_precompile_module_interface_yields_single_entry_for_source() -> Result<()> { let env = TestEnvironment::new("clang_precompile_module_interface")?; let temp_dir = env.test_dir().to_str().unwrap(); let event = json!({ "executable": "clang++", "arguments": ["clang++", "--precompile", "-std=c++20", "foo.cppm", "-o", "foo.pcm"], "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[("events.json", &event.to_string()), ("foo.cppm", "export module foo;\n")])?; env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; db.assert_contains(&compilation_entry!( file: "foo.cppm".to_string(), directory: temp_dir.to_string(), arguments: vec![ "clang++".to_string(), "--precompile".to_string(), "-std=c++20".to_string(), "foo.cppm".to_string(), "-o".to_string(), "foo.pcm".to_string(), ] ))?; Ok(()) } // Requirements: recognition-cpp20-modules // // Consuming a precompiled module via `-fmodule-file=<name>=<file>` must not // break recognition of the real source (`main.cpp`), and the referenced // `.pcm` must never surface as its own entry. #[test] fn clang_module_file_flag_yields_single_entry_for_main_source() -> Result<()> { let env = TestEnvironment::new("clang_module_file_flag")?; let temp_dir = env.test_dir().to_str().unwrap(); let event = json!({ "executable": "clang++", "arguments": ["clang++", "-std=c++20", "-fmodule-file=foo=foo.pcm", "-c", "main.cpp"], "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[ ("events.json", &event.to_string()), ("main.cpp", "import foo;\nint main() { return 0; }\n"), ])?; env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; db.assert_contains(&compilation_entry!( file: "main.cpp".to_string(), directory: temp_dir.to_string(), arguments: vec![ "clang++".to_string(), "-std=c++20".to_string(), "-fmodule-file=foo=foo.pcm".to_string(), "-c".to_string(), "main.cpp".to_string(), ] ))?; Ok(()) } // Requirements: recognition-cpp20-modules // // A mixed build with one module interface and one consumer, processed in a // single semantic run, must yield exactly two entries: no cross-talk between // the `--precompile` invocation and the `-fmodule-file=` invocation. #[test] fn clang_module_interface_and_consumer_yield_two_entries() -> Result<()> { let env = TestEnvironment::new("clang_module_interface_and_consumer")?; let temp_dir = env.test_dir().to_str().unwrap(); let event1 = json!({ "executable": "clang++", "arguments": ["clang++", "--precompile", "-std=c++20", "foo.cppm", "-o", "foo.pcm"], "working_dir": temp_dir, "environment": {} }); let event2 = json!({ "executable": "clang++", "arguments": ["clang++", "-std=c++20", "-fmodule-file=foo=foo.pcm", "-c", "main.cpp"], "working_dir": temp_dir, "environment": {} }); let events_content = format!("{}\n{}", event1, event2); env.create_source_files(&[ ("events.json", &events_content), ("foo.cppm", "export module foo;\n"), ("main.cpp", "import foo;\nint main() { return 0; }\n"), ])?; env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(2)?; db.assert_contains(&compilation_entry!( file: "foo.cppm".to_string(), directory: temp_dir.to_string(), arguments: vec![ "clang++".to_string(), "--precompile".to_string(), "-std=c++20".to_string(), "foo.cppm".to_string(), "-o".to_string(), "foo.pcm".to_string(), ] ))?; db.assert_contains(&compilation_entry!( file: "main.cpp".to_string(), directory: temp_dir.to_string(), arguments: vec![ "clang++".to_string(), "-std=c++20".to_string(), "-fmodule-file=foo=foo.pcm".to_string(), "-c".to_string(), "main.cpp".to_string(), ] ))?; Ok(()) } // Requirements: interception-events-format // // `bear semantic --input -` must read the event stream from standard input // and produce the same compilation database as the equivalent file-backed // run, so the pipeline `<producer> | bear semantic --input -` works. #[test] #[cfg(has_executable_compiler_c)] fn semantic_input_stdin_matches_file_input() -> Result<()> { let env = TestEnvironment::new("semantic_input_stdin")?; let temp_dir = env.test_dir().to_str().unwrap(); let event1 = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "test.c"], "working_dir": temp_dir, "environment": {} }); let event2 = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "other.c"], "working_dir": temp_dir, "environment": {} }); let events_content = format!("{}\n{}\n", event1, event2); env.create_source_files(&[ ("events.json", &events_content), ("test.c", "int main() { return 0; }"), ("other.c", "int main() { return 0; }"), ])?; // Baseline: the same event stream read from a file. env.run_bear_success(&["semantic", "--input", "events.json", "--output", "from_file.json"])?; let from_file = env.load_compilation_database("from_file.json")?; from_file.assert_count(2)?; // Under test: the identical stream read from standard input via `-`. let stdin_result = env.run_bear_with_stdin( &["semantic", "--input", "-", "--output", "from_stdin.json"], events_content.as_bytes(), )?; stdin_result.assert_success()?; let from_stdin = env.load_compilation_database("from_stdin.json")?; let mut file_entries: Vec<String> = from_file.entries().iter().map(|entry| entry.to_string()).collect(); let mut stdin_entries: Vec<String> = from_stdin.entries().iter().map(|entry| entry.to_string()).collect(); file_entries.sort(); stdin_entries.sort(); assert_eq!( stdin_entries, file_entries, "semantic --input - must produce the same compilation database as semantic --input <file>" ); Ok(()) } // Requirements: interception-events-format // // `bear semantic` is a filter: with no input named it reads the event // stream from standard input, so a producer pipes into it with no flags // (`<producer> | bear semantic`). #[test] #[cfg(has_executable_compiler_c)] fn semantic_input_defaults_to_stdin() -> Result<()> { let env = TestEnvironment::new("semantic_default_stdin")?; let temp_dir = env.test_dir().to_str().unwrap(); let event = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "test.c"], "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[("test.c", "int main() { return 0; }")])?; let result = env.run_bear_with_stdin( &["semantic", "--output", "compile_commands.json"], format!("{event}\n").as_bytes(), )?; result.assert_success()?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; db.assert_contains(&compilation_entry!( file: "test.c".to_string(), directory: temp_dir.to_string(), arguments: vec![COMPILER_C_PATH.to_string(), "-c".to_string(), "test.c".to_string()] ))?; Ok(()) } // Requirements: interception-events-format, cli-exit-codes // // An empty event stream yields an empty database and success, but it is // almost always a plumbing mistake (a producer that emitted nothing, or a // forgotten redirect), so a stderr notice must say the stream was empty. #[test] fn semantic_empty_stdin_succeeds_with_warning() -> Result<()> { let env = TestEnvironment::new("semantic_empty_stdin")?; let result = env.run_bear_with_stdin(&["semantic", "--output", "compile_commands.json"], b"")?; result.assert_success()?; let stderr = result.stderr(); assert!(stderr.contains("no events"), "stderr must warn about the empty event stream: {stderr}"); let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(0)?; Ok(()) } // Requirements: recognition-cpp20-modules // // GCC's transitional module flag `-fmodules-ts` must not break recognition // of the module-interface source (`mod.cppm`). #[test] fn gcc_modules_ts_flag_yields_single_entry_for_module_interface() -> Result<()> { let env = TestEnvironment::new("gcc_modules_ts_flag")?; let temp_dir = env.test_dir().to_str().unwrap(); let event = json!({ "executable": "g++", "arguments": ["g++", "-std=c++20", "-fmodules-ts", "-c", "mod.cppm"], "working_dir": temp_dir, "environment": {} }); env.create_source_files(&[("events.json", &event.to_string()), ("mod.cppm", "export module mod;\n")])?; env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; db.assert_contains(&compilation_entry!( file: "mod.cppm".to_string(), directory: temp_dir.to_string(), arguments: vec![ "g++".to_string(), "-std=c++20".to_string(), "-fmodules-ts".to_string(), "-c".to_string(), "mod.cppm".to_string(), ] ))?; Ok(()) } // Requirements: interception-events-format // // A malformed line in the middle of the event stream must not drop every // valid line that follows it: both the line before and the line after the // malformed one must still become compilation-database entries, and the // malformed line is reported with its physical line number on stderr at // the default log level (no `RUST_LOG` opt-in required). #[test] #[cfg(has_executable_compiler_c)] fn semantic_input_mixed_valid_and_malformed_lines_keeps_both_valid_entries() -> Result<()> { let env = TestEnvironment::new("semantic_mixed_valid_malformed")?; let temp_dir = env.test_dir().to_str().unwrap(); let event1 = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "test.c"], "working_dir": temp_dir, "environment": {} }); let event2 = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "other.c"], "working_dir": temp_dir, "environment": {} }); // Line 2 is deliberately not valid JSON at all. let events_content = format!("{event1}\nthis is not json\n{event2}\n"); env.create_source_files(&[ ("test.c", "int main() { return 0; }"), ("other.c", "int main() { return 0; }"), ])?; let result = env.run_bear_with_stdin_default_log( &["semantic", "--input", "-", "--output", "compile_commands.json"], events_content.as_bytes(), )?; result.assert_success()?; let stderr = result.stderr(); assert!( stderr.contains("line 2"), "stderr must cite the malformed line's physical line number: {stderr}" ); let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(2)?; db.assert_contains(&compilation_entry!( file: "test.c".to_string(), directory: temp_dir.to_string(), arguments: vec![COMPILER_C_PATH.to_string(), "-c".to_string(), "test.c".to_string()] ))?; db.assert_contains(&compilation_entry!( file: "other.c".to_string(), directory: temp_dir.to_string(), arguments: vec![COMPILER_C_PATH.to_string(), "-c".to_string(), "other.c".to_string()] ))?; Ok(()) } // Requirements: interception-events-format, cli-exit-codes // // When every line of a non-empty event stream is rejected, the run must // not silently succeed with an empty compilation database: it exits // non-zero so the failure is visible. #[test] fn semantic_input_all_malformed_lines_exits_non_zero() -> Result<()> { let env = TestEnvironment::new("semantic_all_malformed")?; let events_content = "not json\n{\"executable\": 42}\nalso not json\n"; let result = env.run_bear_with_stdin( &["semantic", "--input", "-", "--output", "compile_commands.json"], events_content.as_bytes(), )?; result.assert_failure()?; Ok(()) } // Requirements: output-json-compilation-database // // `bear semantic` runs no build, so unlike the combined/intercept modes its // stdout is a safe, clean channel: `--output -` must stream the JSON // compilation database to standard output, rather than literally creating a // file named `-`. The stdout database must be identical to the one produced // by the same input written to a file (same dedup/header/source-filter/ // format decorators, only the sink differs). #[test] #[cfg(has_executable_compiler_c)] fn semantic_output_stdout_matches_file_output() -> Result<()> { let env = TestEnvironment::new("semantic_output_stdout")?; let temp_dir = env.test_dir().to_str().unwrap(); let event1 = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "test.c"], "working_dir": temp_dir, "environment": {} }); let event2 = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "other.c"], "working_dir": temp_dir, "environment": {} }); let events_content = format!("{event1}\n{event2}\n"); env.create_source_files(&[ ("events.json", &events_content), ("test.c", "int main() { return 0; }"), ("other.c", "int main() { return 0; }"), ])?; // Baseline: the same event input, written to a file. env.run_bear_success(&["semantic", "--input", "events.json", "--output", "from_file.json"])?; let from_file = env.load_compilation_database("from_file.json")?; from_file.assert_count(2)?; // Under test: identical input, output streamed to stdout via `-`. let stdout_result = env.run_bear_success(&["semantic", "--input", "events.json", "--output", "-"])?; let stdout_entries: Vec<Value> = serde_json::from_str(&stdout_result.stdout()) .context("stdout must be a valid JSON compilation database")?; assert_eq!(stdout_entries.len(), 2, "stdout database must contain 2 entries: {stdout_entries:?}"); let mut file_entries: Vec<String> = from_file.entries().iter().map(|entry| entry.to_string()).collect(); let mut stdout_strings: Vec<String> = stdout_entries.iter().map(|entry| entry.to_string()).collect(); file_entries.sort(); stdout_strings.sort(); assert_eq!( stdout_strings, file_entries, "semantic --output - must produce the same compilation database as semantic --output <file>" ); assert!(!env.file_exists("-"), "must not create a file literally named `-`"); Ok(()) } // Requirements: output-duplicate-detection // // Duplicate filtering must still run when the pipeline streams to stdout: // two identical compiles collapse to one entry, proving the dedup decorator // (and not just the base writer) is present in the stdout pipeline branch. #[test] #[cfg(has_executable_compiler_c)] fn semantic_output_stdout_deduplicates_entries() -> Result<()> { let env = TestEnvironment::new("semantic_output_stdout_dedup")?; let temp_dir = env.test_dir().to_str().unwrap(); let event = json!({ "executable": COMPILER_C_PATH, "arguments": [COMPILER_C_PATH, "-c", "test.c"], "working_dir": temp_dir, "environment": {} }); let events_content = format!("{event}\n{event}\n"); env.create_source_files(&[("events.json", &events_content), ("test.c", "int main() { return 0; }")])?; let result = env.run_bear_success(&["semantic", "--input", "events.json", "--output", "-"])?; let entries: Vec<Value> = serde_json::from_str(&result.stdout()).context("stdout must be a valid JSON compilation database")?; assert_eq!(entries.len(), 1, "duplicate compiles must collapse to one stdout entry: {entries:?}"); Ok(()) } // Requirements: output-json-compilation-database, output-append // // Appending means reading back the existing output before writing, which is // impossible for a stream: `--append` combined with `--output -` must be // rejected up front with a clear message, rather than attempting (and // failing confusingly) to open `-` as a file. #[test] fn semantic_output_stdout_rejects_append() -> Result<()> { let env = TestEnvironment::new("semantic_output_stdout_append")?; env.create_source_files(&[("events.json", "")])?; let result = env.run_bear(&["semantic", "--input", "events.json", "--output", "-", "--append"])?; result.assert_failure()?; let stderr = result.stderr(); assert!( stderr.contains("cannot append to standard output"), "stderr must explain why append + stdout output is rejected: {stderr}" ); assert!(!env.file_exists("-"), "must not create a file literally named `-`"); Ok(()) } // Requirements: recognition-compiler-names // // `--print-compilers` is a pure static dump of the built-in recognition // tables: dispatch happens before any input stream is opened, so the // command must exit promptly even with stdin left open and undrained -- // unlike every other `semantic` invocation, which reads an event stream. // Spawned directly (rather than through `run_bear`, whose `Command::output` // already closes stdin) so an accidental stdin read on this path would show // up here as a hang, not as a false pass. #[test] fn semantic_print_compilers_does_not_block_on_stdin() -> Result<()> { use std::io::Read; use std::process::{Command, Stdio}; use std::time::{Duration, Instant}; let env = TestEnvironment::new("semantic_print_compilers")?; let mut child = Command::new(env.bear_path()) .current_dir(env.test_dir()) .args(["semantic", "--print-compilers"]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn()?; // Keep the write end of stdin open for the whole wait -- never written // to, never dropped/closed. If the dispatcher read from it, this test // would hang instead of the process exiting on its own. let _stdin = child.stdin.take().context("stdin is piped")?; let deadline = Instant::now() + Duration::from_secs(5); let status = loop { if let Some(status) = child.try_wait()? { break status; } if Instant::now() >= deadline { let _ = child.kill(); let _ = child.wait(); anyhow::bail!( "bear semantic --print-compilers did not exit within 5s; it must not block on stdin" ); } std::thread::sleep(Duration::from_millis(20)); }; assert!(status.success(), "bear semantic --print-compilers must exit successfully"); let mut stdout = String::new(); child.stdout.take().context("stdout is piped")?.read_to_string(&mut stdout)?; assert!(stdout.contains("recognizes the following compilers"), "missing banner in stdout: {stdout}"); assert!(stdout.contains("GCC"), "missing GCC entry in stdout: {stdout}"); assert!(stdout.contains("as gcc"), "missing gcc alias in stdout: {stdout}"); Ok(()) } ������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/tests/fixtures/���������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0023313�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/tests/fixtures/constants.rs���������������������������������0000664�0000000�0000000�00000004442�15271777203�0025701�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: GPL-3.0-or-later // Bear executable path - only available when integration tests are enabled pub const DRIVER_EXECUTABLE_PATH: &str = env!("DRIVER_EXECUTABLE_PATH"); pub const WRAPPER_EXECUTABLE_PATH: &str = env!("WRAPPER_EXECUTABLE_PATH"); pub const PRELOAD_LIBRARY_PATH: &str = env!("PRELOAD_LIBRARY_PATH"); pub const DRIVER_EXECUTABLE: &str = env!("DRIVER_EXECUTABLE"); pub const WRAPPER_EXECUTABLE: &str = env!("WRAPPER_EXECUTABLE"); pub const PRELOAD_LIBRARY: &str = env!("PRELOAD_LIBRARY"); #[cfg(has_executable_true)] pub const TRUE_PATH: &str = env!("TRUE_PATH"); #[cfg(has_executable_false)] pub const FALSE_PATH: &str = env!("FALSE_PATH"); #[cfg(has_executable_echo)] pub const ECHO_PATH: &str = env!("ECHO_PATH"); #[cfg(has_executable_sleep)] pub const SLEEP_PATH: &str = env!("SLEEP_PATH"); #[cfg(has_executable_setsid)] pub const SETSID_PATH: &str = env!("SETSID_PATH"); #[cfg(has_executable_shell)] pub const SHELL_PATH: &str = env!("SHELL_PATH"); #[cfg(has_executable_make)] pub const MAKE_PATH: &str = env!("MAKE_PATH"); #[cfg(has_executable_compiler_c)] pub const COMPILER_C_PATH: &str = env!("COMPILER_C_PATH"); #[cfg(has_executable_compiler_cxx)] pub const COMPILER_CXX_PATH: &str = env!("COMPILER_CXX_PATH"); #[cfg(has_executable_compiler_fortran)] pub const COMPILER_FORTRAN_PATH: &str = env!("COMPILER_FORTRAN_PATH"); #[cfg(has_executable_compiler_cuda)] pub const COMPILER_CUDA_PATH: &str = env!("COMPILER_CUDA_PATH"); #[cfg(has_executable_libtool)] pub const LIBTOOL_PATH: &str = env!("LIBTOOL_PATH"); #[cfg(has_executable_fakeroot)] pub const FAKEROOT_PATH: &str = env!("FAKEROOT_PATH"); #[cfg(has_executable_valgrind)] pub const VALGRIND_PATH: &str = env!("VALGRIND_PATH"); #[cfg(has_executable_ar)] pub const AR_PATH: &str = env!("AR_PATH"); #[cfg(has_executable_env)] pub const ENV_PATH: &str = env!("ENV_PATH"); #[cfg(has_executable_cat)] pub const CAT_PATH: &str = env!("CAT_PATH"); #[cfg(has_executable_ls)] pub const LS_PATH: &str = env!("LS_PATH"); #[cfg(has_executable_mkdir)] pub const MKDIR_PATH: &str = env!("MKDIR_PATH"); #[cfg(has_executable_rm)] pub const RM_PATH: &str = env!("RM_PATH"); pub const INSTALL_SCRIPT_PATH: &str = env!("INSTALL_SCRIPT_PATH"); pub const INTERCEPT_LIBDIR: &str = env!("INTERCEPT_LIBDIR"); pub const REPO_ROOT: &str = env!("REPO_ROOT"); ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/tests/fixtures/data/����������������������������������������0000775�0000000�0000000�00000000000�15271777203�0024224�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/tests/fixtures/data/zlib.make-n-w.sh������������������������0000664�0000000�0000000�00000013747�15271777203�0027147�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������make: Entering directory '/tmp/build' gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -I../zlib-1.3.1/ -c -o example.o ../zlib-1.3.1/test/example.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -c -o adler32.o ../zlib-1.3.1/adler32.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -c -o crc32.o ../zlib-1.3.1/crc32.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -c -o deflate.o ../zlib-1.3.1/deflate.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -c -o infback.o ../zlib-1.3.1/infback.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -c -o inffast.o ../zlib-1.3.1/inffast.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -c -o inflate.o ../zlib-1.3.1/inflate.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -c -o inftrees.o ../zlib-1.3.1/inftrees.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -c -o trees.o ../zlib-1.3.1/trees.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -c -o zutil.o ../zlib-1.3.1/zutil.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -c -o compress.o ../zlib-1.3.1/compress.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -c -o uncompr.o ../zlib-1.3.1/uncompr.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -c -o gzclose.o ../zlib-1.3.1/gzclose.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -c -o gzlib.o ../zlib-1.3.1/gzlib.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -c -o gzread.o ../zlib-1.3.1/gzread.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -c -o gzwrite.o ../zlib-1.3.1/gzwrite.c ar rc libz.a adler32.o crc32.o deflate.o infback.o inffast.o inflate.o inftrees.o trees.o zutil.o compress.o uncompr.o gzclose.o gzlib.o gzread.o gzwrite.o (ranlib libz.a || true) >/dev/null 2>&1 gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -o example example.o -L. libz.a gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -I../zlib-1.3.1/ -c -o minigzip.o ../zlib-1.3.1/test/minigzip.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -o minigzip minigzip.o -L. libz.a mkdir objs 2>/dev/null || test -d objs gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -DPIC -c -o objs/adler32.o ../zlib-1.3.1/adler32.c mv objs/adler32.o adler32.lo mkdir objs 2>/dev/null || test -d objs gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -DPIC -c -o objs/crc32.o ../zlib-1.3.1/crc32.c mv objs/crc32.o crc32.lo mkdir objs 2>/dev/null || test -d objs gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -DPIC -c -o objs/deflate.o ../zlib-1.3.1/deflate.c mv objs/deflate.o deflate.lo mkdir objs 2>/dev/null || test -d objs gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -DPIC -c -o objs/infback.o ../zlib-1.3.1/infback.c mv objs/infback.o infback.lo mkdir objs 2>/dev/null || test -d objs gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -DPIC -c -o objs/inffast.o ../zlib-1.3.1/inffast.c mv objs/inffast.o inffast.lo mkdir objs 2>/dev/null || test -d objs gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -DPIC -c -o objs/inflate.o ../zlib-1.3.1/inflate.c mv objs/inflate.o inflate.lo mkdir objs 2>/dev/null || test -d objs gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -DPIC -c -o objs/inftrees.o ../zlib-1.3.1/inftrees.c mv objs/inftrees.o inftrees.lo mkdir objs 2>/dev/null || test -d objs gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -DPIC -c -o objs/trees.o ../zlib-1.3.1/trees.c mv objs/trees.o trees.lo mkdir objs 2>/dev/null || test -d objs gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -DPIC -c -o objs/zutil.o ../zlib-1.3.1/zutil.c mv objs/zutil.o zutil.lo mkdir objs 2>/dev/null || test -d objs gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -DPIC -c -o objs/compress.o ../zlib-1.3.1/compress.c mv objs/compress.o compress.lo mkdir objs 2>/dev/null || test -d objs gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -DPIC -c -o objs/uncompr.o ../zlib-1.3.1/uncompr.c mv objs/uncompr.o uncompr.lo mkdir objs 2>/dev/null || test -d objs gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -DPIC -c -o objs/gzclose.o ../zlib-1.3.1/gzclose.c mv objs/gzclose.o gzclose.lo mkdir objs 2>/dev/null || test -d objs gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -DPIC -c -o objs/gzlib.o ../zlib-1.3.1/gzlib.c mv objs/gzlib.o gzlib.lo mkdir objs 2>/dev/null || test -d objs gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -DPIC -c -o objs/gzread.o ../zlib-1.3.1/gzread.c mv objs/gzread.o gzread.lo mkdir objs 2>/dev/null || test -d objs gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -DPIC -c -o objs/gzwrite.o ../zlib-1.3.1/gzwrite.c mv objs/gzwrite.o gzwrite.lo gcc -shared -Wl,-soname,libz.so.1,--version-script,../zlib-1.3.1/zlib.map -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -o libz.so.1.3.1 adler32.lo crc32.lo deflate.lo infback.lo inffast.lo inflate.lo inftrees.lo trees.lo zutil.lo compress.lo uncompr.lo gzclose.lo gzlib.lo gzread.lo gzwrite.lo -lc rm -f libz.so libz.so.1 ln -s libz.so.1.3.1 libz.so ln -s libz.so.1.3.1 libz.so.1 rmdir objs gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -o examplesh example.o -L. libz.so.1.3.1 gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -o minigzipsh minigzip.o -L. libz.so.1.3.1 gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -I../zlib-1.3.1/ -D_FILE_OFFSET_BITS=64 -c -o example64.o ../zlib-1.3.1/test/example.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -o example64 example64.o -L. libz.a gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -I../zlib-1.3.1/ -D_FILE_OFFSET_BITS=64 -c -o minigzip64.o ../zlib-1.3.1/test/minigzip.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -o minigzip64 minigzip64.o -L. libz.a make: Leaving directory '/tmp/build' �������������������������rizsotto-Bear-0c30b27/tests/integration/tests/fixtures/data/zlib.make-n.sh��������������������������0000664�0000000�0000000�00000013634�15271777203�0026676�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -I../zlib-1.3.1/ -c -o example.o ../zlib-1.3.1/test/example.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -c -o adler32.o ../zlib-1.3.1/adler32.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -c -o crc32.o ../zlib-1.3.1/crc32.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -c -o deflate.o ../zlib-1.3.1/deflate.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -c -o infback.o ../zlib-1.3.1/infback.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -c -o inffast.o ../zlib-1.3.1/inffast.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -c -o inflate.o ../zlib-1.3.1/inflate.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -c -o inftrees.o ../zlib-1.3.1/inftrees.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -c -o trees.o ../zlib-1.3.1/trees.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -c -o zutil.o ../zlib-1.3.1/zutil.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -c -o compress.o ../zlib-1.3.1/compress.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -c -o uncompr.o ../zlib-1.3.1/uncompr.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -c -o gzclose.o ../zlib-1.3.1/gzclose.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -c -o gzlib.o ../zlib-1.3.1/gzlib.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -c -o gzread.o ../zlib-1.3.1/gzread.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -c -o gzwrite.o ../zlib-1.3.1/gzwrite.c ar rc libz.a adler32.o crc32.o deflate.o infback.o inffast.o inflate.o inftrees.o trees.o zutil.o compress.o uncompr.o gzclose.o gzlib.o gzread.o gzwrite.o (ranlib libz.a || true) >/dev/null 2>&1 gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -o example example.o -L. libz.a gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -I../zlib-1.3.1/ -c -o minigzip.o ../zlib-1.3.1/test/minigzip.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -o minigzip minigzip.o -L. libz.a mkdir objs 2>/dev/null || test -d objs gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -DPIC -c -o objs/adler32.o ../zlib-1.3.1/adler32.c mv objs/adler32.o adler32.lo mkdir objs 2>/dev/null || test -d objs gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -DPIC -c -o objs/crc32.o ../zlib-1.3.1/crc32.c mv objs/crc32.o crc32.lo mkdir objs 2>/dev/null || test -d objs gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -DPIC -c -o objs/deflate.o ../zlib-1.3.1/deflate.c mv objs/deflate.o deflate.lo mkdir objs 2>/dev/null || test -d objs gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -DPIC -c -o objs/infback.o ../zlib-1.3.1/infback.c mv objs/infback.o infback.lo mkdir objs 2>/dev/null || test -d objs gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -DPIC -c -o objs/inffast.o ../zlib-1.3.1/inffast.c mv objs/inffast.o inffast.lo mkdir objs 2>/dev/null || test -d objs gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -DPIC -c -o objs/inflate.o ../zlib-1.3.1/inflate.c mv objs/inflate.o inflate.lo mkdir objs 2>/dev/null || test -d objs gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -DPIC -c -o objs/inftrees.o ../zlib-1.3.1/inftrees.c mv objs/inftrees.o inftrees.lo mkdir objs 2>/dev/null || test -d objs gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -DPIC -c -o objs/trees.o ../zlib-1.3.1/trees.c mv objs/trees.o trees.lo mkdir objs 2>/dev/null || test -d objs gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -DPIC -c -o objs/zutil.o ../zlib-1.3.1/zutil.c mv objs/zutil.o zutil.lo mkdir objs 2>/dev/null || test -d objs gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -DPIC -c -o objs/compress.o ../zlib-1.3.1/compress.c mv objs/compress.o compress.lo mkdir objs 2>/dev/null || test -d objs gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -DPIC -c -o objs/uncompr.o ../zlib-1.3.1/uncompr.c mv objs/uncompr.o uncompr.lo mkdir objs 2>/dev/null || test -d objs gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -DPIC -c -o objs/gzclose.o ../zlib-1.3.1/gzclose.c mv objs/gzclose.o gzclose.lo mkdir objs 2>/dev/null || test -d objs gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -DPIC -c -o objs/gzlib.o ../zlib-1.3.1/gzlib.c mv objs/gzlib.o gzlib.lo mkdir objs 2>/dev/null || test -d objs gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -DPIC -c -o objs/gzread.o ../zlib-1.3.1/gzread.c mv objs/gzread.o gzread.lo mkdir objs 2>/dev/null || test -d objs gcc -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -include zconf.h -DPIC -c -o objs/gzwrite.o ../zlib-1.3.1/gzwrite.c mv objs/gzwrite.o gzwrite.lo gcc -shared -Wl,-soname,libz.so.1,--version-script,../zlib-1.3.1/zlib.map -O3 -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -o libz.so.1.3.1 adler32.lo crc32.lo deflate.lo infback.lo inffast.lo inflate.lo inftrees.lo trees.lo zutil.lo compress.lo uncompr.lo gzclose.lo gzlib.lo gzread.lo gzwrite.lo -lc rm -f libz.so libz.so.1 ln -s libz.so.1.3.1 libz.so ln -s libz.so.1.3.1 libz.so.1 rmdir objs gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -o examplesh example.o -L. libz.so.1.3.1 gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -o minigzipsh minigzip.o -L. libz.so.1.3.1 gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -I../zlib-1.3.1/ -D_FILE_OFFSET_BITS=64 -c -o example64.o ../zlib-1.3.1/test/example.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -o example64 example64.o -L. libz.a gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -I. -I../zlib-1.3.1/ -D_FILE_OFFSET_BITS=64 -c -o minigzip64.o ../zlib-1.3.1/test/minigzip.c gcc -O3 -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -o minigzip64 minigzip64.o -L. libz.a ����������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/tests/fixtures/external_dependencies.rs���������������������0000664�0000000�0000000�00000005610�15271777203�0030213�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: GPL-3.0-or-later use super::constants::*; use assert_cmd::Command; #[cfg(has_executable_true)] #[test] fn true_works() { Command::new(TRUE_PATH).assert().success(); } #[cfg(has_executable_false)] #[test] fn false_works() { Command::new(FALSE_PATH).assert().failure(); } #[cfg(has_executable_echo)] #[test] fn echo_works() { // Testing echo with as executing to print out a value. // Testing with `--help` or `--version` is not a portable test. Command::new(ECHO_PATH).arg("testing").assert().success(); } #[cfg(has_executable_sleep)] #[test] fn sleep_works() { // Testing sleep with a zero second value. // Testing with `--help` or `--version` is not a portable test. Command::new(SLEEP_PATH).arg("0").assert().success(); } #[cfg(has_executable_shell)] #[test] fn shell_works() { // Testing shell to execute a built it function. // Testing with `--help` or `--version` is not a portable test. Debian `dash` is failing with those arguments. Command::new(SHELL_PATH).args(["-c", "true"]).assert().success(); } #[cfg(has_executable_make)] #[test] fn make_works() { // Testing make by querying its version. Command::new(MAKE_PATH).arg("--version").assert().success(); } #[cfg(has_executable_compiler_c)] #[test] fn compiler_c_works() { // Testing compiler by querying its version. Command::new(COMPILER_C_PATH).arg("--version").assert().success(); } #[cfg(has_executable_compiler_cxx)] #[test] fn compiler_cxx_works() { // Testing compiler by querying its version. Command::new(COMPILER_CXX_PATH).arg("--version").assert().success(); } #[cfg(has_executable_compiler_fortran)] #[test] fn compiler_fortran_works() { // Testing compiler by querying its version. Command::new(COMPILER_FORTRAN_PATH).arg("--version").assert().success(); } #[cfg(has_executable_compiler_cuda)] #[test] fn compiler_cuda_works() { // Testing compiler by querying its version. Command::new(COMPILER_CUDA_PATH).arg("--version").assert().success(); } #[cfg(not(target_os = "macos"))] #[cfg(has_executable_libtool)] #[test] fn libtool_works() { // Testing libtool by querying its version. // FIXME: libtool does not have version or help parameters on macOS Command::new(LIBTOOL_PATH).arg("--version").assert().success(); } #[cfg(has_executable_fakeroot)] #[test] fn fakeroot_works() { // Testing fakeroot by querying its version. Command::new(FAKEROOT_PATH).arg("--version").assert().success(); } #[cfg(has_executable_valgrind)] #[test] fn valgrind_works() { // Testing valgrind by querying its version. Command::new(VALGRIND_PATH).arg("--version").assert().success(); } #[cfg(not(target_os = "macos"))] #[cfg(has_executable_ar)] #[test] fn ar_works() { // Testing ar by querying its version. // FIXME: ar does not have version or help parameters on macOS Command::new(AR_PATH).arg("--version").assert().success(); } ������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/tests/fixtures/infrastructure/������������������������������0000775�0000000�0000000�00000000000�15271777203�0026373�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/tests/fixtures/infrastructure/bear_output.rs����������������0000664�0000000�0000000�00000005061�15271777203�0031274�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: GPL-3.0-or-later use anyhow::Result; use std::path::PathBuf; use std::process::Output; /// Bear command output wrapper #[derive(Debug)] pub struct BearOutput { pub(super) output: Output, pub(super) temp_dir: PathBuf, } impl Clone for BearOutput { fn clone(&self) -> Self { Self { output: Output { status: self.output.status, stdout: self.output.stdout.clone(), stderr: self.output.stderr.clone(), }, temp_dir: self.temp_dir.clone(), } } } impl BearOutput { /// Dump bear's stdout, stderr, and exit code to the test binary's stderr. /// Used by `TestEnvironment::preserve_on_panic` for failure diagnostics. pub fn show_output(&self) { let stdout = String::from_utf8_lossy(&self.output.stdout); let stderr = String::from_utf8_lossy(&self.output.stderr); eprintln!("Bear stdout:"); if stdout.is_empty() { eprintln!(" (empty)"); } else { for line in stdout.lines() { eprintln!(" {}", line); } } eprintln!("Bear stderr:"); if stderr.is_empty() { eprintln!(" (empty)"); } else { for line in stderr.lines() { eprintln!(" {}", line); } } eprintln!("Bear exit code: {:?}", self.output.status.code()); } /// Assert that bear command succeeded pub fn assert_success(&self) -> Result<()> { if !self.output.status.success() { let stderr = String::from_utf8_lossy(&self.output.stderr); let stdout = String::from_utf8_lossy(&self.output.stdout); anyhow::bail!( "Bear command failed with exit code: {:?}\nstdout: {}\nstderr: {}", self.output.status.code(), stdout, stderr ); } Ok(()) } /// Assert that bear command failed pub fn assert_failure(&self) -> Result<()> { if self.output.status.success() { anyhow::bail!("Expected bear command to fail, but it succeeded"); } Ok(()) } /// Get stdout as string pub fn stdout(&self) -> String { String::from_utf8_lossy(&self.output.stdout).to_string() } /// Get stderr as string pub fn stderr(&self) -> String { String::from_utf8_lossy(&self.output.stderr).to_string() } /// Get exit code pub fn exit_code(&self) -> Option<i32> { self.output.status.code() } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/tests/fixtures/infrastructure/compilation_database.rs�������0000664�0000000�0000000�00000002575�15271777203�0033114�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: GPL-3.0-or-later use anyhow::Result; use serde_json::Value; // The order-independent matching core lives in the shared `bear-test-tools` // crate so the integration suite and the `cdb-compare` tool share one // implementation. Re-exported here to keep the existing test import path. pub use bear_test_tools::CompilationEntryMatcher; /// Compilation database wrapper with assertion helpers #[derive(Debug)] pub struct CompilationDatabase { pub(super) entries: Vec<Value>, } impl CompilationDatabase { /// Assert the number of entries pub fn assert_count(&self, expected: usize) -> Result<()> { let actual = self.entries.len(); if actual != expected { anyhow::bail!("Expected {} compilation entries, but found {}", expected, actual); } Ok(()) } /// Assert that the database contains an entry matching the criteria pub fn assert_contains(&self, matcher: &CompilationEntryMatcher) -> Result<()> { let found = self.entries.iter().any(|entry| matcher.matches(entry)); if !found { anyhow::bail!( "Expected to find compilation entry matching: {:?}\nActual entries: {:#?}", matcher, self.entries ); } Ok(()) } /// Get all entries pub fn entries(&self) -> &[Value] { &self.entries } } �����������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/tests/fixtures/infrastructure/install_environment.rs��������0000664�0000000�0000000�00000005400�15271777203�0033032�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: GPL-3.0-or-later use crate::fixtures::constants::*; use anyhow::{Context, Result}; use std::path::{Path, PathBuf}; /// Install environment for Bear /// /// On Unix, uses `scripts/install.sh` to create a proper installation layout /// in a temp directory — this exercises the install script on every test run. /// /// On Windows, runs `bear-driver.exe` directly (no preload library, no shell /// entry script). pub struct InstallEnvironment { install_dir: tempfile::TempDir, bear: PathBuf, } impl InstallEnvironment { pub fn new() -> Result<Self> { Self::install(None) } /// Install with `LIBEXECDIR` set to `subdir` inside the install prefix, /// instead of the default `libexec`. pub fn with_libexecdir(subdir: &str) -> Result<Self> { Self::install(Some(subdir)) } fn install(libexecdir: Option<&str>) -> Result<Self> { let install_dir = tempfile::TempDir::new().with_context(|| "install dir tempdir failed")?; if cfg!(unix) { let libexecdir = libexecdir.map(|subdir| install_dir.path().join(subdir)); Self::install_via_script(install_dir.path(), libexecdir.as_deref())?; let bear = install_dir.path().join("bin").join("bear"); Ok(Self { install_dir, bear }) } else { // Windows: run bear-driver.exe directly from the build output let bear = PathBuf::from(DRIVER_EXECUTABLE_PATH); Ok(Self { install_dir, bear }) } } fn install_via_script(prefix: &Path, libexecdir: Option<&Path>) -> Result<()> { let source_dir = Path::new(DRIVER_EXECUTABLE_PATH) .parent() .with_context(|| "cannot determine artifact directory from DRIVER_EXECUTABLE_PATH")?; let mut command = std::process::Command::new("sh"); command .arg(INSTALL_SCRIPT_PATH) .env("PREFIX", prefix) .env("INTERCEPT_LIBDIR", INTERCEPT_LIBDIR) .env("SRCDIR", source_dir); if let Some(libexecdir) = libexecdir { command.env("LIBEXECDIR", libexecdir); } let output = command.output().with_context(|| "failed to run install.sh")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); let stdout = String::from_utf8_lossy(&output.stdout); anyhow::bail!( "install.sh failed with exit code {:?}\nstdout: {}\nstderr: {}", output.status.code(), stdout, stderr ); } Ok(()) } /// The install prefix. pub fn install_dir(&self) -> &Path { self.install_dir.path() } pub fn path(&self) -> &Path { self.bear.as_path() } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/tests/fixtures/infrastructure/intercept_events.rs�����������0000664�0000000�0000000�00000013410�15271777203�0032321�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: GPL-3.0-or-later use anyhow::Result; use serde_json::Value; use std::path::PathBuf; /// Intercept events wrapper with assertion helpers #[derive(Debug)] pub struct InterceptEvents { pub(super) events: Vec<Value>, } impl InterceptEvents { /// Assert the number of events pub fn assert_count(&self, expected: usize) -> Result<()> { let actual = self.events.len(); if actual != expected { anyhow::bail!("Expected {} intercept events, but found {}", expected, actual); } Ok(()) } /// Assert that the events contain an entry matching the criteria pub fn assert_contains(&self, matcher: &EventMatcher) -> Result<()> { let found = self.events.iter().any(|event| matcher.matches(event)); if !found { anyhow::bail!( "Expected to find intercept event matching: {:?}\nActual events: {:#?}", matcher, self.events ); } Ok(()) } /// Count events matching specific criteria pub fn count_matching(&self, matcher: &EventMatcher) -> usize { self.events.iter().filter(|event| matcher.matches(event)).count() } /// Assert that events contain a specific number of entries matching the criteria pub fn assert_count_matching(&self, matcher: &EventMatcher, expected: usize) -> Result<()> { let actual = self.count_matching(matcher); if actual != expected { anyhow::bail!( "Expected {} intercept events matching {:?}, but found {}", expected, matcher, actual ); } Ok(()) } /// Assert that there are at least the minimum number of events pub fn assert_min_count(&self, min_expected: usize) -> Result<()> { let actual = self.events.len(); if actual < min_expected { anyhow::bail!("Expected at least {} intercept events, but found {}", min_expected, actual); } Ok(()) } /// Get all events pub fn events(&self) -> &[Value] { &self.events } } /// Matcher for intercept events #[derive(Debug)] pub struct EventMatcher { pub executable_name: Option<String>, pub executable_path: Option<String>, pub arguments: Option<Vec<String>>, pub working_directory: Option<String>, pub event_type: Option<String>, } impl EventMatcher { pub fn new() -> Self { Self { executable_name: None, executable_path: None, arguments: None, working_directory: None, event_type: None, } } pub fn executable_name<S: Into<String>>(mut self, name: S) -> Self { self.executable_name = Some(name.into()); self } pub fn executable_path<S: Into<String>>(mut self, path: S) -> Self { self.executable_path = Some(path.into()); self } pub fn arguments(mut self, arguments: Vec<String>) -> Self { self.arguments = Some(arguments); self } pub fn working_directory<S: Into<String>>(mut self, dir: S) -> Self { self.working_directory = Some(dir.into()); self } pub fn event_type<S: Into<String>>(mut self, event_type: S) -> Self { self.event_type = Some(event_type.into()); self } pub(super) fn matches(&self, execution: &Value) -> bool { // Check event type if specified if self.event_type.as_ref().is_some_and(|expected_type| execution.get(expected_type).is_none()) { return false; } // Check executable path if let Some(ref expected_path) = self.executable_path { if let Some(actual_path) = execution.get("executable").and_then(|v| v.as_str()) { if actual_path != expected_path { return false; } } else { return false; } } // Check executable name (basename of executable path) if let Some(ref expected_name) = self.executable_name { if let Some(actual_path) = execution.get("executable").and_then(|v| v.as_str()) { let actual_name = std::path::Path::new(actual_path) .file_name() .and_then(|n| n.to_str()) .unwrap_or(actual_path); if actual_name != expected_name && !actual_name.contains(expected_name) { return false; } } else { return false; } } // Check arguments if let Some(ref expected_args) = self.arguments { if let Some(actual_args) = execution.get("arguments").and_then(|v| v.as_array()) { let actual_str_args: Vec<String> = actual_args.iter().filter_map(|v| v.as_str()).map(|s| s.to_string()).collect(); if &actual_str_args != expected_args { return false; } } else { return false; } } // Check working directory if let Some(ref expected_dir) = self.working_directory { if let Some(actual_dir) = execution.get("working_directory").and_then(|v| v.as_str()) { // Canonicalize both paths for comparison let expected_canonical = std::fs::canonicalize(expected_dir).unwrap_or_else(|_| PathBuf::from(expected_dir)); let actual_canonical = std::fs::canonicalize(actual_dir).unwrap_or_else(|_| PathBuf::from(actual_dir)); if expected_canonical != actual_canonical { return false; } } else { return false; } } true } } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/tests/fixtures/infrastructure/mod.rs������������������������0000664�0000000�0000000�00000024407�15271777203�0027527�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: GPL-3.0-or-later //! Test infrastructure for Bear integration tests //! //! This module provides utilities for setting up test environments, //! running bear commands, and validating outputs. //! //! # Failure diagnostics //! //! When a test panics, `TestEnvironment::Drop` automatically dumps the last //! captured `BearOutput` (stdout, stderr, exit code) to the test binary's //! stderr. How rich that dump is depends on `RUST_LOG`: //! //! - Local default (no `RUST_LOG`) → `run_bear` sets `RUST_LOG=info`, so //! warn/info/error log lines are captured (tests that assert on them //! work) but per-event `debug` traces from the preload library are //! filtered out, keeping ccache-cached compilation stderr clean. //! - `RUST_LOG=debug cargo test` → propagated; bear logs verbosely and //! failure dumps include the full per-event interception trace. //! - CI sets `RUST_LOG=debug` so failures on platforms that can't be //! reproduced locally carry full diagnostic context without a re-run. //! //! `BEAR_TEST_PRESERVE_FAILURES=1` additionally preserves the temp directory //! at `/tmp/bear-test-<test_name>-<pid>` on panic. mod bear_output; mod compilation_database; mod install_environment; mod intercept_events; mod test_environment; #[cfg(test)] mod tests; pub use bear_output::BearOutput; pub use compilation_database::{CompilationDatabase, CompilationEntryMatcher}; pub use install_environment::InstallEnvironment; pub use intercept_events::{EventMatcher, InterceptEvents}; pub use test_environment::TestEnvironment; use anyhow::Result; use serde_json::json; use std::path::Path; /// Source content shared by the single-driver / launcher / duplicate-collapse /// semantic fixtures below: none of them care about the source's contents, /// only that a file named `hello.c` exists next to the crafted events file. const HELLO_C_SOURCE: &str = "int main() { return 0; }"; /// Helper macros for common test patterns #[macro_export] macro_rules! bear_test { ($name:ident, $body:expr) => { #[test] fn $name() -> anyhow::Result<()> { let env = TestEnvironment::new(stringify!($name))?; $body(&env) } }; } #[macro_export] macro_rules! compilation_entry { (file: $file:expr, directory: $dir:expr, arguments: $args:expr) => { $crate::fixtures::infrastructure::CompilationEntryMatcher::new() .file($file) .directory($dir) .arguments($args) }; } #[macro_export] macro_rules! event_matcher { (executable_path: $path:expr) => { $crate::fixtures::infrastructure::EventMatcher::new().executable_path($path) }; (executable_name: $name:expr) => { $crate::fixtures::infrastructure::EventMatcher::new().executable_name($name) }; (executable_path: $path:expr, arguments: $args:expr) => { $crate::fixtures::infrastructure::EventMatcher::new().executable_path($path).arguments($args) }; } // Re-export the macros at module level for easier importing pub use compilation_entry; pub use event_matcher; /// Helper function to get the appropriate compiler command for build scripts /// Always uses just the filename to ensure compatibility across all platforms pub fn filename_of(compiler_path: &str) -> String { Path::new(compiler_path).file_name().unwrap().to_string_lossy().to_string() } // Test helper functions for common operations pub fn touch_file(env: &TestEnvironment, path: &str) -> Result<()> { env.create_source_files(&[(path, "")])?; Ok(()) } pub fn create_c_file(env: &TestEnvironment, path: &str, content: &str) -> Result<()> { env.create_source_files(&[(path, content)])?; Ok(()) } /// Returns a PATH value that resolves no executables: an empty directory /// inside the test environment. /// /// Synthetic events carry a bare driver name (`swiftc`, `craycc`, ...). /// With no PATH in the event environment, Bear's executable resolution /// falls back to the host's confstr(_CS_PATH) - and when the driver name /// REALLY exists there (swiftc on a macOS runner), the recorded compiler /// token is rewritten to the host's absolute path and the literal-argv /// assertion breaks on that platform only. Pointing PATH at an empty /// directory makes resolution fail deterministically everywhere, so the /// bare name always survives verbatim. fn path_shield(env: &TestEnvironment) -> Result<String> { let shield = env.test_dir().join("path-shield"); std::fs::create_dir_all(&shield)?; Ok(shield.to_str().unwrap().to_string()) } /// Shared core for the per-compiler-family "recognition-style" semantic /// tests: create `dummy_executables` as empty files alongside a /// `source_file` (with `source_content`), write one event whose /// `executable`/`arguments` are `argv`, run `bear semantic`, and assert the /// resulting database holds exactly one entry for `source_file` whose /// recorded arguments equal `expected_args`. fn assert_semantic_event_yields_entry_for_source( env_name: &str, dummy_executables: &[&str], argv: &[&str], expected_args: &[&str], source_file: &str, source_content: &str, ) -> Result<()> { let env = TestEnvironment::new(env_name)?; let temp_dir = env.test_dir().to_str().unwrap().to_string(); let event = json!({ "executable": argv[0], "arguments": argv, "working_dir": temp_dir, "environment": { "PATH": path_shield(&env)? } }); let events_content = event.to_string(); let mut files: Vec<(&str, &str)> = vec![("events.json", events_content.as_str()), (source_file, source_content)]; for name in dummy_executables { files.push((name, "")); } env.create_source_files(&files)?; env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; db.assert_contains(&compilation_entry!( file: source_file.to_string(), directory: temp_dir.clone(), arguments: expected_args.iter().map(|s| s.to_string()).collect::<Vec<_>>() ))?; Ok(()) } /// Shared core for the per-compiler-family "recognition-style" semantic /// tests whose source is `hello.c`: create `dummy_executables` as empty /// files alongside a `hello.c` source, write one event whose /// `executable`/`arguments` are `argv`, run `bear semantic`, and assert the /// resulting database holds exactly one entry for `hello.c` whose recorded /// arguments equal `expected_args`. fn assert_semantic_event_yields_entry( env_name: &str, dummy_executables: &[&str], argv: &[&str], expected_args: &[&str], ) -> Result<()> { assert_semantic_event_yields_entry_for_source( env_name, dummy_executables, argv, expected_args, "hello.c", HELLO_C_SOURCE, ) } /// Common shape used by the per-compiler-family recognition tests (mpi /// wrapper, cray, hipcc, qnx, emcc, ti, xc8): a bare `<driver> -c /// hello.c`-style invocation must be recognized and its argv recorded /// verbatim as the sole compilation entry. Creates a dummy executable named /// `argv[0]` on disk (recognition only inspects the basename, but the path /// must exist). pub fn assert_driver_yields_single_entry(env_name: &str, argv: &[&str]) -> Result<()> { assert_semantic_event_yields_entry(env_name, &[argv[0]], argv, argv) } /// Same shape as [`assert_driver_yields_single_entry`], but for a family /// whose source isn't a `hello.c` translation unit (e.g. an assembler's /// `hello.asm`): the entry is asserted against `source_file`/ /// `source_content` instead of the fixed `hello.c` default. Creates a dummy /// executable named `argv[0]` on disk. pub fn assert_driver_yields_single_entry_for_source( env_name: &str, argv: &[&str], source_file: &str, source_content: &str, ) -> Result<()> { assert_semantic_event_yields_entry_for_source( env_name, &[argv[0]], argv, argv, source_file, source_content, ) } /// Launcher shape (e.g. icecc): the event's argv is the launcher token /// followed by the real compiler invocation, but the recorded entry drops /// the launcher token -- `expected_args` is what must survive. /// `dummy_executables` must list every basename referenced in `argv` that /// needs to exist on disk (the launcher and the wrapped compiler). pub fn assert_launcher_execution_yields_entry( env_name: &str, dummy_executables: &[&str], argv: &[&str], expected_args: &[&str], ) -> Result<()> { assert_semantic_event_yields_entry(env_name, dummy_executables, argv, expected_args) } /// Duplicate-collapse shape (e.g. mpi wrapper + child compiler, emcc + clang /// child): in preload mode a driver's own execution and its intercepted /// child compiler process both produce an event for the same source. The /// default duplicate filter (directory+file match) must collapse the pair /// to one entry, keeping the FIRST event's argv (the driver's, not the /// child's, since it comes first in the event stream). pub fn assert_duplicate_events_collapse_to_first( env_name: &str, first_argv: &[&str], second_argv: &[&str], ) -> Result<()> { let env = TestEnvironment::new(env_name)?; let temp_dir = env.test_dir().to_str().unwrap().to_string(); let shield = path_shield(&env)?; let first_event = json!({ "executable": first_argv[0], "arguments": first_argv, "working_dir": temp_dir, "environment": { "PATH": shield } }); let second_event = json!({ "executable": second_argv[0], "arguments": second_argv, "working_dir": temp_dir, "environment": { "PATH": shield } }); let events_content = format!("{}\n{}", first_event, second_event); env.create_source_files(&[ ("events.json", events_content.as_str()), ("hello.c", HELLO_C_SOURCE), (first_argv[0], ""), (second_argv[0], ""), ])?; env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?; let db = env.load_compilation_database("compile_commands.json")?; db.assert_count(1)?; db.assert_contains(&compilation_entry!( file: "hello.c".to_string(), directory: temp_dir.clone(), arguments: first_argv.iter().map(|s| s.to_string()).collect::<Vec<_>>() ))?; Ok(()) } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/tests/fixtures/infrastructure/test_environment.rs�����������0000664�0000000�0000000�00000034461�15271777203�0032354�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: GPL-3.0-or-later use super::{BearOutput, CompilationDatabase, InstallEnvironment, InterceptEvents}; use crate::fixtures::constants::*; use anyhow::{Context, Result}; use assert_cmd::Command; use assert_fs::{TempDir, prelude::*}; use predicates::prelude::*; use serde_json::{self, Value}; use std::fs; use std::path::{Path, PathBuf}; /// Test environment for Bear integration tests /// /// Manages temporary directories, file setup, and cleanup with /// debugging preservation on test failure. pub struct TestEnvironment { install: InstallEnvironment, test_dir: tempfile::TempDir, test_name: String, preserve_on_failure: bool, last_bear_output: std::cell::RefCell<Option<BearOutput>>, } impl TestEnvironment { /// Create a new test environment pub fn new(test_name: &str) -> Result<Self> { Self::with_install(test_name, InstallEnvironment::new()?) } /// Create a test environment that runs Bear from the given installation pub fn with_install(test_name: &str, install: InstallEnvironment) -> Result<Self> { let test_dir = tempfile::TempDir::new() .with_context(|| format!("Failed to create temp dir for test: {}", test_name))?; let preserve_on_failure = std::env::var("BEAR_TEST_PRESERVE_FAILURES") .map(|v| v == "1" || v.to_lowercase() == "true") .unwrap_or(false); Ok(Self { install, test_dir, test_name: test_name.to_string(), preserve_on_failure, last_bear_output: std::cell::RefCell::new(None), }) } /// Get the temporary directory path pub fn test_dir(&self) -> &Path { self.test_dir.path() } /// Path of the installed bear binary, for tests that must drive the /// process manually (for example a partial stdout read to break the /// pipe) instead of through the buffering `run_bear_*` helpers. pub fn bear_path(&self) -> &Path { self.install.path() } /// Create source files in the test directory pub fn create_source_files(&self, files: &[(&str, &str)]) -> Result<()> { for (path, content) in files { let file_path = self.test_dir().join(path); if let Some(parent) = file_path.parent() { fs::create_dir_all(parent) .with_context(|| format!("Failed to create directory: {:?}", parent))?; } fs::write(&file_path, content).with_context(|| format!("Failed to write file: {}", path))?; } Ok(()) } /// Create a build script in the test directory pub fn create_build_script(&self, script_name: &str, content: &str) -> Result<PathBuf> { let script_path = self.test_dir().join(script_name); fs::write(&script_path, content)?; // Make executable on Unix systems #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let mut perms = fs::metadata(&script_path)?.permissions(); perms.set_mode(0o755); fs::set_permissions(&script_path, perms)?; } Ok(script_path) } /// Create a shell script with proper shebang and shell detection #[cfg(has_executable_shell)] pub fn create_shell_script(&self, script_name: &str, commands: &str) -> Result<PathBuf> { let content = format!("#!{}\n{}", SHELL_PATH, commands); self.create_build_script(script_name, &content) } /// Create shell script with specific encoding #[cfg(has_executable_shell)] pub fn create_shell_script_with_encoding( &self, script_name: &str, commands: &str, encoding: &'static encoding_rs::Encoding, ) -> Result<PathBuf> { let content = format!("#!{}\n{}", SHELL_PATH, commands); self.create_build_script_with_encoding(script_name, &content, encoding) } /// Create build script with specific encoding pub fn create_build_script_with_encoding( &self, script_name: &str, content: &str, encoding: &'static encoding_rs::Encoding, ) -> Result<PathBuf> { let script_path = self.test_dir().join(script_name); // Encode content using the specified encoding let (encoded_bytes, _, had_errors) = encoding.encode(content); if had_errors { return Err(anyhow::anyhow!("Failed to encode content with {:?}", encoding.name())); } // Write the encoded bytes to file fs::write(&script_path, &*encoded_bytes)?; // Make executable on Unix systems #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let mut perms = fs::metadata(&script_path)?.permissions(); perms.set_mode(0o755); fs::set_permissions(&script_path, perms)?; } Ok(script_path) } /// Verify that a file is encoded with the specified encoding pub fn verify_file_encoding( &self, file_path: &Path, expected_encoding: &'static encoding_rs::Encoding, ) -> Result<bool> { let bytes = fs::read(file_path)?; // Try to decode with the expected encoding let (decoded_string, encoding_used, had_errors) = expected_encoding.decode(&bytes); // Check if decoding was successful and used the expected encoding Ok(!had_errors && encoding_used == expected_encoding && !decoded_string.is_empty()) } /// Create a Makefile in the test directory pub fn create_makefile(&self, makefile_name: &str, content: &str) -> Result<PathBuf> { let makefile_path = self.test_dir().join(makefile_name); fs::write(&makefile_path, content)?; Ok(makefile_path) } /// Create a configuration file (YAML format) pub fn create_config(&self, config_yaml: &str) -> Result<PathBuf> { let config_path = self.test_dir().join("config.yml"); fs::write(&config_path, config_yaml)?; Ok(config_path) } pub fn command_bear(&self) -> std::process::Command { std::process::Command::new(self.install.path()) } /// Run bear with the given arguments. /// /// `RUST_LOG` is inherited when set in the test process; otherwise it /// defaults to `info`. The default keeps warn/info/error log lines in /// captured stderr (so tests that assert on them work) without pulling /// in the noisy per-event `debug` traces from the preload library. CI /// sets `RUST_LOG=debug` explicitly to get the full diagnostic stream. /// `RUST_BACKTRACE=1` is always forced so panics in bear surface /// readable backtraces. pub fn run_bear(&self, args: &[&str]) -> Result<BearOutput> { let mut cmd = Command::new(self.install.path()); cmd.current_dir(self.test_dir()).env("RUST_BACKTRACE", "1").args(args); if std::env::var_os("RUST_LOG").is_none() { cmd.env("RUST_LOG", "info"); } let output = cmd.output()?; let bear_output = BearOutput { output, temp_dir: self.test_dir().to_path_buf() }; // Store the output for potential later display *self.last_bear_output.borrow_mut() = Some(bear_output.clone()); Ok(bear_output) } /// Run bear with the given arguments, feeding `stdin_bytes` to its /// standard input. Used to exercise `bear semantic --input -`, which /// reads the event stream from stdin instead of a file. /// /// See `run_bear` for the `RUST_LOG`/`RUST_BACKTRACE` defaults; this /// mirrors that behaviour and only adds the stdin payload. pub fn run_bear_with_stdin(&self, args: &[&str], stdin_bytes: &[u8]) -> Result<BearOutput> { let mut cmd = Command::new(self.install.path()); cmd.current_dir(self.test_dir()) .env("RUST_BACKTRACE", "1") .args(args) .write_stdin(stdin_bytes.to_vec()); if std::env::var_os("RUST_LOG").is_none() { cmd.env("RUST_LOG", "info"); } let output = cmd.output()?; let bear_output = BearOutput { output, temp_dir: self.test_dir().to_path_buf() }; // Store the output for potential later display *self.last_bear_output.borrow_mut() = Some(bear_output.clone()); Ok(bear_output) } /// Run bear with the given arguments, feeding `stdin_bytes` to its /// standard input, with `RUST_LOG` explicitly removed from the child /// environment (rather than defaulted to `info` like /// `run_bear_with_stdin`). Exercises the binary's *built-in* default log /// level (`env_logger`'s `default_filter_or` in `bear-driver`) instead /// of the harness's own `info` default, which would otherwise mask a /// regression in that built-in default. `RUST_BACKTRACE=1` is still /// forced so panics in bear surface readable backtraces. pub fn run_bear_with_stdin_default_log(&self, args: &[&str], stdin_bytes: &[u8]) -> Result<BearOutput> { let mut cmd = Command::new(self.install.path()); cmd.current_dir(self.test_dir()) .env("RUST_BACKTRACE", "1") .env_remove("RUST_LOG") .args(args) .write_stdin(stdin_bytes.to_vec()); let output = cmd.output()?; let bear_output = BearOutput { output, temp_dir: self.test_dir().to_path_buf() }; // Store the output for potential later display *self.last_bear_output.borrow_mut() = Some(bear_output.clone()); Ok(bear_output) } /// Run bear and expect success pub fn run_bear_success(&self, args: &[&str]) -> Result<BearOutput> { let result = self.run_bear(args)?; result.assert_success()?; Ok(result) } /// Run bear and expect failure pub fn run_bear_failure(&self, args: &[&str]) -> Result<BearOutput> { let result = self.run_bear(args)?; result.assert_failure()?; Ok(result) } /// Run C compiler directly (without Bear) to compile test programs /// /// This method provides semantic separation between compilation and Bear interception. /// Use this for compiling test programs that will later be executed under Bear's /// intercept mode, rather than using `run_bear` which would run the compiler through Bear. /// /// # Arguments /// * `output_name` - Name of the executable to produce /// * `source_files` - Array of source file paths to compile /// /// # Example /// ```ignore /// env.create_source_files(&[("test.c", "int main() { return 0; }")])?; /// let executable_path = env.run_c_compiler("test_program", &["test.c"])?; /// env.run_bear(&["intercept", "--output", "events.json", "--", "./test_program"])?; /// ``` #[cfg(has_executable_compiler_c)] pub fn run_c_compiler(&self, output_name: &str, source_files: &[&str]) -> Result<PathBuf> { let mut cmd = std::process::Command::new(COMPILER_C_PATH); cmd.current_dir(self.test_dir()); // Add output flag cmd.arg("-o").arg(output_name); // Add source files for source in source_files { cmd.arg(source); } let output = cmd.output().with_context(|| format!("Failed to run C compiler: {}", COMPILER_C_PATH))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); let stdout = String::from_utf8_lossy(&output.stdout); anyhow::bail!( "C compiler failed with exit code {:?}:\nstdout: {}\nstderr: {}", output.status.code(), stdout, stderr ); } // On Windows, executables have .exe extension let executable_name = if cfg!(windows) { format!("{}.exe", output_name) } else { output_name.to_string() }; let executable_path = self.test_dir().join(executable_name); Ok(executable_path) } /// Check if a file exists in the test directory pub fn file_exists(&self, path: &str) -> bool { self.test_dir().join(path).exists() } /// Read file content from test directory pub fn read_file(&self, path: &str) -> Result<String> { let file_path = self.test_dir().join(path); fs::read_to_string(&file_path).with_context(|| format!("Failed to read file: {}", path)) } /// Load compilation database from file pub fn load_compilation_database(&self, path: &str) -> Result<CompilationDatabase> { let db_path = self.test_dir().join(path); let content = fs::read_to_string(&db_path) .with_context(|| format!("Failed to read compilation database: {:?}", db_path))?; let entries: Vec<Value> = serde_json::from_str(&content).with_context(|| "Failed to parse compilation database JSON")?; Ok(CompilationDatabase { entries }) } /// Load intercept events file pub fn load_events_file(&self, path: &str) -> Result<InterceptEvents> { let events_path = self.test_dir().join(path); let content = fs::read_to_string(&events_path) .with_context(|| format!("Failed to read events file: {:?}", events_path))?; let events: Vec<Value> = content .lines() .map(serde_json::from_str) .collect::<Result<Vec<_>, _>>() .with_context(|| "Failed to parse events JSON lines")?; Ok(InterceptEvents { events }) } /// Dump the last captured bear output unconditionally; cargo's per-test /// capture discards it for passing tests and surfaces it for failing /// ones (both `Err` returns and panics). Optionally preserve the temp /// dir, but only on actual panic — `BEAR_TEST_PRESERVE_FAILURES` has /// always been panic-gated. fn preserve_on_panic(&self) { if let Some(ref output) = *self.last_bear_output.borrow() { eprintln!("\n=== Bear output (test: {}) ===", self.test_name); output.show_output(); eprintln!("=== end ===\n"); } if self.preserve_on_failure && std::thread::panicking() { let preserve_dir = format!("/tmp/bear-test-{}-{}", self.test_name, std::process::id()); if let Err(e) = fs::rename(self.test_dir(), &preserve_dir) { eprintln!("Failed to preserve test directory: {}", e); } else { eprintln!("Test failed. Directory preserved at: {}", preserve_dir); } } } } impl Drop for TestEnvironment { fn drop(&mut self) { self.preserve_on_panic(); } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/tests/fixtures/infrastructure/tests.rs����������������������0000664�0000000�0000000�00000011374�15271777203�0030111�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: GPL-3.0-or-later use super::*; use anyhow::Result; #[test] fn environment_creation() -> Result<()> { let env = TestEnvironment::new("test_creation")?; assert!(env.test_dir().exists()); Ok(()) } #[test] fn file_creation() -> Result<()> { let env = TestEnvironment::new("test_files")?; env.create_source_files(&[("test.c", "int main() { return 0; }"), ("subdir/test.h", "#pragma once")])?; assert!(env.test_dir().join("test.c").exists()); assert!(env.test_dir().join("subdir/test.h").exists()); Ok(()) } #[test] fn compilation_matcher() { let entry = serde_json::json!({ "file": "/path/to/test.c", "directory": "/path/to", "arguments": ["gcc", "-c", "test.c"] }); let matcher = CompilationEntryMatcher::new() .file("/path/to/test.c") .directory("/path/to") .arguments(vec!["gcc".to_string(), "-c".to_string(), "test.c".to_string()]); assert!(matcher.matches(&entry)); } #[test] fn event_matcher_executable_path() { let event = serde_json::json!({ "executable": "/usr/bin/gcc", "arguments": ["gcc", "-c", "test.c"], "working_directory": "/tmp" }); let matcher = EventMatcher::new().executable_path("/usr/bin/gcc"); assert!(matcher.matches(&event)); // Test non-matching path let matcher_no_match = EventMatcher::new().executable_path("/usr/bin/clang"); assert!(!matcher_no_match.matches(&event)); } #[test] fn event_matcher_executable_name() { let event = serde_json::json!({ "executable": "/usr/bin/gcc", "arguments": ["gcc", "-c", "test.c"] }); let matcher = EventMatcher::new().executable_name("gcc"); assert!(matcher.matches(&event)); // Test partial name matching let matcher_partial = EventMatcher::new().executable_name("gc"); assert!(matcher_partial.matches(&event)); // Test non-matching name let matcher_no_match = EventMatcher::new().executable_name("clang"); assert!(!matcher_no_match.matches(&event)); } #[test] fn event_matcher_arguments() { let event = serde_json::json!({ "executable": "/usr/bin/gcc", "arguments": ["gcc", "-c", "test.c", "-o", "test.o"] }); let matcher = EventMatcher::new().arguments(vec![ "gcc".to_string(), "-c".to_string(), "test.c".to_string(), "-o".to_string(), "test.o".to_string(), ]); assert!(matcher.matches(&event)); // Test non-matching arguments let matcher_no_match = EventMatcher::new().arguments(vec!["gcc".to_string(), "-c".to_string(), "other.c".to_string()]); assert!(!matcher_no_match.matches(&event)); } #[test] #[cfg(has_executable_compiler_c)] fn run_c_compiler_basic() -> Result<()> { let env = TestEnvironment::new("test_c_compiler")?; // Create a simple C program env.create_source_files(&[( "hello.c", r#" #include <stdio.h> int main() { printf("Hello, World!\n"); return 0; } "#, )])?; // Compile it using our new method let executable_path = env.run_c_compiler("hello", &["hello.c"])?; // Verify the executable exists at the returned path assert!(executable_path.exists()); // Verify the executable actually works by running it let output = std::process::Command::new(&executable_path) .current_dir(env.test_dir()) .output() .expect("Failed to run compiled executable"); assert!(output.status.success()); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("Hello, World!")); Ok(()) } #[test] #[cfg(has_executable_compiler_c)] fn run_c_compiler_error_handling() -> Result<()> { let env = TestEnvironment::new("test_c_compiler_error")?; // Create a C program with syntax errors env.create_source_files(&[( "broken.c", r#" #include <stdio.h> int main() { printf("Hello, World!\n" // Missing closing parenthesis and semicolon return 0; } "#, )])?; // Compilation should fail and return an error let compile_result = env.run_c_compiler("broken", &["broken.c"]); assert!(compile_result.is_err()); Ok(()) } #[test] fn assert_min_count_test() -> Result<()> { use serde_json::json; // Create mock events let events = vec![ json!({"executable": "/usr/bin/gcc", "arguments": ["gcc", "-c", "test.c"]}), json!({"executable": "/usr/bin/gcc", "arguments": ["gcc", "-c", "test2.c"]}), ]; let intercept_events = InterceptEvents { events }; // Should pass when actual >= min_expected assert!(intercept_events.assert_min_count(1).is_ok()); assert!(intercept_events.assert_min_count(2).is_ok()); // Should fail when actual < min_expected assert!(intercept_events.assert_min_count(3).is_err()); Ok(()) } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/tests/fixtures/mod.rs���������������������������������������0000664�0000000�0000000�00000000657�15271777203�0024450�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: GPL-3.0-or-later //! Test fixtures and infrastructure for Bear integration tests //! //! This module contains all the shared infrastructure, constants, and fixture tests //! that are used across the Bear integration test suite. pub mod constants; pub mod external_dependencies; pub mod infrastructure; // Re-export commonly used items for convenience pub use constants::*; pub use infrastructure::*; ���������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/integration/tests/integration.rs����������������������������������������0000664�0000000�0000000�00000001073�15271777203�0024334�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: GPL-3.0-or-later //! Bear integration test suite //! //! This module organizes the Bear integration tests into two main components: //! - `fixtures`: Test infrastructure, constants, and fixture tests for external dependencies //! - `cases`: Actual integration test cases that verify Bear functionality #![allow(clippy::unwrap_used, reason = "the whole crate is test code")] mod cases; #[allow( dead_code, unused_imports, reason = "tests are gated on host tools, so helpers used only by skipped tests go unused" )] mod fixtures; ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/tools/������������������������������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0017115�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/tools/CLAUDE.md���������������������������������������������������������0000664�0000000�0000000�00000001261�15271777203�0020374�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������## tests/tools The behavioral contract for `cdb-compare` (subcommands, normalization flags, invariants checks, JSON shapes, sampler) lives in `SPEC.md`. Read it before changing what the tool does, and keep it in sync when you change behavior. ## Single implementation `tests/integration` depends on this library so there is one comparison implementation, not two. `CompilationEntryMatcher` lives here and is re-exported by the integration fixtures; the dogfooding harness layers on the same library. Do not fork a second comparator - extend this one. ## Constraints - Tests are host-independent: they operate on in-memory or small fixture CDB JSON, never a live build or container. �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/tools/Cargo.toml��������������������������������������������������������0000664�0000000�0000000�00000001152�15271777203�0021044�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# SPDX-License-Identifier: GPL-3.0-or-later [package] name = "bear-test-tools" version.workspace = true authors.workspace = true repository.workspace = true homepage.workspace = true license.workspace = true edition.workspace = true rust-version.workspace = true publish = false [lib] name = "bear_test_tools" path = "src/lib.rs" [[bin]] name = "cdb-compare" path = "src/bin/cdb-compare.rs" [dependencies] anyhow.workspace = true clap = { workspace = true, features = ["derive"] } serde.workspace = true serde_json.workspace = true shell-words.workspace = true thiserror.workspace = true [lints] workspace = true ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/tools/SPEC.md�����������������������������������������������������������0000664�0000000�0000000�00000011507�15271777203�0020175�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# tests/tools specification The behavioral contract for the shared test tooling. The library exposes the compilation-database comparison and normalization logic; the binary `cdb-compare` wraps it. It is the single comparison implementation, used by both `tests/integration` (which re-exports `CompilationEntryMatcher`) and the dogfooding harness (`tests/dogfooding/SPEC.md`). Agent-maintenance rules for this crate live in `CLAUDE.md`. ## cdb-compare - `compare [NORM...] [--intersection] [--format human|json] A B` decides multiset equivalence (order-independent) and exits non-zero on non-equivalence. - `normalize [--sort] [NORM...] IN [-o OUT]` emits a canonical database (how a golden manifest is produced). Normalization operations (`NORM`) are individually toggleable and off by default. Both subcommands accept: - `--substitute-compiler V` - replace the first argument (compiler driver) with V. - `--relativize-paths ROOT` - rebase absolute `directory`/`file`/`output` against ROOT. - `--output-from-o` - rewrite each entry's `output` to the absolute object path derived from its `-o` argument resolved (lexically, no filesystem access) against `directory`. Last `-o` wins; no `-o` leaves `output` unchanged. This makes the (file, output) match key a true object identity across producers that encode `output` against different base directories. - `--drop-dependency-flags` - strip the dependency-file flags from `arguments`: `-MD`/`-MMD`/`-MP` (no argument) and `-MF`/`-MT`/`-MQ`/`-MJ` (drop the flag and its following token). They control only the build's `.d` side-file, so they are benign for a compilation-database comparison. `compare` also accepts a gating flag (not a normalization): - `--intersection` - gate the exit code on the `differing` set only. Entries present on just one side (`only_in_a`/`only_in_b`) are reported as advisory extras but do not fail the gate; exit 0 iff `differing` is empty AND at least one TU matched. The non-vacuity clause is deliberate: a comparison that pairs nothing (matched == 0, e.g. `--output-from-o` omitted so the match key never lines up) has an empty `differing` set and would otherwise pass green having compared nothing; instead it fails with a diagnostic on stderr. The human report gains a one-line summary (matched/differing/Bear-only/CMake-only); the JSON report is unchanged. Default behavior (flag absent) is unchanged: any non-empty set fails. - `invariants [NORM...] [--expected-objects N] [--tolerance PCT] [--min-entries N] [--format human|json] CDB` asserts structural invariants on one database and exits non-zero iff any check fails. The `NORM` flags apply before the checks (no sort), so the duplicate key uses normalized arguments. Checks: - `non-empty-arguments` (always): fails if any entry has empty `arguments`; offenders are those entries' `file`s. - `non-empty-directory` (always): fails if any entry has a blank `directory`; offenders are those entries' `file`s. A blank `directory` is non-replayable (a `cd ""` consumer lands in the wrong tree). - `no-true-duplicates` (always): groups entries by the full triple `(file, output, arguments)` post-normalization; any group of two or more is a true duplicate. The legitimate multi-output case (same `file`, different `output`/arguments) forms distinct groups and is not flagged. - `entry-count` (opt-in): present iff `--expected-objects` or `--min-entries` is given, else `"skipped"`. Passes iff every given constraint holds: `--expected-objects N` requires `|entries - N| <= ceil(N * PCT / 100)` (`--tolerance PCT`, default 0); `--min-entries M` requires `entries >= M`. The tool never walks the filesystem; the harness passes `N`. (Exact object-set membership is a deferred refinement.) JSON shape: `{"pass": bool, "checks": [{"name", "status": "pass"|"fail"|"skipped", "offenders"?, "detail"?}]}`. An offender is `{"file", "output"?, "count"?}` (absent fields omitted); `offenders` is omitted when empty and `detail` (`{"entries", "expected"?, "tolerance_pct"?, "min"?}`) appears only on the entry-count check. - `sample --count K [--build-dir DIR] CDB` selects up to `K` entries deterministically (stable order, no RNG) and prints one shell-safe replay line each. With `--build-dir DIR`, entries whose `arguments` carry no `-I` include path under `DIR` (attached `-I<path>` or split `-I <path>`, matched component-wise so `/buildother` is not under `/build`) rank first; the rest fill up to `min(K, total)`. `--count 0` emits nothing; `K` larger than the database emits all. Each line is the entry's `directory` followed by its `arguments`, every token individually quoted with `shell_words::quote` and space-joined, so a jq-less consumer does `eval "set -- $line"; dir=$1; shift; ( cd "$dir" && "$@" ... )`. `sample` takes no normalization flags and never execs. �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/tools/src/��������������������������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0017704�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/tools/src/bin/����������������������������������������������������������0000775�0000000�0000000�00000000000�15271777203�0020454�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/tools/src/bin/cdb-compare.rs��������������������������������������������0000664�0000000�0000000�00000031150�15271777203�0023176�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: GPL-3.0-or-later //! `cdb-compare`: compare or normalize Clang JSON compilation databases. //! //! `compare` decides equivalence as an order-independent multiset comparison //! over normalized entries and prints a three-set diff report; it exits //! non-zero on non-equivalence so it composes as a shell gate. `normalize` //! emits a canonical database, the way a golden manifest is produced. use std::fs::File; use std::io::{self, BufWriter, Write}; use std::path::{Path, PathBuf}; use std::process::ExitCode; use anyhow::{Context, Result}; use clap::{Args, Parser, Subcommand, ValueEnum}; use bear_test_tools::cdb::{CompilationDatabase, Normalization}; use bear_test_tools::compare::{DiffReport, compare}; use bear_test_tools::invariants::{self, CountExpectation}; use bear_test_tools::sample; #[derive(Debug, Parser)] #[command(name = "cdb-compare", about = "Compare or normalize JSON compilation databases")] struct Cli { #[command(subcommand)] command: Command, } #[derive(Debug, Subcommand)] enum Command { /// Compare two databases for multiset equivalence (order-independent). Compare(CompareArgs), /// Emit a canonical compilation database (for golden manifests). Normalize(NormalizeArgs), /// Assert structural invariants on a single database. Invariants(InvariantsArgs), /// Select replayable entries and emit shell-safe replay lines. Sample(SampleArgs), } /// Normalization flags shared by both subcommands. Every operation is off by /// default; pass the corresponding flag to enable it. #[derive(Debug, Args)] struct NormalizationArgs { /// Replace the first argument (compiler driver) with this value. #[arg(long, value_name = "VALUE")] substitute_compiler: Option<String>, /// Rebase absolute paths against this root. #[arg(long, value_name = "ROOT")] relativize_paths: Option<PathBuf>, /// Rewrite each entry's `output` to the absolute object path derived from /// its `-o` argument resolved against `directory`. #[arg(long)] output_from_o: bool, /// Strip the dependency-file flags (`-MD`/`-MMD`/`-MP` and the /// argument-consuming `-MF`/`-MT`/`-MQ`/`-MJ`) from `arguments`. #[arg(long)] drop_dependency_flags: bool, } impl NormalizationArgs { fn into_normalization(self, sort: bool) -> Normalization { Normalization { sort, substitute_compiler: self.substitute_compiler, relativize_paths: self.relativize_paths, output_from_o: self.output_from_o, drop_dependency_flags: self.drop_dependency_flags, } } } #[derive(Debug, Clone, Copy, ValueEnum)] enum Format { Human, Json, } #[derive(Debug, Args)] struct CompareArgs { #[command(flatten)] normalization: NormalizationArgs, /// Report format. #[arg(long, value_enum, default_value_t = Format::Human)] format: Format, /// Gate the exit code on the `differing` set only: entries present on just /// one side are reported as advisory extras but do not fail the comparison. #[arg(long)] intersection: bool, /// First database. a: PathBuf, /// Second database. b: PathBuf, } #[derive(Debug, Args)] struct NormalizeArgs { /// Emit entries in a canonical order. #[arg(long)] sort: bool, #[command(flatten)] normalization: NormalizationArgs, /// Input database. input: PathBuf, /// Output file; defaults to stdout. #[arg(short = 'o', long, value_name = "FILE")] output: Option<PathBuf>, } #[derive(Debug, Args)] struct InvariantsArgs { #[command(flatten)] normalization: NormalizationArgs, /// Expected object count; the entry count must land within `--tolerance` of it. #[arg(long, value_name = "N")] expected_objects: Option<usize>, /// Tolerance percent for the `--expected-objects` band; the band is /// `+/- ceil(N * PCT / 100)`. Defaults to 0 (an exact match). #[arg(long, value_name = "PCT", default_value_t = 0)] tolerance: usize, /// A simple floor on the entry count (assert `entries >= N`). #[arg(long, value_name = "N")] min_entries: Option<usize>, /// Report format. #[arg(long, value_enum, default_value_t = Format::Human)] format: Format, /// Database to check. cdb: PathBuf, } #[derive(Debug, Args)] struct SampleArgs { /// Maximum number of entries to emit. #[arg(long, value_name = "K")] count: usize, /// Prefer entries with no `-I` include path under this directory. #[arg(long, value_name = "DIR")] build_dir: Option<PathBuf>, /// Database to sample. cdb: PathBuf, } fn load(path: &Path) -> Result<CompilationDatabase> { let file = File::open(path).with_context(|| format!("failed to open {}", path.display()))?; CompilationDatabase::from_reader(file).with_context(|| format!("failed to read {}", path.display())) } /// A four-number tally of a comparison, computed at the CLI layer from the /// report plus the side-A length (the report itself carries no matched total, /// since fully-equal matched entries cancel during comparison). struct Summary { matched: usize, only_in_a: usize, only_in_b: usize, differing: usize, } impl Summary { /// `matched` (matched-equal plus differing) is everything in A that did not /// fall into `only_in_a`. fn from_report(report: &DiffReport, a_len: usize) -> Self { Summary { matched: a_len - report.only_in_a.len(), only_in_a: report.only_in_a.len(), only_in_b: report.only_in_b.len(), differing: report.differing.len(), } } /// One-line tally for the human report. `only_in_a`/`only_in_b` are labelled /// Bear-only/CMake-only after the oracle invocation order `bear.json cmake.json`. fn to_line(&self) -> String { format!( "summary: matched={} differing={} Bear-only={} CMake-only={}\n", self.matched, self.differing, self.only_in_a, self.only_in_b ) } } /// Decide the exit code from a report. Without `intersection` (the default) any /// non-empty set fails; with it the gate is the `differing` set alone, so /// only-on-one-side extras are advisory. Intersection additionally requires /// non-vacuity: at least one TU must have matched, otherwise a comparison that /// paired nothing (e.g. a missing normalization that breaks matching) has an /// empty `differing` set and would pass green having compared nothing. fn gate_succeeds(report: &DiffReport, intersection: bool, matched: usize) -> bool { if intersection { report.differing.is_empty() && matched > 0 } else { report.is_equivalent() } } fn run_compare(args: CompareArgs) -> Result<ExitCode> { let norm = args.normalization.into_normalization(false); let mut a = load(&args.a)?; let mut b = load(&args.b)?; a.normalize(&norm); b.normalize(&norm); let report = compare(&a, &b); let summary = Summary::from_report(&report, a.entries.len()); match args.format { Format::Human => { print!("{}", report.to_human()); if args.intersection { print!("{}", summary.to_line()); } } Format::Json => { println!("{}", serde_json::to_string_pretty(&report).context("failed to serialize report")?) } } // Diagnose the vacuous intersection on stderr so it never corrupts the // stdout report (which may be JSON the caller archives). if args.intersection && summary.matched == 0 { eprintln!("no translation units matched; the intersection is empty - nothing was compared"); } Ok(if gate_succeeds(&report, args.intersection, summary.matched) { ExitCode::SUCCESS } else { ExitCode::FAILURE }) } fn run_normalize(args: NormalizeArgs) -> Result<ExitCode> { let sort = args.sort; let norm = args.normalization.into_normalization(sort); let mut db = load(&args.input)?; db.normalize(&norm); match args.output { Some(path) => { let file = File::create(&path).with_context(|| format!("failed to create {}", path.display()))?; db.to_writer(BufWriter::new(file)) .with_context(|| format!("failed to write {}", path.display()))?; } None => db.to_writer(io::stdout().lock()).context("failed to write to stdout")?, } Ok(ExitCode::SUCCESS) } fn run_invariants(args: InvariantsArgs) -> Result<ExitCode> { // Normalize first (no sort - order is irrelevant to the checks) so the // duplicate key uses normalized arguments automatically. let norm = args.normalization.into_normalization(false); let mut db = load(&args.cdb)?; db.normalize(&norm); let expectation = CountExpectation { expected_objects: args.expected_objects, tolerance_pct: args.tolerance, min_entries: args.min_entries, }; let report = invariants::check(&db, &expectation); match args.format { Format::Human => print!("{}", report.to_human()), Format::Json => { println!("{}", serde_json::to_string_pretty(&report).context("failed to serialize report")?) } } Ok(if report.pass { ExitCode::SUCCESS } else { ExitCode::FAILURE }) } fn run_sample(args: SampleArgs) -> Result<ExitCode> { let db = load(&args.cdb)?; let selected = sample::select(&db, args.count, args.build_dir.as_deref()); let stdout = io::stdout(); let mut out = BufWriter::new(stdout.lock()); for entry in selected { writeln!(out, "{}", sample::to_line(entry)).context("failed to write to stdout")?; } Ok(ExitCode::SUCCESS) } fn main() -> Result<ExitCode> { let cli = Cli::parse(); match cli.command { Command::Compare(args) => run_compare(args), Command::Normalize(args) => run_normalize(args), Command::Invariants(args) => run_invariants(args), Command::Sample(args) => run_sample(args), } } #[cfg(test)] mod tests { use super::*; use bear_test_tools::cdb::Entry; fn entry(file: &str) -> Entry { Entry { directory: "/w".to_string(), file: file.to_string(), arguments: vec!["cc".to_string(), "-c".to_string(), file.to_string()], output: Some("a.o".to_string()), } } /// A report with extras on both sides but no matched-but-differing entries - /// the discriminating case between the default gate and `--intersection`. fn report_with_extras_only() -> DiffReport { DiffReport { only_in_a: vec![entry("/w/a.c")], only_in_b: vec![entry("/w/b.c")], differing: Vec::new(), } } /// A report whose only divergence is a matched-but-differing entry - the /// case the intersection gate must still fail on. fn report_with_differing_only() -> DiffReport { use bear_test_tools::compare::DifferingEntry; DiffReport { only_in_a: Vec::new(), only_in_b: Vec::new(), differing: vec![DifferingEntry { file: "/w/a.c".to_string(), output: Some("a.o".to_string()), a_directory: "/w".to_string(), b_directory: "/w".to_string(), a_arguments: vec!["cc".to_string(), "-O0".to_string()], b_arguments: vec!["cc".to_string(), "-O2".to_string()], }], } } #[test] fn intersection_gate_passes_when_only_extras_differ() { // Empty `differing` and a non-empty match set (matched > 0): the extras // are advisory under intersection, but still fail the default gate. let sut = report_with_extras_only(); assert!(gate_succeeds(&sut, true, 2)); assert!(!gate_succeeds(&sut, false, 2)); } #[test] fn intersection_gate_fails_when_differing_is_non_empty() { let sut = report_with_differing_only(); assert!(!gate_succeeds(&sut, true, 1)); assert!(!gate_succeeds(&sut, false, 1)); } #[test] fn intersection_gate_fails_when_nothing_matched() { // The same extras-only report, but with zero matched TUs (everything in // A is exclusive to A): the intersection is vacuous, so the gate must // not pass despite an empty `differing` set. let sut = report_with_extras_only(); assert!(!gate_succeeds(&sut, true, 0)); } #[test] fn summary_counts_matched_as_a_len_minus_only_in_a() { // Side A had three entries; one fell into only_in_a, so two matched. let report = report_with_extras_only(); let sut = Summary::from_report(&report, 3); assert_eq!(sut.matched, 2); assert_eq!(sut.only_in_a, 1); assert_eq!(sut.only_in_b, 1); assert_eq!(sut.differing, 0); } } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/tools/src/cdb.rs��������������������������������������������������������0000664�0000000�0000000�00000056202�15271777203�0021007�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: GPL-3.0-or-later //! Compilation database model, parsing, serialization, and normalization. //! //! A Clang JSON compilation database is an array of entries. Each entry has a //! `directory`, a `file`, and the compiler invocation expressed either as an //! `arguments` array or as a `command` string; `output` is optional. This //! module parses both forms into a single in-memory [`Entry`], and applies the //! explicit, individually toggleable normalization operations the comparator //! uses (see [`Normalization`]). use std::io::{BufReader, Read, Write}; use std::path::{Path, PathBuf}; use serde::Serializer as _; use serde::{Deserialize, Serialize}; /// Errors raised while reading or normalizing a compilation database. #[derive(Debug, thiserror::Error)] pub enum CdbError { #[error("i/o error while reading or writing the compilation database: {0}")] Io(#[from] std::io::Error), #[error("failed to parse compilation database JSON: {0}")] Json(#[from] serde_json::Error), #[error("entry {index} has neither an 'arguments' array nor a 'command' string")] MissingInvocation { index: usize }, #[error("entry {index} has a 'command' string that is not valid shell words: {source}")] BadCommand { index: usize, source: shell_words::ParseError }, } /// A single compilation database entry in normalized in-memory form. /// /// Both the `arguments` and `command` input encodings are folded into /// `arguments` here; serialization always emits the `arguments` form. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct Entry { pub directory: String, pub file: String, pub arguments: Vec<String>, pub output: Option<String>, } /// Raw on-disk shape of an entry, accepting both `arguments` and `command`. #[derive(Debug, Deserialize)] struct RawEntry { directory: String, file: String, #[serde(default)] arguments: Option<Vec<String>>, #[serde(default)] command: Option<String>, #[serde(default)] output: Option<String>, } /// On-disk shape used when serializing a normalized entry. #[derive(Debug, Serialize)] struct OutEntry<'a> { directory: &'a str, file: &'a str, arguments: &'a [String], #[serde(skip_serializing_if = "Option::is_none")] output: Option<&'a str>, } impl Entry { fn from_raw(index: usize, raw: RawEntry) -> Result<Self, CdbError> { let arguments = match (raw.arguments, raw.command) { (Some(args), _) => args, (None, Some(command)) => { shell_words::split(&command).map_err(|source| CdbError::BadCommand { index, source })? } (None, None) => return Err(CdbError::MissingInvocation { index }), }; Ok(Entry { directory: raw.directory, file: raw.file, arguments, output: raw.output }) } fn to_out(&self) -> OutEntry<'_> { OutEntry { directory: &self.directory, file: &self.file, arguments: &self.arguments, output: self.output.as_deref(), } } /// A stable key for matching two entries that describe the same translation /// unit: the `file` plus the `output` (which disambiguates one source /// compiled into several objects with different flags). pub fn match_key(&self) -> (String, Option<String>) { (self.file.clone(), self.output.clone()) } } /// A parsed compilation database. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CompilationDatabase { pub entries: Vec<Entry>, } impl CompilationDatabase { /// Parse a compilation database from a reader over its JSON text. /// /// The reader is consumed incrementally, so no full-text copy is held /// alongside the parsed entries. The entries themselves are still /// materialized into memory; streaming them is a separate step. pub fn from_reader<R: Read>(reader: R) -> Result<Self, CdbError> { let raw: Vec<RawEntry> = serde_json::from_reader(BufReader::new(reader))?; let entries = raw .into_iter() .enumerate() .map(|(index, e)| Entry::from_raw(index, e)) .collect::<Result<_, _>>()?; Ok(CompilationDatabase { entries }) } /// Serialize the database as pretty-printed JSON to a writer (always the /// `arguments` form, with a trailing newline). /// /// Entries are streamed to the writer as they are visited, so no full-text /// copy of the output is buffered in memory. pub fn to_writer<W: Write>(&self, mut writer: W) -> Result<(), CdbError> { let out = self.entries.iter().map(Entry::to_out); let mut serializer = serde_json::Serializer::pretty(&mut writer); serializer.collect_seq(out)?; writer.write_all(b"\n")?; Ok(()) } /// Apply the requested normalization operations in place. /// /// The operations are independent and off by default; only those set on /// `norm` run. `sort` is applied last so the canonical order reflects the /// post-substitution, post-relativization values. pub fn normalize(&mut self, norm: &Normalization) { if norm.output_from_o { for entry in &mut self.entries { if let Some(output) = output_from_o(&entry.directory, &entry.arguments) { entry.output = Some(output); } } } if norm.drop_dependency_flags { for entry in &mut self.entries { entry.arguments = drop_dependency_flags(&entry.arguments); } } if let Some(canonical) = &norm.substitute_compiler { for entry in &mut self.entries { if let Some(first) = entry.arguments.first_mut() { *first = canonical.clone(); } } } if let Some(root) = &norm.relativize_paths { for entry in &mut self.entries { entry.directory = relativize(&entry.directory, root); entry.file = relativize(&entry.file, root); if let Some(output) = &mut entry.output { *output = relativize(output, root); } } } if norm.sort { self.entries.sort_by(Entry::sort_key); } } } impl Entry { /// Total order over entries for the canonical `sort` normalization. The /// arguments are compared as-is; argument order inside an entry is /// semantically significant for a compiler invocation, so it is preserved. fn sort_key(a: &Entry, b: &Entry) -> std::cmp::Ordering { (&a.file, &a.output, &a.directory, &a.arguments).cmp(&( &b.file, &b.output, &b.directory, &b.arguments, )) } } /// The set of normalization operations to apply before comparison or /// serialization. Every field is off/empty by default; construct with /// [`Normalization::default`] and set only what a given comparison needs. #[derive(Debug, Clone, Default)] pub struct Normalization { /// Emit the database in a canonical entry order. pub sort: bool, /// Replace the first argument (compiler driver) with this canonical value. pub substitute_compiler: Option<String>, /// Rebase the absolute `directory`, `file`, and `output` paths against this /// root. Paths embedded inside `arguments` (e.g. `-I/abs/path`) are left /// untouched; argument-level rebasing is deferred until a real diff needs it. pub relativize_paths: Option<PathBuf>, /// Rewrite each entry's `output` to the absolute object path derived from the /// compiler's `-o` argument resolved against `directory` (lexically, without /// touching the filesystem). Makes the match key a true object identity that /// is identical across producers regardless of how each encodes `output`. pub output_from_o: bool, /// Strip the dependency-file generation flags (`-MD`, `-MMD`, `-MP`, and the /// argument-consuming `-MF`/`-MT`/`-MQ`/`-MJ`) from `arguments`. They only /// control the build system's `.d` side-file and never affect the produced /// object or how a Clang tool parses the translation unit. pub drop_dependency_flags: bool, } /// Rebase an absolute path string against `root`. A path that is not under /// `root` (or is already relative) is returned unchanged. Only the structured /// `directory`/`file`/`output` fields are rebased; paths embedded inside /// arguments are left untouched (see the `relativize_paths` field docs). fn relativize(value: &str, root: &Path) -> String { let path = Path::new(value); match path.strip_prefix(root) { Ok(rel) => rel.to_string_lossy().into_owned(), Err(_) => value.to_string(), } } /// Derive the absolute object path from the compiler's `-o` argument resolved /// against `directory`. Only the separate-token `-o <path>` form is recognized /// (the only form the captured data uses); an attached `-ofoo` is ignored. If /// `-o` appears more than once the last occurrence wins, matching compiler /// semantics. Returns `None` when there is no `-o` argument. fn output_from_o(directory: &str, arguments: &[String]) -> Option<String> { let mut value: Option<&str> = None; let mut iter = arguments.iter(); while let Some(arg) = iter.next() { if arg == "-o" && let Some(next) = iter.next() { value = Some(next); } } let value = value?; // POSIX path semantics, NOT the host's: a compilation database always names // files with `/` separators (it is produced inside a Linux container), and // the comparator must derive the same object path whether it runs on Linux, // macOS, or Windows. Going through `std::path` would join with the host // separator (`\` on Windows) and misjudge a leading-`/` path as relative // there, so resolve and normalize purely as `/`-delimited strings. let joined = if value.starts_with('/') { value.to_string() } else { format!("{}/{value}", directory.trim_end_matches('/')) }; Some(lexically_normalize(&joined)) } /// Collapse `.`, `..`, and redundant separators in a `/`-delimited path purely /// lexically - without `canonicalize`/stat - because the paths name files /// inside a throwaway container that does not exist on this host. A leading `..` /// (a path that escapes its root) is kept as-is rather than discarded. fn lexically_normalize(path: &str) -> String { let is_absolute = path.starts_with('/'); let mut stack: Vec<&str> = Vec::new(); for segment in path.split('/') { match segment { // Skip empty segments (redundant separators) and current-dir markers. "" | "." => {} ".." => match stack.last() { Some(&last) if last != ".." => { stack.pop(); } _ => stack.push(segment), }, other => stack.push(other), } } let joined = stack.join("/"); if is_absolute { format!("/{joined}") } else { joined } } /// Remove the dependency-file generation flags from `arguments`. `-MD`, `-MMD`, /// and `-MP` take no argument; `-MF`, `-MT`, `-MQ`, and `-MJ` each consume the /// single following token, which is dropped with them. Matching is on whole /// tokens only (the form the data uses); a trailing consuming flag with no /// following token is simply dropped without panicking. fn drop_dependency_flags(arguments: &[String]) -> Vec<String> { const NO_ARG: [&str; 3] = ["-MD", "-MMD", "-MP"]; const CONSUMES_ARG: [&str; 4] = ["-MF", "-MT", "-MQ", "-MJ"]; let mut result = Vec::with_capacity(arguments.len()); let mut iter = arguments.iter(); while let Some(arg) = iter.next() { if NO_ARG.contains(&arg.as_str()) { continue; } if CONSUMES_ARG.contains(&arg.as_str()) { iter.next(); continue; } result.push(arg.clone()); } result } #[cfg(test)] mod tests { use super::*; /// Parse from a string fixture through the reader API. fn parse(text: &str) -> Result<CompilationDatabase, CdbError> { CompilationDatabase::from_reader(text.as_bytes()) } /// Serialize through the writer API and recover the text for assertions. fn serialize(db: &CompilationDatabase) -> String { let mut buf = Vec::new(); db.to_writer(&mut buf).unwrap(); String::from_utf8(buf).unwrap() } fn arguments_db() -> &'static str { r#"[ { "directory": "/work/build", "file": "/work/src/a.c", "arguments": ["/usr/bin/cc", "-c", "/work/src/a.c", "-o", "a.o"], "output": "a.o" } ]"# } fn command_db() -> &'static str { r#"[ { "directory": "/work/build", "file": "/work/src/a.c", "command": "/usr/bin/cc -c /work/src/a.c -o a.o", "output": "a.o" } ]"# } #[test] fn parses_arguments_form() { let sut = parse(arguments_db()).unwrap(); assert_eq!(sut.entries.len(), 1); assert_eq!(sut.entries[0].arguments[0], "/usr/bin/cc"); assert_eq!(sut.entries[0].output.as_deref(), Some("a.o")); } #[test] fn parses_command_form_into_arguments() { let sut = parse(command_db()).unwrap(); assert_eq!(sut.entries[0].arguments, vec!["/usr/bin/cc", "-c", "/work/src/a.c", "-o", "a.o"]); } #[test] fn arguments_and_command_forms_compare_equal() { let from_args = parse(arguments_db()).unwrap(); let from_command = parse(command_db()).unwrap(); assert_eq!(from_args.entries, from_command.entries); } #[test] fn missing_invocation_is_an_error() { let json = r#"[{"directory": "/w", "file": "/w/a.c"}]"#; let sut = parse(json); assert!(matches!(sut, Err(CdbError::MissingInvocation { index: 0 }))); } #[test] fn substitute_compiler_replaces_first_argument() { let mut sut = parse(arguments_db()).unwrap(); let norm = Normalization { substitute_compiler: Some("cc".to_string()), ..Default::default() }; sut.normalize(&norm); assert_eq!(sut.entries[0].arguments[0], "cc"); } #[test] fn relativize_paths_rebases_fields_but_leaves_arguments_untouched() { let json = r#"[ { "directory": "/work/build", "file": "/work/src/a.c", "arguments": ["cc", "-I/work/include", "-c", "/work/src/a.c"], "output": "/work/build/a.o" } ]"#; let mut sut = parse(json).unwrap(); let norm = Normalization { relativize_paths: Some(PathBuf::from("/work")), ..Default::default() }; sut.normalize(&norm); let entry = &sut.entries[0]; assert_eq!(entry.directory, "build"); assert_eq!(entry.file, "src/a.c"); assert_eq!(entry.output.as_deref(), Some("build/a.o")); // Arguments are deliberately left as-is; only the structured fields rebase. assert_eq!(entry.arguments, vec!["cc", "-I/work/include", "-c", "/work/src/a.c"]); } #[test] fn sort_produces_canonical_order() { // Two input orderings must yield byte-identical canonical output. The // sort key covers all entry fields, so true ties are impossible and // stability is trivially satisfied; this asserts the canonical-order // property, which is what golden-manifest production relies on. let unsorted = r#"[ {"directory": "/w", "file": "/w/b.c", "arguments": ["cc", "-c", "b.c"]}, {"directory": "/w", "file": "/w/a.c", "arguments": ["cc", "-c", "a.c"]} ]"#; let reordered = r#"[ {"directory": "/w", "file": "/w/a.c", "arguments": ["cc", "-c", "a.c"]}, {"directory": "/w", "file": "/w/b.c", "arguments": ["cc", "-c", "b.c"]} ]"#; let norm = Normalization { sort: true, ..Default::default() }; let mut from_unsorted = parse(unsorted).unwrap(); from_unsorted.normalize(&norm); let mut from_reordered = parse(reordered).unwrap(); from_reordered.normalize(&norm); assert_eq!(from_unsorted.entries[0].file, "/w/a.c"); assert_eq!(serialize(&from_unsorted), serialize(&from_reordered)); } /// An entry whose `-o` value is relative to `directory` (Bear's encoding). fn entry_with_relative_o() -> &'static str { r#"[{ "directory": "/build/lib", "file": "/src/lib/altsvc.c", "arguments": ["cc", "-o", "CMakeFiles/libcurl_shared.dir/altsvc.c.o", "-c", "/src/lib/altsvc.c"], "output": "CMakeFiles/libcurl_shared.dir/altsvc.c.o" }]"# } /// The same object as `entry_with_relative_o`, but with the `output` field /// encoded relative to the build root (CMake's encoding). The `-o` value is /// still relative to `directory`, so `output_from_o` must reconcile them. fn entry_with_build_root_output() -> &'static str { r#"[{ "directory": "/build/lib", "file": "/src/lib/altsvc.c", "arguments": ["cc", "-o", "CMakeFiles/libcurl_shared.dir/altsvc.c.o", "-c", "/src/lib/altsvc.c"], "output": "lib/CMakeFiles/libcurl_shared.dir/altsvc.c.o" }]"# } #[test] fn output_from_o_reconciles_differently_encoded_outputs() { let norm = Normalization { output_from_o: true, ..Default::default() }; let mut bear = parse(entry_with_relative_o()).unwrap(); bear.normalize(&norm); let mut cmake = parse(entry_with_build_root_output()).unwrap(); cmake.normalize(&norm); let expected = Some("/build/lib/CMakeFiles/libcurl_shared.dir/altsvc.c.o".to_string()); assert_eq!(bear.entries[0].output, expected); assert_eq!(cmake.entries[0].output, expected); } #[test] fn output_from_o_preserves_absolute_o_value() { let json = r#"[{ "directory": "/build/lib", "file": "/src/a.c", "arguments": ["cc", "-o", "/build/lib/a.o", "-c", "/src/a.c"], "output": "a.o" }]"#; let mut sut = parse(json).unwrap(); let norm = Normalization { output_from_o: true, ..Default::default() }; sut.normalize(&norm); assert_eq!(sut.entries[0].output.as_deref(), Some("/build/lib/a.o")); } #[test] fn output_from_o_last_occurrence_wins() { // Compiler semantics: a repeated -o means the last value is the object. let json = r#"[{ "directory": "/build/lib", "file": "/src/a.c", "arguments": ["cc", "-o", "first.o", "-o", "second.o", "-c", "/src/a.c"] }]"#; let mut sut = parse(json).unwrap(); let norm = Normalization { output_from_o: true, ..Default::default() }; sut.normalize(&norm); assert_eq!(sut.entries[0].output.as_deref(), Some("/build/lib/second.o")); } #[test] fn output_from_o_leaves_output_unchanged_without_o_argument() { let json = r#"[{ "directory": "/build/lib", "file": "/src/a.c", "arguments": ["cc", "-c", "/src/a.c"], "output": "a.o" }]"#; let mut sut = parse(json).unwrap(); let norm = Normalization { output_from_o: true, ..Default::default() }; sut.normalize(&norm); assert_eq!(sut.entries[0].output.as_deref(), Some("a.o")); } #[test] fn output_from_o_keeps_multi_target_outputs_distinct() { // The same source compiled into two targets has distinct `-o` values, so // the derived absolute outputs must stay distinct (file-only matching // would falsely collapse them). let json = r#"[ { "directory": "/build/lib", "file": "/src/lib/base64.c", "arguments": ["cc", "-o", "CMakeFiles/libcurl_shared.dir/base64.c.o", "-c", "/src/lib/base64.c"] }, { "directory": "/build/src", "file": "/src/lib/base64.c", "arguments": ["cc", "-o", "CMakeFiles/curl.dir/__/lib/base64.c.o", "-c", "/src/lib/base64.c"] } ]"#; let mut sut = parse(json).unwrap(); let norm = Normalization { output_from_o: true, ..Default::default() }; sut.normalize(&norm); assert_eq!( sut.entries[0].output.as_deref(), Some("/build/lib/CMakeFiles/libcurl_shared.dir/base64.c.o") ); // The `..` in `__/lib` is not lexically collapsible here (it is a literal // directory name CMake emits, not a real parent reference), so it is kept. assert_eq!( sut.entries[1].output.as_deref(), Some("/build/src/CMakeFiles/curl.dir/__/lib/base64.c.o") ); assert_ne!(sut.entries[0].output, sut.entries[1].output); } #[test] fn drop_dependency_flags_removes_the_depfile_group() { let cases: &[(&[&str], &[&str])] = &[ // No-argument and consuming flags interleaved with kept flags. ( &["cc", "-MD", "-MT", "x.o", "-MF", "x.o.d", "-o", "x.o", "-c", "a.c"], &["cc", "-o", "x.o", "-c", "a.c"], ), // -MMD/-MP no-argument forms and -MQ/-MJ consuming forms. (&["cc", "-MMD", "-MP", "-MQ", "t", "-MJ", "db.json", "-c", "a.c"], &["cc", "-c", "a.c"]), // A trailing consuming flag with no following token must not panic. (&["cc", "-c", "a.c", "-MF"], &["cc", "-c", "a.c"]), // Lookalikes and ordinary flags are kept untouched. ( &["cc", "-MMD-not-a-flag", "-Map", "-O2", "-Wall", "-fPIC", "-c", "a.c"], &["cc", "-MMD-not-a-flag", "-Map", "-O2", "-Wall", "-fPIC", "-c", "a.c"], ), ]; for (input, expected) in cases { let arguments: Vec<String> = input.iter().map(|s| s.to_string()).collect(); let sut = super::drop_dependency_flags(&arguments); assert_eq!(sut, *expected, "case: {input:?}"); } } #[test] fn drop_dependency_flags_makes_depfile_only_difference_cancel() { use crate::compare::compare; let with_depflags = parse( r#"[{ "directory": "/build/lib", "file": "/src/a.c", "arguments": ["cc", "-MD", "-MT", "a.o", "-MF", "a.o.d", "-o", "a.o", "-c", "/src/a.c"], "output": "a.o" }]"#, ) .unwrap(); let without_depflags = parse( r#"[{ "directory": "/build/lib", "file": "/src/a.c", "arguments": ["cc", "-o", "a.o", "-c", "/src/a.c"], "output": "a.o" }]"#, ) .unwrap(); let norm = Normalization { drop_dependency_flags: true, ..Default::default() }; let mut left = with_depflags; left.normalize(&norm); let mut right = without_depflags; right.normalize(&norm); let sut = compare(&left, &right); assert!(sut.is_equivalent(), "report: {sut:?}"); } #[test] fn round_trips_through_reader_and_writer() { let original = parse(arguments_db()).unwrap(); let text = serialize(&original); let sut = parse(&text).unwrap(); assert_eq!(sut.entries, original.entries); } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/tools/src/compare.rs����������������������������������������������������0000664�0000000�0000000�00000027202�15271777203�0021703�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: GPL-3.0-or-later //! Order-independent comparison of two compilation databases. //! //! Equivalence is a multiset comparison over normalized entries, not a textual //! diff: two databases are equivalent when they contain the same entries with //! the same multiplicities, regardless of order. When they differ, the result //! is reported as three sets - entries only in A, only in B, and entries that //! match on `file` + `output` but differ in `arguments` or `directory`. use std::collections::HashMap; use serde::Serialize; use crate::cdb::{CompilationDatabase, Entry}; /// One side of a "matched but differing" pair: entries that agree on /// `file` + `output` but disagree on `arguments` and/or `directory`. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct DifferingEntry { pub file: String, pub output: Option<String>, pub a_directory: String, pub b_directory: String, pub a_arguments: Vec<String>, pub b_arguments: Vec<String>, } /// The outcome of comparing two databases. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct DiffReport { /// Entries present in A but absent from B (as a multiset). pub only_in_a: Vec<Entry>, /// Entries present in B but absent from A (as a multiset). pub only_in_b: Vec<Entry>, /// Entries matching on `file` + `output` across A and B but differing in /// `arguments` and/or `directory`. pub differing: Vec<DifferingEntry>, } impl DiffReport { /// The databases are equivalent when no entry is exclusive to either side /// and none of the matched entries differ. pub fn is_equivalent(&self) -> bool { self.only_in_a.is_empty() && self.only_in_b.is_empty() && self.differing.is_empty() } /// Render a human-readable summary of the differences. pub fn to_human(&self) -> String { if self.is_equivalent() { return "databases are equivalent\n".to_string(); } let mut out = String::new(); out.push_str("databases differ\n"); out.push_str(&format!("\nonly in A ({}):\n", self.only_in_a.len())); for entry in &self.only_in_a { out.push_str(&format!(" {}\n", describe(entry))); } out.push_str(&format!("\nonly in B ({}):\n", self.only_in_b.len())); for entry in &self.only_in_b { out.push_str(&format!(" {}\n", describe(entry))); } out.push_str(&format!("\nmatched but differing ({}):\n", self.differing.len())); for diff in &self.differing { let label = match &diff.output { Some(output) => format!("{} -> {}", diff.file, output), None => diff.file.clone(), }; out.push_str(&format!(" {label}\n")); if diff.a_directory != diff.b_directory { out.push_str(&format!(" directory: A={} B={}\n", diff.a_directory, diff.b_directory)); } if diff.a_arguments != diff.b_arguments { out.push_str(&format!(" arguments A: {}\n", diff.a_arguments.join(" "))); out.push_str(&format!(" arguments B: {}\n", diff.b_arguments.join(" "))); } } out } } fn describe(entry: &Entry) -> String { match &entry.output { Some(output) => format!("{} -> {} [{}]", entry.file, output, entry.arguments.join(" ")), None => format!("{} [{}]", entry.file, entry.arguments.join(" ")), } } /// Compare two databases as multisets of entries and produce a [`DiffReport`]. /// /// Identical entries (all fields equal) cancel out by multiplicity. Of the /// remainder, entries that share a `file` + `output` key on both sides are /// reported as "differing"; the rest are reported as exclusive to their side. pub fn compare(a: &CompilationDatabase, b: &CompilationDatabase) -> DiffReport { let mut leftover_a = subtract_common(&a.entries, &b.entries); let mut leftover_b = subtract_common(&b.entries, &a.entries); let mut differing = Vec::new(); // Pair up leftovers that share a file+output key; what does not pair is // exclusive to its side. let mut b_by_key: HashMap<(String, Option<String>), Vec<Entry>> = HashMap::new(); for entry in leftover_b.drain(..) { b_by_key.entry(entry.match_key()).or_default().push(entry); } let mut only_in_a = Vec::new(); for entry in leftover_a.drain(..) { match b_by_key.get_mut(&entry.match_key()).and_then(Vec::pop) { Some(b_entry) => differing.push(DifferingEntry { file: entry.file.clone(), output: entry.output.clone(), a_directory: entry.directory, b_directory: b_entry.directory, a_arguments: entry.arguments, b_arguments: b_entry.arguments, }), None => only_in_a.push(entry), } } let mut only_in_b: Vec<Entry> = b_by_key.into_values().flatten().collect(); // Deterministic ordering so reports and JSON output are reproducible. only_in_a.sort_by(sort_entries); only_in_b.sort_by(sort_entries); differing.sort_by(|x, y| (&x.file, &x.output).cmp(&(&y.file, &y.output))); DiffReport { only_in_a, only_in_b, differing } } /// Return the entries of `from` that have no equal counterpart in `other`, /// honouring multiplicity (an entry appearing twice in `from` and once in /// `other` yields one leftover). fn subtract_common(from: &[Entry], other: &[Entry]) -> Vec<Entry> { let mut available: HashMap<&Entry, usize> = HashMap::new(); for entry in other { *available.entry(entry).or_insert(0) += 1; } let mut leftover = Vec::new(); for entry in from { match available.get_mut(entry) { Some(count) if *count > 0 => *count -= 1, _ => leftover.push(entry.clone()), } } leftover } fn sort_entries(a: &Entry, b: &Entry) -> std::cmp::Ordering { (&a.file, &a.output, &a.directory, &a.arguments).cmp(&(&b.file, &b.output, &b.directory, &b.arguments)) } // `Entry` is used as a HashMap key in `subtract_common`; derive the hashing // from its public fields so equal entries collide. impl std::hash::Hash for Entry { fn hash<H: std::hash::Hasher>(&self, state: &mut H) { self.directory.hash(state); self.file.hash(state); self.arguments.hash(state); self.output.hash(state); } } #[cfg(test)] mod tests { use super::*; use crate::cdb::Normalization; use std::path::PathBuf; fn db(json: &str) -> CompilationDatabase { CompilationDatabase::from_reader(json.as_bytes()).unwrap() } fn entry_a() -> &'static str { r#"{"directory": "/w", "file": "/w/a.c", "arguments": ["cc", "-c", "a.c"], "output": "a.o"}"# } fn entry_b() -> &'static str { r#"{"directory": "/w", "file": "/w/b.c", "arguments": ["cc", "-c", "b.c"], "output": "b.o"}"# } #[test] fn identical_databases_are_equivalent() { let left = db(&format!("[{}, {}]", entry_a(), entry_b())); let right = db(&format!("[{}, {}]", entry_a(), entry_b())); let sut = compare(&left, &right); assert!(sut.is_equivalent(), "report: {sut:?}"); } #[test] fn reordered_databases_are_equivalent() { let left = db(&format!("[{}, {}]", entry_a(), entry_b())); let right = db(&format!("[{}, {}]", entry_b(), entry_a())); let sut = compare(&left, &right); assert!(sut.is_equivalent(), "report: {sut:?}"); } #[test] fn normalized_databases_are_equivalent() { // Same build seen with an absolute ccache compiler and an absolute build // root vs a canonical compiler name and a relative root. Only the // structured fields rebase; arguments are compared verbatim, so both // sides keep the same absolute source path inside `arguments`. let left = db(r#"[{ "directory": "/work/build", "file": "/work/src/a.c", "arguments": ["/usr/lib/ccache/cc", "-c", "/work/src/a.c"], "output": "a.o" }]"#); let right = db(r#"[{ "directory": "build", "file": "src/a.c", "arguments": ["cc", "-c", "/work/src/a.c"], "output": "a.o" }]"#); let mut normalized_left = left; normalized_left.normalize(&Normalization { substitute_compiler: Some("cc".to_string()), relativize_paths: Some(PathBuf::from("/work")), ..Default::default() }); let sut = compare(&normalized_left, &right); assert!(sut.is_equivalent(), "report: {sut:?}"); } #[test] fn detects_entries_only_in_a() { let left = db(&format!("[{}, {}]", entry_a(), entry_b())); let right = db(&format!("[{}]", entry_b())); let sut = compare(&left, &right); assert!(!sut.is_equivalent()); assert_eq!(sut.only_in_a.len(), 1); assert_eq!(sut.only_in_a[0].file, "/w/a.c"); assert!(sut.only_in_b.is_empty()); assert!(sut.differing.is_empty()); } #[test] fn detects_entries_only_in_b() { let left = db(&format!("[{}]", entry_a())); let right = db(&format!("[{}, {}]", entry_a(), entry_b())); let sut = compare(&left, &right); assert!(!sut.is_equivalent()); assert_eq!(sut.only_in_b.len(), 1); assert_eq!(sut.only_in_b[0].file, "/w/b.c"); assert!(sut.only_in_a.is_empty()); assert!(sut.differing.is_empty()); } #[test] fn detects_matched_but_differing_entries() { let left = db( r#"[{"directory": "/w", "file": "/w/a.c", "arguments": ["cc", "-O0", "-c", "a.c"], "output": "a.o"}]"#, ); let right = db( r#"[{"directory": "/w", "file": "/w/a.c", "arguments": ["cc", "-O2", "-c", "a.c"], "output": "a.o"}]"#, ); let sut = compare(&left, &right); assert!(!sut.is_equivalent()); assert!(sut.only_in_a.is_empty()); assert!(sut.only_in_b.is_empty()); assert_eq!(sut.differing.len(), 1); assert_eq!(sut.differing[0].file, "/w/a.c"); assert_eq!(sut.differing[0].a_arguments, vec!["cc", "-O0", "-c", "a.c"]); assert_eq!(sut.differing[0].b_arguments, vec!["cc", "-O2", "-c", "a.c"]); } #[test] fn detects_matched_but_differing_directory() { // Same file+output and same arguments, but the recorded directory // differs - the report must surface the directory disagreement. let left = db( r#"[{"directory": "/w/a", "file": "/w/a.c", "arguments": ["cc", "-c", "a.c"], "output": "a.o"}]"#, ); let right = db( r#"[{"directory": "/w/b", "file": "/w/a.c", "arguments": ["cc", "-c", "a.c"], "output": "a.o"}]"#, ); let sut = compare(&left, &right); assert_eq!(sut.differing.len(), 1, "report: {sut:?}"); assert_eq!(sut.differing[0].a_directory, "/w/a"); assert_eq!(sut.differing[0].b_directory, "/w/b"); assert_eq!(sut.differing[0].a_arguments, sut.differing[0].b_arguments); assert!(sut.only_in_a.is_empty()); assert!(sut.only_in_b.is_empty()); } #[test] fn multiset_honours_multiplicity() { let left = db(&format!("[{}, {}]", entry_a(), entry_a())); let right = db(&format!("[{}]", entry_a())); let sut = compare(&left, &right); assert_eq!(sut.only_in_a.len(), 1); assert!(sut.only_in_b.is_empty()); } #[test] fn human_report_is_empty_message_when_equivalent() { let left = db(&format!("[{}]", entry_a())); let right = db(&format!("[{}]", entry_a())); let sut = compare(&left, &right).to_human(); assert_eq!(sut, "databases are equivalent\n"); } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/tools/src/invariants.rs�������������������������������������������������0000664�0000000�0000000�00000043260�15271777203�0022435�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: GPL-3.0-or-later //! Structural invariants over a single compilation database. //! //! Stage 4 of the dogfooding plan needs baseline-free checks that a captured //! database is internally well-formed: no entry has an empty argument vector, //! no two entries are true duplicates, and (opt-in) the entry count lands in an //! expected band. The decision logic lives here with unit tests rather than in //! shell, because the duplicate check must reuse the normalization and get the //! legitimate multi-output case right (the Stage 3 lesson). //! //! The checks run over an already-normalized [`CompilationDatabase`]: the caller //! applies [`crate::cdb::Normalization`] first, so the duplicate key uses //! normalized arguments automatically. use serde::Serialize; use std::collections::HashMap; use crate::cdb::CompilationDatabase; /// The outcome of a single invariant check. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] #[serde(rename_all = "lowercase")] pub enum Status { Pass, Fail, Skipped, } /// One offending entry pointed at by a failing check. The optional fields are /// omitted when absent, so a `non-empty-arguments` offender serializes as just /// `{"file": "..."}` while a duplicate offender carries `output` and `count`. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct Offender { pub file: String, #[serde(skip_serializing_if = "Option::is_none")] pub output: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub count: Option<usize>, } /// The numeric detail attached to the entry-count check, describing what was /// asserted. Only the fields in play for the given flags are emitted. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct CountDetail { pub entries: usize, #[serde(skip_serializing_if = "Option::is_none")] pub expected: Option<usize>, #[serde(skip_serializing_if = "Option::is_none")] pub tolerance_pct: Option<usize>, #[serde(skip_serializing_if = "Option::is_none")] pub min: Option<usize>, } /// A single named check in the report. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct Check { pub name: String, pub status: Status, #[serde(skip_serializing_if = "Vec::is_empty")] pub offenders: Vec<Offender>, #[serde(skip_serializing_if = "Option::is_none")] pub detail: Option<CountDetail>, } /// The full invariants report for one database. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct InvariantsReport { pub pass: bool, pub checks: Vec<Check>, } impl InvariantsReport { /// Render the report as one readable line per check, with offenders or detail /// indented beneath a failing check so a failure localizes. pub fn to_human(&self) -> String { let mut out = String::new(); out.push_str(if self.pass { "invariants: pass\n" } else { "invariants: fail\n" }); for check in &self.checks { let status = match check.status { Status::Pass => "pass", Status::Fail => "fail", Status::Skipped => "skipped", }; out.push_str(&format!(" {} - {}\n", check.name, status)); for offender in &check.offenders { let mut line = format!(" {}", offender.file); if let Some(output) = &offender.output { line.push_str(&format!(" -> {output}")); } if let Some(count) = offender.count { line.push_str(&format!(" (x{count})")); } line.push('\n'); out.push_str(&line); } if let Some(detail) = &check.detail { let mut line = format!(" entries={}", detail.entries); if let Some(expected) = detail.expected { line.push_str(&format!(" expected={expected}")); } if let Some(tolerance) = detail.tolerance_pct { line.push_str(&format!(" tolerance_pct={tolerance}")); } if let Some(min) = detail.min { line.push_str(&format!(" min={min}")); } line.push('\n'); out.push_str(&line); } } out } } /// The opt-in constraints for the entry-count check. When both are absent the /// check is reported as skipped. #[derive(Debug, Clone, Copy, Default)] pub struct CountExpectation { /// Expected object count; the band is `+/- ceil(N * tolerance_pct / 100)`. pub expected_objects: Option<usize>, /// Tolerance percent for the expected-objects band; defaults to 0. pub tolerance_pct: usize, /// A simple floor: assert `entries >= min`. pub min_entries: Option<usize>, } impl CountExpectation { fn is_requested(&self) -> bool { self.expected_objects.is_some() || self.min_entries.is_some() } } /// Run the structural invariants over an already-normalized database. /// /// Deferred refinement: exact object-set membership (assert each produced `.o` /// has a matching entry) is not built here; the entry-count band approximates /// "every translation unit appears" for this MVP. pub fn check(database: &CompilationDatabase, expectation: &CountExpectation) -> InvariantsReport { let checks = vec![ non_empty_arguments(database), non_empty_directory(database), no_true_duplicates(database), entry_count(database, expectation), ]; let pass = checks.iter().all(|check| check.status != Status::Fail); InvariantsReport { pass, checks } } /// Every entry must carry a non-empty `arguments` vector. fn non_empty_arguments(database: &CompilationDatabase) -> Check { let offenders: Vec<Offender> = database .entries .iter() .filter(|entry| entry.arguments.is_empty()) .map(|entry| Offender { file: entry.file.clone(), output: None, count: None }) .collect(); let status = if offenders.is_empty() { Status::Pass } else { Status::Fail }; Check { name: "non-empty-arguments".to_string(), status, offenders, detail: None } } /// Every entry must carry a non-empty `directory`. A blank `directory` makes the /// recorded command non-replayable: a consumer that does `cd "$directory"` lands /// in an unrelated tree, and the replay loop's `cd ""` is a silent no-op that /// would validate the entry against the wrong working directory. fn non_empty_directory(database: &CompilationDatabase) -> Check { let offenders: Vec<Offender> = database .entries .iter() .filter(|entry| entry.directory.is_empty()) .map(|entry| Offender { file: entry.file.clone(), output: None, count: None }) .collect(); let status = if offenders.is_empty() { Status::Pass } else { Status::Fail }; Check { name: "non-empty-directory".to_string(), status, offenders, detail: None } } /// No two entries may be true duplicates. Entries are grouped by the full triple /// `(file, output, arguments)`; any group with two or more members is a true /// duplicate. Grouping by the full triple (not just `(file, output)`) is what /// keeps the legitimate multi-output case - the same source compiled into /// different objects with different flags - in distinct groups, so it is not /// flagged; it also produces the reported `{file, output, count}` shape. fn no_true_duplicates(database: &CompilationDatabase) -> Check { type Key<'a> = (&'a str, Option<&'a str>, &'a [String]); let mut groups: HashMap<Key, usize> = HashMap::new(); // Preserve first-seen order so the offender list is deterministic. let mut order: Vec<Key> = Vec::new(); for entry in &database.entries { let key: Key = (entry.file.as_str(), entry.output.as_deref(), entry.arguments.as_slice()); let count = groups.entry(key).or_insert_with(|| { order.push(key); 0 }); *count += 1; } let offenders: Vec<Offender> = order .into_iter() .filter_map(|key| { let count = groups[&key]; (count >= 2).then(|| Offender { file: key.0.to_string(), output: key.1.map(str::to_string), count: Some(count), }) }) .collect(); let status = if offenders.is_empty() { Status::Pass } else { Status::Fail }; Check { name: "no-true-duplicates".to_string(), status, offenders, detail: None } } /// Opt-in entry-count check. Skipped unless `--expected-objects` or /// `--min-entries` is given. When requested it passes iff every provided /// constraint holds (both, when both are given). fn entry_count(database: &CompilationDatabase, expectation: &CountExpectation) -> Check { let name = "entry-count".to_string(); if !expectation.is_requested() { return Check { name, status: Status::Skipped, offenders: Vec::new(), detail: None }; } let entries = database.entries.len(); let mut holds = true; let mut detail = CountDetail { entries, expected: None, tolerance_pct: None, min: None }; if let Some(expected) = expectation.expected_objects { // Integer band: |entries - expected| <= ceil(expected * pct / 100). // Saturating arithmetic throughout so an extreme expected/tolerance // pair cannot overflow (this is a test tool fed arbitrary numbers). let band = expected.saturating_mul(expectation.tolerance_pct).div_ceil(100); let low = expected.saturating_sub(band); let high = expected.saturating_add(band); holds &= entries >= low && entries <= high; detail.expected = Some(expected); detail.tolerance_pct = Some(expectation.tolerance_pct); } if let Some(min) = expectation.min_entries { holds &= entries >= min; detail.min = Some(min); } let status = if holds { Status::Pass } else { Status::Fail }; Check { name, status, offenders: Vec::new(), detail: Some(detail) } } #[cfg(test)] mod tests { use super::*; use crate::cdb::{Entry, Normalization}; /// A self-consistent entry with the given file, output, and arguments. fn entry(file: &str, output: Option<&str>, arguments: &[&str]) -> Entry { Entry { directory: "/build".to_string(), file: file.to_string(), arguments: arguments.iter().map(|s| s.to_string()).collect(), output: output.map(str::to_string), } } fn database(entries: Vec<Entry>) -> CompilationDatabase { CompilationDatabase { entries } } /// Find a check by name in a report. fn find<'a>(report: &'a InvariantsReport, name: &str) -> &'a Check { report.checks.iter().find(|check| check.name == name).expect("check present") } #[test] fn non_empty_arguments_fails_on_empty_and_passes_otherwise() { let cases: &[(Vec<Entry>, Status)] = &[ (vec![entry("/src/a.c", Some("a.o"), &[])], Status::Fail), (vec![entry("/src/a.c", Some("a.o"), &["cc", "-c", "/src/a.c"])], Status::Pass), ]; for (entries, expected) in cases { let sut = check(&database(entries.clone()), &CountExpectation::default()); assert_eq!(find(&sut, "non-empty-arguments").status, *expected, "entries: {entries:?}"); } } #[test] fn non_empty_arguments_offender_is_just_the_file() { let sut = check(&database(vec![entry("/src/a.c", Some("a.o"), &[])]), &CountExpectation::default()); let check = find(&sut, "non-empty-arguments"); assert_eq!(check.offenders.len(), 1); assert_eq!(check.offenders[0].file, "/src/a.c"); assert_eq!(check.offenders[0].output, None); assert_eq!(check.offenders[0].count, None); } #[test] fn non_empty_directory_fails_on_blank_and_passes_otherwise() { let blank = Entry { directory: String::new(), ..entry("/src/a.c", Some("a.o"), &["cc"]) }; let cases: &[(Vec<Entry>, Status)] = &[(vec![blank], Status::Fail), (vec![entry("/src/a.c", Some("a.o"), &["cc"])], Status::Pass)]; for (entries, expected) in cases { let sut = check(&database(entries.clone()), &CountExpectation::default()); assert_eq!(find(&sut, "non-empty-directory").status, *expected, "entries: {entries:?}"); } } #[test] fn byte_identical_entries_are_true_duplicates() { let one = entry("/src/a.c", Some("a.o"), &["cc", "-c", "/src/a.c", "-o", "a.o"]); let sut = check(&database(vec![one.clone(), one]), &CountExpectation::default()); let check = find(&sut, "no-true-duplicates"); assert_eq!(check.status, Status::Fail); assert_eq!(check.offenders.len(), 1); assert_eq!(check.offenders[0].count, Some(2)); } #[test] fn multi_output_same_source_is_not_a_duplicate() { // The same source compiled into two objects with different flags forms two // distinct groups and must not be flagged. let shared = entry("/src/a.c", Some("shared.o"), &["cc", "-fPIC", "-c", "/src/a.c", "-o", "shared.o"]); let static_ = entry("/src/a.c", Some("static.o"), &["cc", "-c", "/src/a.c", "-o", "static.o"]); let sut = check(&database(vec![shared, static_]), &CountExpectation::default()); assert_eq!(find(&sut, "no-true-duplicates").status, Status::Pass); } #[test] fn dependency_flag_only_difference_is_a_duplicate_after_dropping_flags() { // Two entries that differ only by a dependency flag are distinct as-is but // become true duplicates once `--drop-dependency-flags` normalizes them. let with_dep = entry("/src/a.c", Some("a.o"), &["cc", "-MD", "-c", "/src/a.c", "-o", "a.o"]); let without_dep = entry("/src/a.c", Some("a.o"), &["cc", "-c", "/src/a.c", "-o", "a.o"]); let cases: &[(bool, Status)] = &[(false, Status::Pass), (true, Status::Fail)]; for (drop_flags, expected) in cases { let mut db = database(vec![with_dep.clone(), without_dep.clone()]); let norm = Normalization { drop_dependency_flags: *drop_flags, ..Default::default() }; db.normalize(&norm); let sut = check(&db, &CountExpectation::default()); assert_eq!( find(&sut, "no-true-duplicates").status, *expected, "drop_dependency_flags={drop_flags}" ); } } #[test] fn entry_count_is_skipped_without_constraints() { let sut = check(&database(vec![entry("/src/a.c", Some("a.o"), &["cc"])]), &CountExpectation::default()); let check = find(&sut, "entry-count"); assert_eq!(check.status, Status::Skipped); assert!(check.detail.is_none()); } #[test] fn entry_count_expected_objects_within_and_outside_tolerance() { // expected=10, tolerance=20% -> band ceil(10*20/100)=2, so [8, 12]. let cases: &[(usize, Status)] = &[(8, Status::Pass), (12, Status::Pass), (7, Status::Fail), (13, Status::Fail)]; for (count, expected) in cases { let entries: Vec<Entry> = (0..*count).map(|i| entry(&format!("/src/{i}.c"), None, &["cc"])).collect(); let expectation = CountExpectation { expected_objects: Some(10), tolerance_pct: 20, ..Default::default() }; let sut = check(&database(entries), &expectation); assert_eq!(find(&sut, "entry-count").status, *expected, "count={count}"); } } #[test] fn entry_count_ceil_band_boundary_is_exercised() { // expected=3, tolerance=10% -> 3*10/100 = 0.3, ceil = 1, so band is [2, 4]. // This pins the div_ceil rounding: a floor would give band 0 and reject 2/4. let expectation = CountExpectation { expected_objects: Some(3), tolerance_pct: 10, ..Default::default() }; let cases: &[(usize, Status)] = &[(2, Status::Pass), (4, Status::Pass), (1, Status::Fail), (5, Status::Fail)]; for (count, expected) in cases { let entries: Vec<Entry> = (0..*count).map(|i| entry(&format!("/src/{i}.c"), None, &["cc"])).collect(); let sut = check(&database(entries), &expectation); assert_eq!(find(&sut, "entry-count").status, *expected, "count={count}"); } } #[test] fn entry_count_min_entries_floor() { let cases: &[(usize, Status)] = &[(5, Status::Pass), (6, Status::Pass), (4, Status::Fail)]; for (count, expected) in cases { let entries: Vec<Entry> = (0..*count).map(|i| entry(&format!("/src/{i}.c"), None, &["cc"])).collect(); let expectation = CountExpectation { min_entries: Some(5), ..Default::default() }; let sut = check(&database(entries), &expectation); assert_eq!(find(&sut, "entry-count").status, *expected, "count={count}"); } } #[test] fn entry_count_both_constraints_must_hold() { // expected=10 tolerance=0 -> band exactly [10,10]; min=8. With 10 entries // both hold (pass); with 9 the expected band fails even though min holds. let cases: &[(usize, Status)] = &[(10, Status::Pass), (9, Status::Fail)]; for (count, expected) in cases { let entries: Vec<Entry> = (0..*count).map(|i| entry(&format!("/src/{i}.c"), None, &["cc"])).collect(); let expectation = CountExpectation { expected_objects: Some(10), tolerance_pct: 0, min_entries: Some(8) }; let sut = check(&database(entries), &expectation); assert_eq!(find(&sut, "entry-count").status, *expected, "count={count}"); } } #[test] fn report_pass_is_false_when_any_check_fails() { let sut = check(&database(vec![entry("/src/a.c", Some("a.o"), &[])]), &CountExpectation::default()); assert!(!sut.pass); } } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/tools/src/lib.rs��������������������������������������������������������0000664�0000000�0000000�00000001314�15271777203�0021017�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: GPL-3.0-or-later //! Shared test tooling for the Bear project. //! //! This crate exposes the compilation-database comparison and normalization //! logic as a library, and produces the `cdb-compare` binary on top of it. The //! integration suite (`tests/integration`) depends on this library so there is //! a single comparison implementation, not two. pub mod cdb; pub mod compare; pub mod invariants; pub mod matcher; pub mod sample; pub use cdb::{CdbError, CompilationDatabase, Entry, Normalization}; pub use compare::{DiffReport, DifferingEntry, compare}; pub use invariants::{Check, CountExpectation, InvariantsReport, Offender, Status}; pub use matcher::CompilationEntryMatcher; ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/tools/src/matcher.rs����������������������������������������������������0000664�0000000�0000000�00000011525�15271777203�0021701�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: GPL-3.0-or-later //! Partial matching of a single compilation database entry. //! //! [`CompilationEntryMatcher`] checks that an entry (a `serde_json::Value`) //! satisfies a caller-specified subset of fields. It is the entry-level //! predicate the integration suite uses to assert a database contains a given //! compilation; it accepts both the `arguments` array and the `command` string //! encodings, and canonicalizes `directory` paths so symlinked temp roots //! (e.g. macOS `/var` -> `/private/var`) still compare equal. use std::path::PathBuf; use serde_json::Value; /// A builder-style predicate over a compilation database entry. Only the fields /// that are set are checked; unset fields match anything. #[derive(Debug, Default)] pub struct CompilationEntryMatcher { pub file: Option<String>, pub directory: Option<String>, pub arguments: Option<Vec<String>>, pub output: Option<String>, } impl CompilationEntryMatcher { pub fn new() -> Self { Self::default() } pub fn file<S: Into<String>>(mut self, file: S) -> Self { self.file = Some(file.into()); self } pub fn directory<S: Into<String>>(mut self, directory: S) -> Self { self.directory = Some(directory.into()); self } pub fn arguments(mut self, arguments: Vec<String>) -> Self { self.arguments = Some(arguments); self } pub fn output<S: Into<String>>(mut self, output: S) -> Self { self.output = Some(output.into()); self } /// Return whether `entry` satisfies every field set on this matcher. pub fn matches(&self, entry: &Value) -> bool { if let Some(ref expected_file) = self.file { match entry.get("file").and_then(|v| v.as_str()) { Some(actual_file) if actual_file == expected_file => {} _ => return false, } } if let Some(ref expected_dir) = self.directory { match entry.get("directory").and_then(|v| v.as_str()) { Some(actual_dir) => { // Canonicalize both paths so symlinked roots compare equal. let expected_canonical = std::fs::canonicalize(expected_dir).unwrap_or_else(|_| PathBuf::from(expected_dir)); let actual_canonical = std::fs::canonicalize(actual_dir).unwrap_or_else(|_| PathBuf::from(actual_dir)); if expected_canonical != actual_canonical { return false; } } None => return false, } } if let Some(ref expected_args) = self.arguments { // Accept both the 'arguments' array and the 'command' string forms. let actual_args = if let Some(args_array) = entry.get("arguments").and_then(|v| v.as_array()) { args_array.iter().filter_map(|v| v.as_str()).map(|s| s.to_string()).collect::<Vec<_>>() } else if let Some(command_str) = entry.get("command").and_then(|v| v.as_str()) { shell_words::split(command_str).unwrap_or_default() } else { return false; }; if &actual_args != expected_args { return false; } } if let Some(ref expected_output) = self.output { match entry.get("output").and_then(|v| v.as_str()) { Some(actual_output) if actual_output == expected_output => {} _ => return false, } } true } } #[cfg(test)] mod tests { use super::*; use serde_json::json; fn entry() -> Value { json!({ "file": "/path/to/test.c", "directory": "/path/to", "arguments": ["gcc", "-c", "test.c"], "output": "test.o" }) } #[test] fn matches_when_all_set_fields_agree() { let sut = CompilationEntryMatcher::new() .file("/path/to/test.c") .arguments(vec!["gcc".to_string(), "-c".to_string(), "test.c".to_string()]) .output("test.o"); assert!(sut.matches(&entry())); } #[test] fn empty_matcher_matches_anything() { let sut = CompilationEntryMatcher::new(); assert!(sut.matches(&entry())); } #[test] fn rejects_on_disagreeing_file() { let sut = CompilationEntryMatcher::new().file("/path/to/other.c"); assert!(!sut.matches(&entry())); } #[test] fn matches_command_string_form() { let value = json!({ "file": "test.c", "command": "gcc -c test.c" }); let sut = CompilationEntryMatcher::new().arguments(vec![ "gcc".to_string(), "-c".to_string(), "test.c".to_string(), ]); assert!(sut.matches(&value)); } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������rizsotto-Bear-0c30b27/tests/tools/src/sample.rs�����������������������������������������������������0000664�0000000�0000000�00000015411�15271777203�0021535�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// SPDX-License-Identifier: GPL-3.0-or-later //! Deterministic selection of replayable compilation-database entries. //! //! Stage 4 replay needs to pick up to `K` entries and emit a shell-safe record //! a jq-less POSIX `sh` loop can `eval set --` and exec. Selection is //! deterministic (no RNG): the same database always yields the same sample, in a //! stable order, so a replay step is reproducible. use std::path::Path; use crate::cdb::{CompilationDatabase, Entry}; /// Select up to `count` entries for replay, in a deterministic stable order. /// /// With `build_dir`, entries are partitioned into "clean" ones (no `-I` include /// path under `build_dir`, since a build-dir include may reference a generated /// header that makes replay less reliable) and the rest; each group keeps its /// original database order, clean entries come first, and the first /// `min(count, total)` are taken. Without `build_dir`, the first /// `min(count, total)` entries are taken in original order. pub fn select<'a>( database: &'a CompilationDatabase, count: usize, build_dir: Option<&Path>, ) -> Vec<&'a Entry> { let ordered: Vec<&Entry> = match build_dir { None => database.entries.iter().collect(), Some(dir) => { let (clean, rest): (Vec<&Entry>, Vec<&Entry>) = database.entries.iter().partition(|entry| !has_include_under(&entry.arguments, dir)); clean.into_iter().chain(rest).collect() } }; ordered.into_iter().take(count).collect() } /// Render one selected entry as a single shell-safe line: the entry's /// `directory` followed by each argument token, every token individually quoted /// with `shell_words::quote` and space-joined. A consumer recovers the exact /// argv with `eval "set -- $line"`. pub fn to_line(entry: &Entry) -> String { let mut tokens: Vec<&str> = Vec::with_capacity(entry.arguments.len() + 1); tokens.push(entry.directory.as_str()); tokens.extend(entry.arguments.iter().map(String::as_str)); shell_words::join(tokens) } /// Whether any `-I` include path in `arguments` points under `dir`. Recognizes /// both the attached `-I<path>` form and the separate `-I <path>` form. fn has_include_under(arguments: &[String], dir: &Path) -> bool { let mut iter = arguments.iter(); while let Some(arg) = iter.next() { let include = if arg == "-I" { iter.next().map(String::as_str) } else { arg.strip_prefix("-I").filter(|rest| !rest.is_empty()) }; if let Some(path) = include && is_under(Path::new(path), dir) { return true; } } false } /// Whether `path` equals `dir` or is a descendant. Component-wise `starts_with` /// (std `Path`) so `/buildother` does not count as under `/build`. fn is_under(path: &Path, dir: &Path) -> bool { path.starts_with(dir) } #[cfg(test)] mod tests { use super::*; use std::path::PathBuf; /// An entry with the given directory and arguments. `file`/`output` are not /// exercised by selection, so they carry placeholder values. fn entry(directory: &str, arguments: &[&str]) -> Entry { Entry { directory: directory.to_string(), file: "/src/a.c".to_string(), arguments: arguments.iter().map(|s| s.to_string()).collect(), output: None, } } fn database(entries: Vec<Entry>) -> CompilationDatabase { CompilationDatabase { entries } } #[test] fn line_round_trips_tricky_tokens_through_shell_words() { // An argument with a space and one with a quote must come back intact when // a consumer parses the emitted line, which is what `eval set --` relies on. let sut = entry("/build dir", &["cc", "-DMSG=a b", "-DQ=\"x\"", "-c", "/src/a.c"]); let line = to_line(&sut); let parsed = shell_words::split(&line).unwrap(); let mut expected = vec![sut.directory.clone()]; expected.extend(sut.arguments.clone()); assert_eq!(parsed, expected); } #[test] fn count_larger_than_database_returns_all() { let db = database(vec![entry("/b", &["cc"]), entry("/b", &["cc"])]); let sut = select(&db, 10, None); assert_eq!(sut.len(), 2); } #[test] fn count_zero_returns_nothing() { let db = database(vec![entry("/b", &["cc"]), entry("/b", &["cc"])]); let sut = select(&db, 0, None); assert!(sut.is_empty()); } #[test] fn build_dir_ranks_build_includes_after_clean_entries() { // A clean entry (only /src and system includes) must rank before entries // carrying a build-dir include in either the attached or the split form. let clean = entry("/b", &["cc", "-I/src/include", "-I", "/usr/include", "-c", "/src/a.c"]); let attached = entry("/b", &["cc", "-I/build/x", "-c", "/src/b.c"]); let split = entry("/b", &["cc", "-I", "/build/x", "-c", "/src/c.c"]); let db = database(vec![attached.clone(), clean.clone(), split.clone()]); let sut = select(&db, 3, Some(&PathBuf::from("/build"))); // Clean entry first; the two build-dir entries keep their original order. assert_eq!(sut[0].file, clean.file); assert_eq!(sut[0].arguments, clean.arguments); assert_eq!(sut[1].arguments, attached.arguments); assert_eq!(sut[2].arguments, split.arguments); } #[test] fn build_dir_does_not_match_sibling_prefix() { // `/buildother` must not count as an include under `/build`. let sut = entry("/b", &["cc", "-I/buildother/x", "-c", "/src/a.c"]); assert!(!has_include_under(&sut.arguments, Path::new("/build"))); } #[test] fn build_dir_fills_with_unclean_entries_to_reach_count() { // Fewer clean entries than K: build-dir entries still appear so the sample // reaches min(K, total). let clean = entry("/b", &["cc", "-I/src", "-c", "/src/a.c"]); let dirty1 = entry("/b", &["cc", "-I/build/g", "-c", "/src/b.c"]); let dirty2 = entry("/b", &["cc", "-I/build/h", "-c", "/src/c.c"]); let db = database(vec![dirty1, clean, dirty2]); let sut = select(&db, 3, Some(&PathBuf::from("/build"))); assert_eq!(sut.len(), 3); } #[test] fn selection_is_deterministic_across_runs() { let db = database(vec![ entry("/b", &["cc", "-I/build/x", "-c", "/src/a.c"]), entry("/b", &["cc", "-I/src", "-c", "/src/b.c"]), entry("/b", &["cc", "-c", "/src/c.c"]), ]); let build_dir = PathBuf::from("/build"); let first: Vec<String> = select(&db, 2, Some(&build_dir)).iter().map(|e| to_line(e)).collect(); let second: Vec<String> = select(&db, 2, Some(&build_dir)).iter().map(|e| to_line(e)).collect(); assert_eq!(first, second); } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������