Repository navigation
Expand file tree
/
Copy pathbuild.rs
More file actions
223 lines (198 loc) Β· 7.82 KB
/
Copy pathbuild.rs
File metadata and controls
223 lines (198 loc) Β· 7.82 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
//! Embeds the web UI's built files in release builds of microscope, names the
//! sources the loader was built from, and compiles the syscall tracer's BPF
//! programs with the flake's clang.
//!
//! Development builds read `build/web` at run time instead, so rebuilding
//! the page never rebuilds the crate. Node is never needed to build microscope:
//! a missing `build/web` embeds nothing, and the page says to run
//! `just web`.
use std::ffi::OsString;
use std::fmt::Write as _;
use std::hash::Hasher as _;
use std::path::{Path, PathBuf};
fn main() {
println!("cargo::rerun-if-changed=build.rs");
let manifest = PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").expect("cargo sets it"));
sources_digest(&manifest);
let out = PathBuf::from(std::env::var_os("OUT_DIR").expect("cargo sets it"));
bpf_programs(&manifest, &out);
agent_protocol(&manifest, &out);
let root = manifest.join("build/web");
let mut files = Vec::new();
if std::env::var("PROFILE").as_deref() == Ok("release") {
println!("cargo::rerun-if-changed={}", root.display());
collect(&root, &root, &mut files);
files.sort();
}
let mut table = String::from("&[\n");
for (name, path) in files {
writeln!(
table,
" ({name:?}, include_bytes!({path:?}).as_slice()),"
)
.expect("writing to a string");
}
table.push(']');
std::fs::write(out.join("web_assets.rs"), table).expect("write the asset table");
}
/// Digests the library's source files and the locked dependencies, so that
/// images a cache holds are never read by a loader built from other sources:
/// any change may change what a load produces. The binary's own modules are
/// left out, so editing them rebuilds neither the library nor its tests.
fn sources_digest(manifest: &Path) {
let mut files = Vec::new();
let binary_only = ["bin", "cli", "dap", "main.rs", "present", "web"];
let mut roots = vec!["Cargo.lock".to_owned()];
for entry in std::fs::read_dir(manifest.join("src"))
.expect("read src")
.flatten()
{
let name = entry.file_name().to_string_lossy().into_owned();
if !binary_only.contains(&name.as_str()) {
roots.push(format!("src/{name}"));
}
}
for root in &roots {
println!("cargo::rerun-if-changed={root}");
let path = manifest.join(root);
if path.is_dir() {
collect(manifest, &path, &mut files);
} else {
files.push((root.clone(), path.to_string_lossy().into_owned()));
}
}
files.sort();
// SipHash with fixed keys: stable for one toolchain, and a toolchain
// that changes it only empties caches.
let mut digest = std::hash::DefaultHasher::new();
for (name, path) in files {
let contents = std::fs::read(&path).expect("read a source file");
digest.write(name.as_bytes());
digest.write_usize(contents.len());
digest.write(&contents);
}
println!(
"cargo::rustc-env=MICROSCOPE_SOURCES_DIGEST={:016x}",
digest.finish()
);
}
/// Compiles `src/backend/linux/bpf/*.bpf.c` into `OUT_DIR`, with the
/// unwrapped clang and only the headers the flake names, so that the objects
/// depend on nothing of the host's. Outside the dev shell the build stops
/// and says which variable is missing; it never looks for another compiler.
///
/// Every edit to the library reruns this script, since the sources digest
/// covers it, so the objects are rebuilt only when what clang reads or how
/// it is run changed: the compiler, its flags, and the C files beside the
/// programs. The headers outside come from store paths the flags name,
/// which change with their contents.
fn bpf_programs(manifest: &Path, out: &Path) {
let directory = manifest.join("src/backend/linux/bpf");
println!("cargo::rerun-if-changed={}", directory.display());
let variable = |name: &str| {
println!("cargo::rerun-if-env-changed={name}");
std::env::var_os(name).unwrap_or_else(|| {
panic!(
"{name} is not set: build microscope inside its dev shell (`just dev`), \
which names the clang and headers the BPF programs are built with"
)
})
};
let clang = variable("MICROSCOPE_BPF_CLANG");
let include = variable("MICROSCOPE_BPF_INCLUDE");
let mut flags: Vec<OsString> = [
"-target",
"bpf",
"-O2",
"-g",
"-Wall",
"-Werror",
"-nostdinc",
]
.map(OsString::from)
.into();
flags.push(format!("-fdebug-prefix-map={}=.", manifest.display()).into());
for path in std::env::split_paths(&include) {
flags.push("-I".into());
flags.push(path.into());
}
let mut files: Vec<PathBuf> = std::fs::read_dir(&directory)
.expect("read the BPF sources")
.flatten()
.map(|entry| entry.path())
.filter(|path| path.extension().is_none_or(|extension| extension != "rs"))
.collect();
files.sort();
let mut digest = std::hash::DefaultHasher::new();
for part in std::iter::once(&clang).chain(&flags) {
digest.write_usize(part.len());
digest.write(part.as_encoded_bytes());
}
for path in &files {
let contents = std::fs::read(path).expect("read a BPF source");
let name = path.file_name().expect("a file");
digest.write_usize(name.len());
digest.write(name.as_encoded_bytes());
digest.write_usize(contents.len());
digest.write(&contents);
}
let inputs = format!("{:016x}", digest.finish());
let sources: Vec<(&PathBuf, PathBuf)> = files
.iter()
.filter(|path| path.to_string_lossy().ends_with(".bpf.c"))
.map(|source| {
let name = source.file_name().expect("a file").to_string_lossy();
(source, out.join(name.replace(".bpf.c", ".bpf.o")))
})
.collect();
let stamp = out.join("bpf-inputs");
let built = std::fs::read_to_string(&stamp).is_ok_and(|built| built == inputs);
if built && sources.iter().all(|(_, object)| object.exists()) {
return;
}
// An interrupted build leaves no stamp, so the next one starts over.
if stamp.exists() {
std::fs::remove_file(&stamp).expect("remove the BPF stamp");
}
for (source, object) in sources {
let status = std::process::Command::new(&clang)
.args(&flags)
.arg("-c")
.arg(source)
.arg("-o")
.arg(&object)
.status()
.unwrap_or_else(|error| panic!("run {}: {error}", Path::new(&clang).display()));
assert!(status.success(), "compiling {} failed", source.display());
}
std::fs::write(&stamp, inputs).expect("write the BPF stamp");
}
fn collect(root: &Path, directory: &Path, files: &mut Vec<(String, String)>) {
let Ok(entries) = std::fs::read_dir(directory) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect(root, &path, files);
} else if let Ok(relative) = path.strip_prefix(root) {
files.push((
relative.to_string_lossy().into_owned(),
path.to_string_lossy().into_owned(),
));
}
}
}
/// Generates the agent protocol's Rust types from its schema, with protox,
/// so that no protoc is needed.
fn agent_protocol(manifest: &Path, out: &Path) {
let schema = "microscope/agent/v1/agent.proto";
let include = manifest.join("proto");
println!("cargo::rerun-if-changed={}", include.join(schema).display());
let descriptors = protox::compile([schema], [&include]).expect("the agent schema compiles");
prost_build::Config::new()
.bytes(["."])
.out_dir(out)
.compile_fds(descriptors)
.expect("generating the agent protocol");
}