Repository navigation
Expand file tree
/
Copy pathbuild.rs
More file actions
276 lines (253 loc) · 10.6 KB
/
Copy pathbuild.rs
File metadata and controls
276 lines (253 loc) · 10.6 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
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
//! Stages the mzLib bridge executable so the crate works without a manual setup step.
//!
//! # What this does, and what it deliberately does not
//!
//! The bridge is a self-contained .NET executable of roughly 130 MB. crates.io will not carry that
//! (the limit is ~10 MB), and `include_bytes!` is obviously out, so a Rust binding has to fetch it.
//! That is the one part of the design pyMzLib's wheel did not have to solve — a wheel just carries
//! the payload.
//!
//! Resolution order, cheapest first:
//!
//! 1. **`MZLIB_BRIDGE`** — if the caller has a bridge, nothing needs staging. Checked at *runtime*
//! too, so it always wins.
//! 2. **`_dotnet/<rid>/mzlib-bridge`** beside the crate — a source checkout where someone has run
//! pyMzLib's `publish-bridge.ps1`. This is the current lab workflow.
//! 3. **Download** from `MZLIB_BRIDGE_URL`, verified against `MZLIB_BRIDGE_SHA256` when given.
//! A URL for a **bare executable**, not for the published `mzlib-bridge-<rid>.tar.gz` — nothing
//! here unpacks an archive. Fetching the published payload is `mzlib::install::install_bridge`,
//! which is a user action rather than a build side-effect (`BRIDGE-FETCH-IS-A-USER-ACTION`).
//!
//! **This build script never downloads by default, and never fails the build.** A missing bridge is a *runtime* problem with a good
//! error message, not a compile-time one — `cargo check`, `cargo doc`, `cargo clippy` and the whole
//! offline test suite must work on a machine that has never seen a .NET binary, and they do. Failing
//! here would make the crate undocumentable and untestable for contributors, to buy nothing: the
//! program still cannot run without the payload either way.
//!
//! Staging goes to `OUT_DIR`, not to the source tree, because a crate unpacked from the registry is
//! read-only.
use std::path::{Path, PathBuf};
fn main() {
println!("cargo:rerun-if-env-changed=MZLIB_BRIDGE");
println!("cargo:rerun-if-env-changed=MZLIB_BRIDGE_URL");
println!("cargo:rerun-if-env-changed=MZLIB_BRIDGE_SHA256");
println!("cargo:rerun-if-changed=build.rs");
// 1. The caller already has one. Nothing to stage, and the runtime check will find it.
if std::env::var_os("MZLIB_BRIDGE").is_some() {
return;
}
let Some(rid) = runtime_identifier() else {
warn("mzLibRust does not know a .NET runtime identifier for this target; set MZLIB_BRIDGE to a bridge you built yourself.");
return;
};
let exe = executable_name();
// 2. A source checkout with a staged payload.
let staged = PathBuf::from(env("CARGO_MANIFEST_DIR"))
.join("_dotnet")
.join(&rid)
.join(exe);
if staged.is_file() {
emit_staged_path(&staged);
return;
}
// 3. Download, if we were told where from.
let Some(url) = std::env::var("MZLIB_BRIDGE_URL")
.ok()
.filter(|u| !u.is_empty())
else {
warn(&format!(
"no mzLib bridge staged for {rid}. The crate will compile, and the offline tests will \
pass, but any call that reaches mzLib will fail until you have one. Easiest: call \
mzlib::install::install_bridge(), which fetches the published bridge for {rid}, \
verifies it and caches it — it asks first. Otherwise set MZLIB_BRIDGE to a bridge you \
already have, or stage one at _dotnet/{rid}/{exe}. See README.md."
));
return;
};
// What is staged here is a bare executable, and this is the one place that has to say so out
// loud. pyMzLib publishes the bridge as `mzlib-bridge-<rid>.tar.gz`, whose payload is a TREE —
// the executable alongside Resources/, the Bruker and timsTOF natives, libmmd — so pointing
// MZLIB_BRIDGE_URL at the published asset would write a gzip stream to a file called
// `mzlib-bridge`, mark it executable, and hand the runtime something that cannot possibly run.
// No build failure, because this script never fails the build; just a confusing exec error
// later, blamed on the crate. Refuse it here and name the thing that does handle a tarball.
if url.ends_with(".tar.gz") || url.ends_with(".tgz") || url.ends_with(".zip") {
warn(&format!(
"MZLIB_BRIDGE_URL points at an archive ({url}). This staging path takes a URL for a \
bare bridge executable and does not unpack anything. For the published \
mzlib-bridge-<rid>.tar.gz, call mzlib::install::install_bridge() instead — it \
verifies the checksum, unpacks the payload tree and caches it."
));
return;
}
let destination = PathBuf::from(env("OUT_DIR")).join(exe);
match download(&url, &destination) {
Ok(()) => {
if let Some(expected) = std::env::var("MZLIB_BRIDGE_SHA256").ok().filter(|s| !s.is_empty())
{
match verify(&destination, &expected) {
Ok(()) => {}
Err(problem) => {
// A payload that fails its checksum must not be left where the runtime
// would pick it up and run it.
let _ = std::fs::remove_file(&destination);
warn(&format!("discarded the downloaded bridge: {problem}"));
return;
}
}
} else {
warn("MZLIB_BRIDGE_URL was used without MZLIB_BRIDGE_SHA256, so the download was not verified. Pin a checksum for anything reproducible.");
}
make_executable(&destination);
emit_staged_path(&destination);
}
Err(problem) => warn(&format!(
"could not download the mzLib bridge from {url}: {problem}. The crate still builds; set \
MZLIB_BRIDGE at runtime to point at one."
)),
}
}
/// Tell the crate where the staged bridge landed, as a compile-time fallback path.
fn emit_staged_path(path: &Path) {
println!("cargo:rustc-env=MZLIB_BRIDGE_STAGED={}", path.display());
}
fn env(key: &str) -> String {
std::env::var(key).unwrap_or_else(|_| panic!("cargo should set {key}"))
}
fn warn(message: &str) {
println!("cargo:warning={message}");
}
/// The .NET runtime identifier for the target being built.
///
/// These strings have to match what `publish-bridge.ps1` stages under, or nothing is ever found.
fn runtime_identifier() -> Option<String> {
let os = match env("CARGO_CFG_TARGET_OS").as_str() {
"windows" => "win",
"linux" => "linux",
"macos" => "osx",
_ => return None,
};
let arch = match env("CARGO_CFG_TARGET_ARCH").as_str() {
"x86_64" => "x64",
"aarch64" => "arm64",
_ => return None,
};
Some(format!("{os}-{arch}"))
}
fn executable_name() -> &'static str {
if env("CARGO_CFG_TARGET_OS") == "windows" {
"mzlib-bridge.exe"
} else {
"mzlib-bridge"
}
}
/// Fetch a file, shelling out rather than taking an HTTP stack as a build dependency.
///
/// `curl` ships with Windows 10+, macOS and essentially every Linux; PowerShell is the fallback on
/// Windows. Pulling `reqwest` and its TLS stack into the *build* graph to copy one file would cost
/// every downstream build far more than this is worth.
fn download(url: &str, destination: &Path) -> Result<(), String> {
if let Some(parent) = destination.parent() {
std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
let curl = std::process::Command::new("curl")
.args([
"--fail",
"--location",
"--silent",
"--show-error",
"--output",
])
.arg(destination)
.arg(url)
.status();
match curl {
Ok(status) if status.success() => return Ok(()),
Ok(status) => {
if !cfg!(windows) {
return Err(format!("curl exited with {status}"));
}
}
Err(error) => {
if !cfg!(windows) {
return Err(format!("could not run curl: {error}"));
}
}
}
let script = format!(
"$ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri '{url}' -OutFile '{}'",
destination.display()
);
let status = std::process::Command::new("powershell")
.args(["-NoProfile", "-NonInteractive", "-Command", &script])
.status()
.map_err(|error| format!("could not run powershell: {error}"))?;
if status.success() {
Ok(())
} else {
Err(format!("powershell download exited with {status}"))
}
}
/// Check a downloaded payload against an expected SHA-256, shelling out for the digest.
fn verify(path: &Path, expected: &str) -> Result<(), String> {
let expected = expected.trim().to_ascii_lowercase();
let actual = sha256(path)?;
if actual == expected {
Ok(())
} else {
Err(format!(
"checksum mismatch: expected {expected}, got {actual}"
))
}
}
fn sha256(path: &Path) -> Result<String, String> {
// One of these exists on every platform we target.
let candidates: [(&str, Vec<String>); 3] = [
("sha256sum", vec![path.display().to_string()]),
(
"shasum",
vec!["-a".into(), "256".into(), path.display().to_string()],
),
(
"certutil",
vec![
"-hashfile".into(),
path.display().to_string(),
"SHA256".into(),
],
),
];
for (program, args) in candidates {
let Ok(output) = std::process::Command::new(program).args(&args).output() else {
continue;
};
if !output.status.success() {
continue;
}
let text = String::from_utf8_lossy(&output.stdout).to_ascii_lowercase();
// sha256sum/shasum print "<digest> <path>"; certutil puts the digest on its own line.
if let Some(digest) = text
.split_whitespace()
.find(|token| token.len() == 64 && token.chars().all(|c| c.is_ascii_hexdigit()))
{
return Ok(digest.to_owned());
}
}
Err("no usable SHA-256 tool found (tried sha256sum, shasum, certutil)".to_owned())
}
/// Downloaded files arrive without the execute bit on Unix.
fn make_executable(path: &Path) {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(metadata) = std::fs::metadata(path) {
let mut permissions = metadata.permissions();
permissions.set_mode(permissions.mode() | 0o755);
let _ = std::fs::set_permissions(path, permissions);
}
}
#[cfg(not(unix))]
{
let _ = path;
}
}