Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
2644e8e
A platform's config store returns a long document whole
jwrosewell Oct 15, 2026
c4ac157
The platform says whether it serves from its staging environment
jwrosewell Oct 15, 2026
3ec6623
The adapter names the request headers its platform sets
jwrosewell Oct 15, 2026
8d3042c
The platform's own device module is selected as `platform`
jwrosewell Oct 15, 2026
69ba032
Proxy response conversions are named for what they produce
jwrosewell Oct 15, 2026
cee089c
Merge 48-aws-signing into 49-host-features
jwrosewell Oct 15, 2026
b319bf0
The Fastly names design takes its round 1 review
jwrosewell Oct 15, 2026
67d34c2
The Fastly names design takes its round 2 corrections
jwrosewell Oct 15, 2026
853cd68
Merge 48-aws-signing into 49-host-features
jwrosewell Oct 15, 2026
b52f5ea
Merge 48-aws-signing into 49-host-features
jwrosewell Oct 15, 2026
bc7d16c
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 16, 2026
0fa462b
Read a written document back whole in the Fastly adapter
jwrosewell Oct 16, 2026
7b8d312
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 16, 2026
9d98bc6
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 16, 2026
c38f98b
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 16, 2026
d6ee980
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 16, 2026
afe1456
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 16, 2026
c87c734
The rules guide maps the Fastly device module to `platform`
jwrosewell Oct 16, 2026
9360c0b
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 16, 2026
3bebe1b
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 16, 2026
9b5faa5
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 16, 2026
4553ead
Merge the review fixes into split/49-host-features
jwrosewell Oct 16, 2026
13d57b0
The design header names the pull request that carries it
jwrosewell Oct 16, 2026
2e52a41
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 16, 2026
84757bb
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 16, 2026
43a9335
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 16, 2026
353f2d0
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 16, 2026
4303fef
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 16, 2026
ae30545
Core's comments describe platform behavior without naming a platform
jwrosewell Oct 16, 2026
4fcd822
Core's tests name no hosting platform
jwrosewell Oct 16, 2026
f93a912
Core's README files point at TESTING.md for the adapter aliases
jwrosewell Oct 16, 2026
cae1cd2
The KV metadata limit is tested in the Fastly adapter
jwrosewell Oct 16, 2026
c52db65
The Fastly SDK guard moves from core to the Fastly adapter
jwrosewell Oct 16, 2026
6af6e6f
Core's attestation prefixes and HTML tests name no hosting platform
jwrosewell Oct 16, 2026
6aa06bb
A guard outside core fails when core names a hosting platform
jwrosewell Oct 16, 2026
5c2bb83
The Fastly names design says core names no hosting platform
jwrosewell Oct 16, 2026
4cce662
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 16, 2026
dde89a5
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 16, 2026
147dbb7
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 18, 2026
64f9fde
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 18, 2026
4360c41
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 18, 2026
991b457
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 18, 2026
a69d14b
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 18, 2026
47bb09b
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 18, 2026
c9602bf
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 19, 2026
9ad85ca
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 19, 2026
1252745
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 19, 2026
6fee073
The header rewrite's handler tests mark HTTPS without a platform header
jwrosewell Oct 19, 2026
58c6fef
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 19, 2026
d1aa512
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 19, 2026
8975dc8
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 28, 2026
be12b3d
The Fastly chunk check writes its SHA-256 in hex one byte at a time
jwrosewell Oct 28, 2026
ede533b
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 28, 2026
116d5df
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 28, 2026
b8dcbda
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 28, 2026
23c44d0
Row 49 takes the final Fastly names in core design
jwrosewell Oct 28, 2026
f499a6e
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 28, 2026
aee8778
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 28, 2026
c3a294c
Row 49's design copy ties its count to a published commit
jwrosewell Oct 28, 2026
d8b1d99
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 28, 2026
360d02b
Row 49's design copy carries the date it is proposed, 2026-10-27
jwrosewell Oct 28, 2026
02be79c
The generator's loader test reads a long entry whole, as Fastly retur…
jwrosewell Oct 28, 2026
0fc903b
Merge split/48-aws-signing into split/49-host-features
jwrosewell Oct 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/format.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,19 @@ jobs:
exit 1
fi

check-core-platform-names:
name: core platform names guard
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

# Core is built on EdgeZero and names no hosting platform or platform
# domain, in code, comments, tests, documents or file names, so one
# platform's change is never a change in core. Each adapter names its
# own platform.
- name: Check that crates/trusted-server-core names no hosting platform
run: python3 scripts/lint-core-platform-names.py

