RFA-100 · Case file with fixtures · Case 72 of 694 · Compiler evidence
Why include_str! Cannot Find a File That Exists
include_str! embeds a compile-time input using a path relative to the invoking source file. Keep assets tracked, package-aware, portable, and tested from the published crate—not only the workspace checkout.
- Reviewed
- Rust
- Rust 1.98.1
- Targets
- all targets; path syntax portability matters
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- The macro embeds bytes at compile time and interprets a relative path from the file containing the invocation, not from the shell or workspace root.
- First discriminating check
- Resolve the path from the invoking `.rs` file and ensure the asset is packaged and tracked as a real compile-time input.
A file may exist in the repository and still be missing from this macro's point of view:
const SCHEMA: &str = include_str!("missing-schema.json");
Rust 1.98.1 reports that it could not read the path and shows the path of the Rust source containing the invocation. The failing fixture preserves that diagnostic.
include_str! runs during compilation and embeds the file as a &'static str. Its relative path is resolved from the current source file, not from the shell's current directory, workspace root, or final executable.
Source-relative paths make modules movable as units
The include_str! documentation states the path is interpreted relative to the file containing the macro. If src/parser/mod.rs contains include_str!("schema.json"), the compiler looks beside that module.
This differs from many shell and build-tool paths, which start from a current working directory. Running Cargo from another directory therefore should not change a correct include path.
The repaired fixture includes a real neighboring schema.json and verifies its content. The evidence pack intentionally ships that third file; otherwise the compile-pass claim would be incomplete.
A repository file may be absent from a published crate
Cargo packages contain a selected set of files. Manifest include and exclude rules, ignored files, generated assets, or packaging defaults can leave an asset out of the .crate archive even though local checkout builds succeed.
The Cargo manifest documentation defines these packaging controls. I use cargo package --list to inspect the actual package and test the packaged source, not only the workspace tree.
This catches a common release failure: examples or schemas generated locally but never tracked or packaged.
concat with CARGO_MANIFEST_DIR changes the contract
Some projects write:
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/schema.json"))
This intentionally anchors the path at the package manifest directory. It can be appropriate for a package-level asset shared by several modules. It also couples the source to Cargo and to that package layout. Direct rustc compilation will not define the variable unless the caller does.
I choose source-relative paths for assets owned by one module and manifest-relative paths for clearly package-level assets. The important part is that the ownership boundary is deliberate.
Generated files belong in OUT_DIR
If a build script creates the file, writing into src or relying on a previous local generation step makes builds stateful. Cargo provides OUT_DIR for generated artifacts. Source can include a generated file through a path built from that variable.
The build script must declare its real inputs so Cargo reruns it when necessary. A generated file also needs deterministic content if reproducible builds matter.
Static tracked assets do not need a build script. Adding one only to copy a file creates another moving part.
Paths must work on supported hosts
The macro uses host path rules during compilation. A path written with platform-specific separators or names can build on one host and fail on another. Case sensitivity also differs among file systems.
I use forward-slash style in portable source paths, avoid case-only filename distinctions, and run package builds on the host platforms the project supports. Cross-compilation still reads the file on the build host, not the target device.
Embedding changes artifact behavior
Because content is compiled into the binary, changing the source file after compilation does not change the running program. This is correct for built-in templates, small schemas, test data, or default certificates when their release lifecycle matches the binary.
It is wrong for configuration expected to update independently. Runtime file loading or a resource package is then the honest design.
Large embedded files also increase artifact size and can duplicate data across crates or targets. I measure instead of treating inclusion as free.
Text validity is checked
include_str! requires UTF-8. Binary assets belong with include_bytes!. Renaming a binary file to .txt does not change its encoding. For schemas and templates, I validate both UTF-8 and domain syntax in a test or build step.
My debugging sequence
When the compiler says it cannot read an included file, I check:
- The exact
.rsfile containing the macro after macro expansion. - The normalized path from that file.
- Filename case and host portability.
- Whether the file is tracked and appears in
cargo package --list. - Whether it should be generated into OUT_DIR instead.
- Whether compile-time embedding is the right lifecycle.
I do not fix the issue by depending on where the developer happens to run Cargo. A trustworthy include is anchored to a documented source or package boundary and is present in the artifact inputs used by clean builds.