-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbuild.rs
More file actions
190 lines (172 loc) · 8.73 KB
/
Copy pathbuild.rs
File metadata and controls
190 lines (172 loc) · 8.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
//! Embeds the compiled support `cdylib` (this crate's own `cdylib` output) into
//! the `pnl`/`pnlx` binaries so `pnl install` can expand it into `@pnlx/runtime`
//! for the PHP runtime to load via FFI.
//!
//! The cdylib and the binaries are produced by the same `cargo build`, so on a
//! single build the cdylib does not yet exist when this script runs — it embeds
//! an empty blob. Release builds (`make build`) run `cargo build` twice: the
//! second pass embeds the cdylib produced by the first. A development build that
//! runs once simply ships an empty blob; `pnl install` then falls back to copying
//! the cdylib sitting next to the executable.
//!
//! It also bakes `config.toml` into compile-time constants (see
//! [`generate_config_constants`]) so the built-in endpoints and defaults travel
//! with the binary without any runtime TOML parsing.
use std::collections::hash_map::DefaultHasher;
use std::env;
use std::fs;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use handlebars::Handlebars;
use serde_json::json;
fn main() {
println!("cargo:rerun-if-changed=build.rs");
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR is set for build scripts"));
generate_config_constants(&out_dir);
// The PHP SDK under `src/sdk` is embedded with `include_dir!` (see
// `sdk_assets.rs`). On stable Rust that macro does NOT register the embedded
// files as rebuild triggers, so editing an SDK file alone would otherwise leave
// a stale runtime baked into the binary (a fixed `Util`/`NativeLibrary` method
// would silently not ship). Two-part guard: (1) re-export every SDK file as a
// build-script trigger so this script reruns on any change, and (2) write a
// content fingerprint that `sdk_assets.rs` pulls in via `include_str!` (which
// cargo DOES track), so a changed fingerprint forces that module to recompile
// and `include_dir!` to re-embed the current tree.
let manifest_dir =
PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set"));
let mut fingerprint = String::new();
fingerprint_tree(&manifest_dir.join("src/sdk"), &mut fingerprint);
fs::write(out_dir.join("sdk_fingerprint.txt"), fingerprint)
.expect("failed to write SDK fingerprint");
// The same `include_dir!` staleness applies to the JSON schemas embedded by
// `schema.rs`; fingerprint them too so a schema edit re-embeds (otherwise the
// runtime validates against a stale schema — e.g. rejecting a newly-added key).
let mut schema_fingerprint = String::new();
fingerprint_tree(&manifest_dir.join("schemas"), &mut schema_fingerprint);
fs::write(out_dir.join("schema_fingerprint.txt"), schema_fingerprint)
.expect("failed to write schema fingerprint");
let dest = out_dir.join("support.lib");
let lib_name = match env::var("CARGO_CFG_TARGET_OS").unwrap_or_default().as_str() {
"windows" => "pnl.dll",
"macos" => "libpnl.dylib",
_ => "libpnl.so",
};
// OUT_DIR is `<target>/<profile>/build/<pkg>-<hash>/out`; the cdylib from a
// prior build sits at `<target>/<profile>/<lib_name>`.
let mut bytes = Vec::new();
if let Some(profile_dir) = out_dir.ancestors().nth(3) {
let candidate = profile_dir.join(lib_name);
if candidate.is_file() {
println!("cargo:rerun-if-changed={}", candidate.display());
bytes = fs::read(&candidate).unwrap_or_default();
}
}
fs::write(&dest, &bytes).expect("failed to write embedded support library");
}
/// Parse `config.toml` and render `config_constants.rs` (included by
/// `src/cli/config.rs`) from a Handlebars template, so the built-in defaults are
/// compiled in rather than parsed at runtime — and the Rust we emit is shaped by
/// a template, not assembled with string concatenation.
fn generate_config_constants(out_dir: &Path) {
let manifest_dir =
PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set"));
let config_path = manifest_dir.join("config.toml");
let template_path = manifest_dir.join("src/cli/templates/build/config_constants.rs.tpl");
println!("cargo:rerun-if-changed={}", config_path.display());
println!("cargo:rerun-if-changed={}", template_path.display());
let raw = fs::read_to_string(&config_path).expect("failed to read config.toml");
let config: toml::Table = raw.parse().expect("config.toml is not valid TOML");
let string = |path: &[&str]| -> String {
lookup(&config, path)
.and_then(toml::Value::as_str)
.unwrap_or_else(|| panic!("config.toml is missing string {}", path.join(".")))
.to_owned()
};
let integer = |path: &[&str]| -> i64 {
lookup(&config, path)
.and_then(toml::Value::as_integer)
.unwrap_or_else(|| panic!("config.toml is missing integer {}", path.join(".")))
};
let string_array = |path: &[&str], item_desc: &str| -> Vec<String> {
lookup(&config, path)
.and_then(toml::Value::as_array)
.unwrap_or_else(|| panic!("config.toml is missing array {}", path.join(".")))
.iter()
.map(|value| {
value
.as_str()
.unwrap_or_else(|| panic!("{item_desc} must hold strings"))
.to_owned()
})
.collect()
};
let binaries = string_array(&["binaries", "names"], "binaries.names");
let authorized_repositories = string_array(
&["repositories", "authorized_repositories"],
"repositories.authorized_repositories",
);
let context = json!({
"schema_version": string(&["schema_version"]),
"self_repository": string(&["repositories", "self"]),
"packages_repository": string(&["repositories", "packages"]),
"output_dir": string(&["workspace", "output_dir"]),
"ttl_seconds": integer(&["update_check", "ttl_seconds"]),
"opt_out_env": string(&["update_check", "opt_out_env"]),
"cache_key": string(&["update_check", "cache_key"]),
"binaries": binaries,
"binaries_len": binaries.len(),
"authorized_repositories": authorized_repositories,
"authorized_repositories_len": authorized_repositories.len(),
"pnl_manifest": string(&["filenames", "pnl_manifest"]),
"pnlx_manifest": string(&["filenames", "pnlx_manifest"]),
"lockfile": string(&["filenames", "lockfile"]),
"pathmap": string(&["filenames", "pathmap"]),
"autoload": string(&["filenames", "autoload"]),
"generated_dir": string(&["filenames", "generated_dir"]),
"aliases_file": string(&["filenames", "aliases_file"]),
"ffi_suffix": string(&["filenames", "ffi_suffix"]),
// The build target, surfaced to the PHP layer as PNLX_BUILD_OS/ARCH.
"build_os": env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(),
"build_arch": env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default(),
});
let template = fs::read_to_string(&template_path).expect("failed to read config template");
let mut handlebars = Handlebars::new();
handlebars.register_escape_fn(handlebars::no_escape);
let generated = handlebars
.render_template(&template, &context)
.expect("failed to render config constants template");
fs::write(out_dir.join("config_constants.rs"), generated)
.expect("failed to write generated config constants");
}
/// Walk a directory tree in sorted order, emitting a `rerun-if-changed` for every file
/// and appending each file's path and length to `fingerprint`. The fingerprint is
/// written to OUT_DIR and `include_str!`d by `sdk_assets.rs`, so any SDK edit
/// changes a cargo-tracked input and forces the embedded copy to be rebuilt.
fn fingerprint_tree(dir: &Path, fingerprint: &mut String) {
println!("cargo:rerun-if-changed={}", dir.display());
let Ok(entries) = fs::read_dir(dir) else {
return;
};
let mut paths: Vec<PathBuf> = entries.flatten().map(|entry| entry.path()).collect();
paths.sort();
for path in paths {
if path.is_dir() {
fingerprint_tree(&path, fingerprint);
} else {
println!("cargo:rerun-if-changed={}", path.display());
let bytes = fs::read(&path).unwrap_or_default();
let mut hasher = DefaultHasher::new();
bytes.hash(&mut hasher);
fingerprint.push_str(&format!("{}:{:x}\n", path.display(), hasher.finish()));
}
}
}
/// Walk a dotted path of nested TOML tables, returning the value at the leaf.
fn lookup<'a>(table: &'a toml::Table, path: &[&str]) -> Option<&'a toml::Value> {
let (first, rest) = path.split_first()?;
let mut value = table.get(*first)?;
for key in rest {
value = value.get(*key)?;
}
Some(value)
}