format-rust:
name: cargo fmt
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ Every PR must pass:
9. Native core build-digest test and lint (`cargo test -p trusted-server-core --test template_build_digest` and `cargo clippy -p trusted-server-core --test template_build_digest -- -D warnings`)
10. Wasm clock lint: `scripts/lint-wasm-clock.sh` (crates built into the Cloudflare Worker must use `web_time`, not the `std::time::{Instant,SystemTime}` `now`/`elapsed` methods, which panic there)
11. Documentation proposal scripts: `shellcheck scripts/docs-proposal/*.sh && node --test scripts/docs-proposal/hunks.test.mjs && scripts/docs-proposal/test.sh`
12. Core platform names: `python3 scripts/lint-core-platform-names.py` (no file in `crates/trusted-server-core` names a hosting platform or one of its domains, in code, comments, tests, documents or file names)

---

Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **Breaking:** the platform's own device module is selected with `[device] module = "platform"`, as `[geo] module = "platform"` selects the platform's geo lookup, where it was `module = "fastly"`. A document that still writes `fastly` is refused at startup as naming a device module no running module supplies. **For crates outside core**, `build_device_module` takes a `build_platform` factory.
- **Breaking for crates outside core:** an adapter declares the request headers its platform sets. `platform::declare_platform_tls_headers` names a header the platform sets on a request that arrived over TLS, which core reads for the scheme and never forwards to an origin. `platform::declare_platform_spoofable_headers` names the platform's other headers a client could also send, which `sanitize_forwarded_headers` strips at entry with the standard forwarding headers, in an adapter that calls it. `platform::declare_platform_internal_headers` names headers that are never passed to a third party. The Fastly adapter declares `fastly-ssl`, `fastly-client-ip` and `x-debug-fastly-pop` before it serves. `SPOOFABLE_FORWARDED_HEADERS` holds the three standard forwarding headers, `HEADER_X_DEBUG_FASTLY_POP` leaves core, and `constants::is_internal_header` answers for the fixed list and the declared headers together. A `[trusted_client_ip]` `ip_header` may start with `x-` or end with `-client-ip`, so `fastly-client-ip` stays valid. On the Axum, Cloudflare and Spin adapters, which declare none of these headers, a client's `fastly-ssl` is no longer removed from the request to the publisher's origin, and on Spin a client's `fastly-ssl` and `fastly-client-ip` are no longer stripped at entry. On those adapters the `x-debug-fastly-pop` header from a client is also no longer kept from third parties, because `INTERNAL_HEADERS` no longer holds it, and is no longer refused as a `[trusted_client_ip]` header name. On the Axum and Cloudflare adapters a client's `fastly-ssl` also no longer makes the request HTTPS. On Fastly, `ts config validate`, which runs with no adapter's declarations, no longer refuses `x-debug-fastly-pop` as a `[trusted_client_ip]` header name, and the service refuses it when it loads the document.
- **Breaking for crates outside core:** an adapter tells core whether its platform runs the service in a staging environment with `platform::declare_platform_staging`, and a module asks `platform::platform_is_staging`. `ENV_FASTLY_IS_STAGING` leaves core, the Fastly adapter reads `FASTLY_IS_STAGING` itself, and the DataDome module's test bypass follows the declaration. On the Axum, Cloudflare and Spin adapters, which declare no staging environment, the bypass no longer follows a `FASTLY_IS_STAGING` variable.
- **Breaking for crates outside core:** a settings document too long for one config store entry is put back together by the platform's config store, which keeps it in parts. The Fastly adapter's `PlatformConfigStore` follows the chunk pointer, reads each chunk and checks every length and hash, where core's settings loader recognized the pointer and read the chunks itself. A deployment's documents read as before. A `PlatformConfigStore` an adapter implements returns each entry whole.
- Core names no hosting platform or platform domain in any line, its comments, tests and README files included. Comments and tests describe what a platform does without naming one, the test that core never uses the Fastly SDK and the test of the Fastly KV Store's metadata limit move to the Fastly adapter, and `scripts/lint-core-platform-names.py`, which the Run Format workflow runs, fails when a file under `crates/trusted-server-core` names a hosting platform, a tool named for one or a domain a platform serves deployments from.
- **Breaking for a build that links its own crates:** a source's `auth` table names an origin signer a crate offers. Core keeps the interface, `OriginSigner` in `trusted_server_core::origin_auth`, and signs the publisher's origin requests and each asset route's through it where it signed before. `type` names the signer, `origin_query` is read by core, and the rest of the table is the signer's. The `s3_sigv4` signer moves to `crates/shared/aws`, keeping its settings, defaults and secret-store key references, and every adapter and the `ts` tool link the crate's builder, so a stock build still offers it. A build that composes its own modules adds `trusted_server_shared_aws::builder()`, or a `type` of `s3_sigv4` is refused at startup naming the signers on offer. A table the signer cannot use is refused at startup and by `ts config validate` naming the setting. The deprecated spelling `s3_sig_v4` is no longer read. **For crates outside core**, `AssetOriginAuth` and `S3SigV4AuthConfig` are replaced by `OriginAuth`, `SourceLocation::auth` gives an `OriginSigning`, `OriginSettings::location` and `OriginSource::new` take the signers on offer and the setting's name, and a crate offers a signer with `IntegrationBuilder::with_origin_signer`.
- **Breaking for crates outside core:** an adapter answers how it names a backend and bounds its transport timers through `PlatformBackend` alone, which gains `auction_backend_budget` and `transport_timeout_bucket_count` with defaults, and describes itself to auction plan validation through an `AuctionTargetDescriptor` it builds from its name, its backend implementation and its `AuctionTargetCapabilities`. `AuctionPlan::validate_for_target` takes that descriptor, and `BackendNamingPolicy`, `AuctionTargetId`, `PredictedBackend`, `BackendNamingError` and `PlatformBackend::naming_policy` are gone from core, because core names no hosting platform. Each adapter carries its own naming policy beside the code that registers its backends, and the names a deployment sees are unchanged.
- Core names no platform's cache header. It removes from a response it makes private only `Surrogate-Control`, `CDN-Cache-Control` and a platform header it wrote its own policy in, so on every adapter `fastly-surrogate-control` and `cloudflare-cdn-cache-control` stay where an origin or an operator wrote them, until a fetch middleware removes them, which none in the stack does yet. The Fastly and Cloudflare adapters declare their own platform's header, which keeps an origin response with a policy in it out of the stored copies on that adapter alone, and each adapter writes its policy in the header it wrote before. **For crates outside core**, `EdgeCacheHeader::CloudflareCdnCacheControl` is `EdgeCacheHeader::Platform(name)`, `EDGE_CACHE_HEADER_NAMES` lists only the two standard headers, the two platform header constants leave core, an adapter declares its platform's headers with `platform::declare_platform_cache_headers`, and a policy written with `CachePolicy::apply_to_response` is removed by `cache_policy::remove_owned_cache_headers` when the response is made private.
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions crates/audience/permutive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use http::{Method, StatusCode};
use serde::Deserialize;
use validator::Validate;

use trusted_server_core::constants::INTERNAL_HEADERS;
use trusted_server_core::constants::is_internal_header;
use trusted_server_core::error::TrustedServerError;
use trusted_server_core::integrations::{
AttributeRewriteAction, INTEGRATION_MAX_BODY_BYTES, IntegrationEndpoint, IntegrationProxy,
Expand Down Expand Up @@ -312,7 +312,7 @@ impl PermutiveIntegration {
// Copy any X-* custom headers, skipping TS-internal headers
for (name, value) in from {
let name_str = name.as_str();
if name_str.starts_with("x-") && !INTERNAL_HEADERS.contains(&name_str) {
if name_str.starts_with("x-") && !is_internal_header(name_str) {
to.append(name.clone(), value.clone());
}
}
Expand Down
1 change: 0 additions & 1 deletion crates/bot-protection/datadome/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ web-time = { workspace = true }

[dev-dependencies]
futures = { workspace = true }
temp-env = { workspace = true }
toml = { workspace = true }
trusted-server-core = { workspace = true, features = ["test-utils"] }

Expand Down
6 changes: 3 additions & 3 deletions crates/bot-protection/datadome/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ use serde_json::Value as JsonValue;
use url::Url;
use validator::Validate;

use trusted_server_core::constants::ENV_FASTLY_IS_STAGING;
use trusted_server_core::error::TrustedServerError;
use trusted_server_core::integrations::{
AttributeRewriteAction, INTEGRATION_MAX_BODY_BYTES, IntegrationDocumentState,
Expand Down Expand Up @@ -568,7 +567,7 @@ impl DataDomeIntegration {
}

fn active_protection_test_bypass(&self) -> Option<&ProtectionTestBypassConfig> {
if std::env::var(ENV_FASTLY_IS_STAGING).as_deref() != Ok("1") {
if !trusted_server_core::platform::platform_is_staging() {
return None;
}

Expand Down Expand Up @@ -1100,7 +1099,8 @@ fn build(
let protection_test_bypass_active = integration.active_protection_test_bypass().is_some();
if protection_test_bypass_configured && !protection_test_bypass_active {
log::warn!(
"[datadome] DataDome test bypass is configured but inactive because FASTLY_IS_STAGING is not 1"
"[datadome] DataDome test bypass is configured but inactive because the platform \
does not run this service in its staging environment"
);
}
log::info!(
Expand Down
77 changes: 38 additions & 39 deletions crates/bot-protection/datadome/src/protection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -836,7 +836,24 @@ mod tests {

use super::*;

static FASTLY_IS_STAGING_ENV_LOCK: Mutex<()> = Mutex::new(());
static PLATFORM_STAGING_LOCK: Mutex<()> = Mutex::new(());

/// Runs `run` with the platform declared as staging or not, putting the
/// declaration back afterwards, even when `run` panics.
fn with_platform_staging<T>(staging: bool, run: impl FnOnce() -> T) -> T {
struct Restore(bool);
impl Drop for Restore {
fn drop(&mut self) {
trusted_server_core::platform::declare_platform_staging(self.0);
}
}
let _guard = PLATFORM_STAGING_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let _restore = Restore(trusted_server_core::platform::platform_is_staging());
trusted_server_core::platform::declare_platform_staging(staging);
run()
}

fn protection_config() -> DataDomeConfig {
DataDomeConfig {
Expand Down Expand Up @@ -864,25 +881,16 @@ mod tests {
services: &RuntimeServices,
request: &mut Request<EdgeBody>,
) -> RequestFilterDecision {
let _guard = FASTLY_IS_STAGING_ENV_LOCK
.lock()
.expect("should lock staging environment test guard");
temp_env::with_var(
trusted_server_core::constants::ENV_FASTLY_IS_STAGING,
Some("1"),
|| {
futures::executor::block_on(integration.filter_protection_request(
RequestFilterInput {
settings,
services,
request,
geo_info: None,
permissions: None,
is_integration_route: false,
},
))
},
)
with_platform_staging(true, || {
futures::executor::block_on(integration.filter_protection_request(RequestFilterInput {
settings,
services,
request,
geo_info: None,
permissions: None,
is_integration_route: false,
}))
})
}

fn filter_marks_request(
Expand Down Expand Up @@ -1137,25 +1145,16 @@ mod tests {
edgezero_core::http::HeaderValue::from_static("temporary-test-credential-32-bytes!"),
);

let _guard = FASTLY_IS_STAGING_ENV_LOCK
.lock()
.expect("should lock staging environment test guard");
let decision = temp_env::with_var(
trusted_server_core::constants::ENV_FASTLY_IS_STAGING,
None::<&str>,
|| {
futures::executor::block_on(integration.filter_protection_request(
RequestFilterInput {
settings: &settings,
services: &services,
request: &mut request,
geo_info: None,
permissions: None,
is_integration_route: false,
},
))
},
);
let decision = with_platform_staging(false, || {
futures::executor::block_on(integration.filter_protection_request(RequestFilterInput {
settings: &settings,
services: &services,
request: &mut request,
geo_info: None,
permissions: None,
is_integration_route: false,
}))
});

assert!(
matches!(decision, RequestFilterDecision::Continue(_)),
Expand Down
4 changes: 2 additions & 2 deletions crates/device/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ HTTP/2 signals. Future vendor modules (for example
`crates/device/<vendor>`) slot in alongside it.

The built-in default module (User-Agent only) ships in `trusted-server-core`
(`ec::device`). Adapters select and inject the vendor module via
`build_device_module`.
(`ec::device`). An adapter injects its platform's own module through
`build_device_module`, which builds it when `[device] module = "platform"`.
4 changes: 2 additions & 2 deletions crates/device/fastly/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//!
//! [`FastlyDeviceModule`] strengthens the built-in User-Agent classification
//! with the host's TLS (JA4) and HTTP/2 signals, for deployments on Fastly
//! Compute. It is selected by `[device] module = "fastly"` and wired in by the
//! Compute. It is selected by `[device] module = "platform"` and wired in by the
//! Fastly adapter, which injects the captured host signals and hands the module
//! the request's module context.
//!
Expand Down Expand Up @@ -66,7 +66,7 @@ impl HostSignals for FastlyHostSignals {
}
}

/// The Fastly device module, opt-in via `[device] module = "fastly"`.
/// The Fastly device module, opt-in via `[device] module = "platform"`.
///
/// Classifies a request with [`DeviceSignals::derive`], which strengthens the
/// User-Agent classification with the host signals. It reads the User-Agent
Expand Down
4 changes: 2 additions & 2 deletions crates/identity/lockr/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ use http::{Method, StatusCode};
use serde::Deserialize;
use validator::Validate;

use trusted_server_core::constants::INTERNAL_HEADERS;
use trusted_server_core::constants::is_internal_header;
use trusted_server_core::error::TrustedServerError;
use trusted_server_core::integrations::{
AttributeRewriteAction, INTEGRATION_MAX_BODY_BYTES, IntegrationEndpoint, IntegrationProxy,
Expand Down Expand Up @@ -322,7 +322,7 @@ impl LockrIntegration {

for (name, value) in from {
let name_str = name.as_str();
if name_str.starts_with("x-") && !INTERNAL_HEADERS.contains(&name_str) {
if name_str.starts_with("x-") && !is_internal_header(name_str) {
to.append(name.clone(), value.clone());
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/trusted-server-adapter-fastly/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ trusted-server-geo-fastly = { workspace = true }
[dev-dependencies]
bytes = { workspace = true }
edgezero-core = { workspace = true, features = ["test-utils"] }
regex = { workspace = true }
toml = { workspace = true }
trusted-server-core = { workspace = true, features = ["test-utils"] }
# Test fixture only. It proves the integration seam on this adapter from
Expand Down
76 changes: 76 additions & 0 deletions crates/trusted-server-adapter-fastly/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//! Build script for `trusted-server-adapter-fastly`: lists core's Rust sources
//! for the test that checks core never uses the Fastly SDK.

#![allow(
clippy::print_stdout,
reason = "Cargo build scripts communicate rebuild inputs on stdout"
)]

use std::env;
use std::fs;
use std::path::{Path, PathBuf};

fn main() {
let manifest_dir =
PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("should read CARGO_MANIFEST_DIR"));
let src_dir = manifest_dir
.parent()
.expect("should have the crates folder above this crate")
.join("trusted-server-core")
.join("src");

// Cargo walks a directory named here, so adding, renaming or deleting a
// core source file reruns this script and the list follows the tree.
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed={}", path_literal(&src_dir));

// A checkout without core's sources builds with an empty list, which the
// test that pins the list to core's tree refuses.
let mut sources = Vec::new();
if src_dir.is_dir() {
collect_rust_sources(&src_dir, &src_dir, &mut sources);
}
sources.sort();

let mut generated = String::from(
"// Generated by build.rs. Every Rust source file under core's `src`.\n\
const CHECKED_SOURCES: &[(&str, &str)] = &[\n",
);
for relative in &sources {
let absolute = format!("{}/{relative}", path_literal(&src_dir));
generated.push_str(&format!(
" (\"{relative}\", include_str!(\"{absolute}\")),\n"
));
}
generated.push_str("];\n");

let out_dir = PathBuf::from(env::var("OUT_DIR").expect("should read OUT_DIR"));
fs::write(out_dir.join("core_sources.rs"), generated)
.expect("should write the core source list");
}

/// Collects every `.rs` file under `dir`, as a path relative to `root` with
/// forward slashes, so the generated file reads the same on every platform.
fn collect_rust_sources(root: &Path, dir: &Path, sources: &mut Vec<String>) {
let entries = fs::read_dir(dir).expect("should read a source directory");
for entry in entries {
let entry = entry.expect("should read a source directory entry");
let path = entry.path();
if path.is_dir() {
collect_rust_sources(root, &path, sources);
continue;
}
if path.extension().is_some_and(|extension| extension == "rs") {
let relative = path
.strip_prefix(root)
.expect("should strip the source root prefix");
sources.push(path_literal(relative));
}
}
}

/// Renders a path for a Rust string literal, with forward slashes so a Windows
/// path needs no escaping.
fn path_literal(path: &Path) -> String {
path.display().to_string().replace('\\', "/")
}
Loading
Loading