tangled
alpha
login
or
join now
erika.florist
/
maudit
6
fork
atom
Rust library to generate static websites
6
fork
atom
overview
issues
pulls
1
pipelines
feat: smoother hot reload
Princesseuh
1 month ago
3979a95d
04a42162
+532
-25
6 changed files
expand all
collapse all
unified
split
Cargo.lock
Cargo.toml
crates
maudit-cli
Cargo.toml
src
dev
build.rs
dep_tracker.rs
dev.rs
+61
-10
Cargo.lock
reviewed
···
1662
1662
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
1663
1663
1664
1664
[[package]]
1665
1665
+
name = "fixtures-prefetch-prerender"
1666
1666
+
version = "0.1.0"
1667
1667
+
dependencies = [
1668
1668
+
"maud",
1669
1669
+
"maudit",
1670
1670
+
]
1671
1671
+
1672
1672
+
[[package]]
1665
1673
name = "flate2"
1666
1674
version = "1.1.8"
1667
1675
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2608
2616
"tar",
2609
2617
"tokio",
2610
2618
"tokio-util",
2619
2619
+
"toml",
2611
2620
"toml_edit 0.24.0+spec-1.1.0",
2612
2621
"tower-http",
2613
2622
"tracing",
···
3847
3856
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
3848
3857
3849
3858
[[package]]
3850
3850
-
name = "prefetch-prerender"
3851
3851
-
version = "0.1.0"
3852
3852
-
dependencies = [
3853
3853
-
"maud",
3854
3854
-
"maudit",
3855
3855
-
]
3856
3856
-
3857
3857
-
[[package]]
3858
3859
name = "proc-macro-crate"
3859
3860
version = "3.4.0"
3860
3861
source = "registry+https://github.com/rust-lang/crates.io-index"
···
4514
4515
]
4515
4516
4516
4517
[[package]]
4518
4518
+
name = "serde_spanned"
4519
4519
+
version = "0.6.9"
4520
4520
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4521
4521
+
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
4522
4522
+
dependencies = [
4523
4523
+
"serde",
4524
4524
+
]
4525
4525
+
4526
4526
+
[[package]]
4517
4527
name = "serde_urlencoded"
4518
4528
version = "0.7.1"
4519
4529
source = "registry+https://github.com/rust-lang/crates.io-index"
···
5011
5021
]
5012
5022
5013
5023
[[package]]
5024
5024
+
name = "toml"
5025
5025
+
version = "0.8.23"
5026
5026
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5027
5027
+
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
5028
5028
+
dependencies = [
5029
5029
+
"serde",
5030
5030
+
"serde_spanned",
5031
5031
+
"toml_datetime 0.6.11",
5032
5032
+
"toml_edit 0.22.27",
5033
5033
+
]
5034
5034
+
5035
5035
+
[[package]]
5036
5036
+
name = "toml_datetime"
5037
5037
+
version = "0.6.11"
5038
5038
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5039
5039
+
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
5040
5040
+
dependencies = [
5041
5041
+
"serde",
5042
5042
+
]
5043
5043
+
5044
5044
+
[[package]]
5014
5045
name = "toml_datetime"
5015
5046
version = "0.7.5+spec-1.1.0"
5016
5047
source = "registry+https://github.com/rust-lang/crates.io-index"
···
5021
5052
5022
5053
[[package]]
5023
5054
name = "toml_edit"
5055
5055
+
version = "0.22.27"
5056
5056
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5057
5057
+
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
5058
5058
+
dependencies = [
5059
5059
+
"indexmap",
5060
5060
+
"serde",
5061
5061
+
"serde_spanned",
5062
5062
+
"toml_datetime 0.6.11",
5063
5063
+
"toml_write",
5064
5064
+
"winnow",
5065
5065
+
]
5066
5066
+
5067
5067
+
[[package]]
5068
5068
+
name = "toml_edit"
5024
5069
version = "0.23.10+spec-1.0.0"
5025
5070
source = "registry+https://github.com/rust-lang/crates.io-index"
5026
5071
checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269"
5027
5072
dependencies = [
5028
5073
"indexmap",
5029
5029
-
"toml_datetime",
5074
5074
+
"toml_datetime 0.7.5+spec-1.1.0",
5030
5075
"toml_parser",
5031
5076
"winnow",
5032
5077
]
···
5038
5083
checksum = "8c740b185920170a6d9191122cafef7010bd6270a3824594bff6784c04d7f09e"
5039
5084
dependencies = [
5040
5085
"indexmap",
5041
5041
-
"toml_datetime",
5086
5086
+
"toml_datetime 0.7.5+spec-1.1.0",
5042
5087
"toml_parser",
5043
5088
"toml_writer",
5044
5089
"winnow",
···
5052
5097
dependencies = [
5053
5098
"winnow",
5054
5099
]
5100
5100
+
5101
5101
+
[[package]]
5102
5102
+
name = "toml_write"
5103
5103
+
version = "0.1.2"
5104
5104
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5105
5105
+
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
5055
5106
5056
5107
[[package]]
5057
5108
name = "toml_writer"
+1
-1
Cargo.toml
reviewed
···
1
1
[workspace]
2
2
-
members = ["crates/*", "benchmarks/*", "examples/*", "website", "xtask", "e2e/fixtures/*"]
2
2
+
members = ["crates/*", "benchmarks/*", "examples/*", "website", "xtask", "e2e/fixtures/prefetch-prerender"]
3
3
resolver = "3"
4
4
5
5
[workspace.dependencies]
+1
crates/maudit-cli/Cargo.toml
reviewed
···
28
28
ureq = "3.1.4"
29
29
tar = "0.4.44"
30
30
toml_edit = "0.24.0"
31
31
+
toml = "0.8"
31
32
local-ip-address = "0.6.9"
32
33
flate2 = "1.1.8"
33
34
quanta = "0.12.6"
+36
-12
crates/maudit-cli/src/dev.rs
reviewed
···
1
1
pub(crate) mod server;
2
2
3
3
mod build;
4
4
+
mod dep_tracker;
4
5
mod filterer;
5
6
6
7
use notify::{
···
10
11
use notify_debouncer_full::{DebounceEventResult, DebouncedEvent, new_debouncer};
11
12
use quanta::Instant;
12
13
use server::WebSocketMessage;
13
13
-
use std::{fs, path::Path};
14
14
+
use std::{fs, path::{Path, PathBuf}};
14
15
use tokio::{
15
16
signal,
16
17
sync::{broadcast, mpsc::channel},
···
162
163
}
163
164
}
164
165
} else {
165
165
-
// Normal rebuild - spawn in background so file watcher can continue
166
166
-
info!(name: "watch", "Files changed, rebuilding...");
167
167
-
let build_manager_clone = build_manager_watcher.clone();
168
168
-
tokio::spawn(async move {
169
169
-
match build_manager_clone.start_build().await {
170
170
-
Ok(_) => {
171
171
-
// Build completed (success or failure already logged)
166
166
+
// Normal rebuild - check if we need full recompilation or just rerun
167
167
+
let changed_paths: Vec<PathBuf> = events.iter()
168
168
+
.flat_map(|e| e.paths.iter().cloned())
169
169
+
.collect();
170
170
+
171
171
+
let needs_recompile = build_manager_watcher.needs_recompile(&changed_paths).await;
172
172
+
173
173
+
if needs_recompile {
174
174
+
// Need to recompile - spawn in background so file watcher can continue
175
175
+
info!(name: "watch", "Files changed, rebuilding...");
176
176
+
let build_manager_clone = build_manager_watcher.clone();
177
177
+
tokio::spawn(async move {
178
178
+
match build_manager_clone.start_build().await {
179
179
+
Ok(_) => {
180
180
+
// Build completed (success or failure already logged)
181
181
+
}
182
182
+
Err(e) => {
183
183
+
error!(name: "build", "Failed to start build: {}", e);
184
184
+
}
172
185
}
173
173
-
Err(e) => {
174
174
-
error!(name: "build", "Failed to start build: {}", e);
186
186
+
});
187
187
+
} else {
188
188
+
// Just rerun the binary without recompiling
189
189
+
info!(name: "watch", "Non-dependency files changed, rerunning binary...");
190
190
+
let build_manager_clone = build_manager_watcher.clone();
191
191
+
tokio::spawn(async move {
192
192
+
match build_manager_clone.rerun_binary().await {
193
193
+
Ok(_) => {
194
194
+
// Rerun completed (success or failure already logged)
195
195
+
}
196
196
+
Err(e) => {
197
197
+
error!(name: "build", "Failed to rerun binary: {}", e);
198
198
+
}
175
199
}
176
176
-
}
177
177
-
});
200
200
+
});
201
201
+
}
178
202
}
179
203
}
180
204
}
+183
-2
crates/maudit-cli/src/dev/build.rs
reviewed
···
1
1
use cargo_metadata::Message;
2
2
use quanta::Instant;
3
3
use server::{StatusType, WebSocketMessage, update_status};
4
4
+
use std::path::PathBuf;
4
5
use std::sync::Arc;
5
6
use tokio::process::Command;
6
7
use tokio::sync::broadcast;
7
8
use tokio_util::sync::CancellationToken;
8
8
-
use tracing::{debug, error, info};
9
9
+
use tracing::{debug, error, info, warn};
9
10
10
11
use crate::{
11
12
dev::server,
12
13
logging::{FormatElapsedTimeOptions, format_elapsed_time},
13
14
};
14
15
16
16
+
use super::dep_tracker::{DependencyTracker, find_target_dir};
17
17
+
15
18
#[derive(Clone)]
16
19
pub struct BuildManager {
17
20
current_cancel: Arc<tokio::sync::RwLock<Option<CancellationToken>>>,
18
21
build_semaphore: Arc<tokio::sync::Semaphore>,
19
22
websocket_tx: broadcast::Sender<WebSocketMessage>,
20
23
current_status: Arc<tokio::sync::RwLock<Option<server::PersistentStatus>>>,
24
24
+
dep_tracker: Arc<tokio::sync::RwLock<Option<DependencyTracker>>>,
25
25
+
binary_path: Arc<tokio::sync::RwLock<Option<PathBuf>>>,
21
26
}
22
27
23
28
impl BuildManager {
···
27
32
build_semaphore: Arc::new(tokio::sync::Semaphore::new(1)), // Only one build at a time
28
33
websocket_tx,
29
34
current_status: Arc::new(tokio::sync::RwLock::new(None)),
35
35
+
dep_tracker: Arc::new(tokio::sync::RwLock::new(None)),
36
36
+
binary_path: Arc::new(tokio::sync::RwLock::new(None)),
30
37
}
31
38
}
32
39
···
35
42
self.current_status.clone()
36
43
}
37
44
45
45
+
/// Check if the given paths require recompilation based on dependency tracking
46
46
+
/// Returns true if recompilation is needed, false if we can just rerun the binary
47
47
+
pub async fn needs_recompile(&self, changed_paths: &[PathBuf]) -> bool {
48
48
+
let dep_tracker = self.dep_tracker.read().await;
49
49
+
50
50
+
if let Some(tracker) = dep_tracker.as_ref() {
51
51
+
if tracker.has_dependencies() {
52
52
+
let needs_recompile = tracker.needs_recompile(changed_paths);
53
53
+
if !needs_recompile {
54
54
+
debug!(name: "build", "Changed files are not dependencies, rerun binary without recompile");
55
55
+
}
56
56
+
return needs_recompile;
57
57
+
}
58
58
+
}
59
59
+
60
60
+
// If we don't have a dependency tracker yet, always recompile
61
61
+
true
62
62
+
}
63
63
+
64
64
+
/// Rerun the binary without recompiling
65
65
+
pub async fn rerun_binary(&self) -> Result<bool, Box<dyn std::error::Error>> {
66
66
+
let binary_path = self.binary_path.read().await;
67
67
+
68
68
+
let Some(path) = binary_path.as_ref() else {
69
69
+
warn!(name: "build", "No binary path available, falling back to full rebuild");
70
70
+
return self.start_build().await;
71
71
+
};
72
72
+
73
73
+
if !path.exists() {
74
74
+
warn!(name: "build", "Binary at {:?} no longer exists, falling back to full rebuild", path);
75
75
+
return self.start_build().await;
76
76
+
}
77
77
+
78
78
+
info!(name: "build", "Rerunning binary without recompilation...");
79
79
+
80
80
+
// Notify that build is starting (even though we're just rerunning)
81
81
+
update_status(
82
82
+
&self.websocket_tx,
83
83
+
self.current_status.clone(),
84
84
+
StatusType::Info,
85
85
+
"Rerunning...",
86
86
+
)
87
87
+
.await;
88
88
+
89
89
+
let build_start_time = Instant::now();
90
90
+
91
91
+
let child = Command::new(path)
92
92
+
.envs([
93
93
+
("MAUDIT_DEV", "true"),
94
94
+
("MAUDIT_QUIET", "true"),
95
95
+
])
96
96
+
.stdout(std::process::Stdio::piped())
97
97
+
.stderr(std::process::Stdio::piped())
98
98
+
.spawn()?;
99
99
+
100
100
+
// Wait for the process to complete
101
101
+
let output = child.wait_with_output().await?;
102
102
+
103
103
+
let duration = build_start_time.elapsed();
104
104
+
let formatted_elapsed_time = format_elapsed_time(
105
105
+
duration,
106
106
+
&FormatElapsedTimeOptions::default_dev(),
107
107
+
);
108
108
+
109
109
+
if output.status.success() {
110
110
+
info!(name: "build", "Binary rerun finished {}", formatted_elapsed_time);
111
111
+
update_status(
112
112
+
&self.websocket_tx,
113
113
+
self.current_status.clone(),
114
114
+
StatusType::Success,
115
115
+
"Binary rerun finished successfully",
116
116
+
)
117
117
+
.await;
118
118
+
Ok(true)
119
119
+
} else {
120
120
+
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
121
121
+
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
122
122
+
error!(name: "build", "Binary rerun failed {}\nstdout: {}\nstderr: {}",
123
123
+
formatted_elapsed_time, stdout, stderr);
124
124
+
update_status(
125
125
+
&self.websocket_tx,
126
126
+
self.current_status.clone(),
127
127
+
StatusType::Error,
128
128
+
&format!("Binary rerun failed:\n{}\n{}", stdout, stderr),
129
129
+
)
130
130
+
.await;
131
131
+
Ok(false)
132
132
+
}
133
133
+
}
134
134
+
38
135
/// Do initial build that can be cancelled (but isn't stored as current build)
39
136
pub async fn do_initial_build(&self) -> Result<bool, Box<dyn std::error::Error>> {
40
137
self.internal_build(true).await
···
91
188
92
189
let websocket_tx = self.websocket_tx.clone();
93
190
let current_status = self.current_status.clone();
191
191
+
let dep_tracker_clone = self.dep_tracker.clone();
192
192
+
let binary_path_clone = self.binary_path.clone();
94
193
let build_start_time = Instant::now();
95
194
96
195
// Create a channel to get the build result back
···
182
281
if output.status.success() {
183
282
let build_type = if is_initial { "Initial build" } else { "Rebuild" };
184
283
info!(name: "build", "{} finished {}", build_type, formatted_elapsed_time);
185
185
-
update_status(&websocket_tx, current_status, StatusType::Success, "Build finished successfully").await;
284
284
+
update_status(&websocket_tx, current_status.clone(), StatusType::Success, "Build finished successfully").await;
285
285
+
286
286
+
// Update dependency tracker after successful build
287
287
+
Self::update_dependency_tracker_after_build(dep_tracker_clone.clone(), binary_path_clone.clone()).await;
288
288
+
186
289
true
187
290
} else {
188
291
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
···
212
315
// Wait for the build result
213
316
let success = result_rx.recv().await.unwrap_or(false);
214
317
Ok(success)
318
318
+
}
319
319
+
320
320
+
/// Update the dependency tracker after a successful build
321
321
+
async fn update_dependency_tracker_after_build(
322
322
+
dep_tracker: Arc<tokio::sync::RwLock<Option<DependencyTracker>>>,
323
323
+
binary_path: Arc<tokio::sync::RwLock<Option<PathBuf>>>,
324
324
+
) {
325
325
+
// Try to get the binary name from Cargo.toml in the current directory
326
326
+
let binary_name = match Self::get_binary_name_from_cargo_toml() {
327
327
+
Ok(name) => name,
328
328
+
Err(e) => {
329
329
+
debug!(name: "build", "Could not determine binary name: {}", e);
330
330
+
return;
331
331
+
}
332
332
+
};
333
333
+
334
334
+
debug!(name: "build", "Detected binary name: {}", binary_name);
335
335
+
336
336
+
// Find the target directory
337
337
+
let target_dir = match find_target_dir() {
338
338
+
Ok(dir) => dir,
339
339
+
Err(e) => {
340
340
+
debug!(name: "build", "Could not find target directory: {}", e);
341
341
+
return;
342
342
+
}
343
343
+
};
344
344
+
345
345
+
// Update binary path
346
346
+
let bin_path = target_dir.join(&binary_name);
347
347
+
if bin_path.exists() {
348
348
+
*binary_path.write().await = Some(bin_path.clone());
349
349
+
debug!(name: "build", "Binary path set to: {:?}", bin_path);
350
350
+
} else {
351
351
+
debug!(name: "build", "Binary not found at expected path: {:?}", bin_path);
352
352
+
}
353
353
+
354
354
+
// Try to load the dependency tracker
355
355
+
match DependencyTracker::load_from_binary_name(&binary_name) {
356
356
+
Ok(tracker) => {
357
357
+
debug!(name: "build", "Loaded {} dependencies for tracking", tracker.get_dependencies().len());
358
358
+
*dep_tracker.write().await = Some(tracker);
359
359
+
}
360
360
+
Err(e) => {
361
361
+
debug!(name: "build", "Could not load dependency tracker: {}", e);
362
362
+
}
363
363
+
}
364
364
+
}
365
365
+
366
366
+
/// Get the binary name from Cargo.toml in the current directory
367
367
+
fn get_binary_name_from_cargo_toml() -> Result<String, Box<dyn std::error::Error>> {
368
368
+
let cargo_toml_path = PathBuf::from("Cargo.toml");
369
369
+
if !cargo_toml_path.exists() {
370
370
+
return Err("Cargo.toml not found in current directory".into());
371
371
+
}
372
372
+
373
373
+
let cargo_toml_content = std::fs::read_to_string(&cargo_toml_path)?;
374
374
+
let cargo_toml: toml::Value = toml::from_str(&cargo_toml_content)?;
375
375
+
376
376
+
// First, try to get the package name
377
377
+
if let Some(package_name) = cargo_toml
378
378
+
.get("package")
379
379
+
.and_then(|p| p.get("name"))
380
380
+
.and_then(|n| n.as_str())
381
381
+
{
382
382
+
// Check if there's a [[bin]] section with a different name
383
383
+
if let Some(bins) = cargo_toml.get("bin").and_then(|b| b.as_array()) {
384
384
+
if let Some(first_bin) = bins.first() {
385
385
+
if let Some(bin_name) = first_bin.get("name").and_then(|n| n.as_str()) {
386
386
+
return Ok(bin_name.to_string());
387
387
+
}
388
388
+
}
389
389
+
}
390
390
+
391
391
+
// No explicit bin name, use package name
392
392
+
return Ok(package_name.to_string());
393
393
+
}
394
394
+
395
395
+
Err("Could not find package name in Cargo.toml".into())
215
396
}
216
397
}
+250
crates/maudit-cli/src/dev/dep_tracker.rs
reviewed
···
1
1
+
use std::collections::HashMap;
2
2
+
use std::fs;
3
3
+
use std::path::{Path, PathBuf};
4
4
+
use std::time::SystemTime;
5
5
+
use tracing::{debug, warn};
6
6
+
7
7
+
/// Tracks dependencies from .d files to determine if recompilation is needed
8
8
+
#[derive(Debug, Clone)]
9
9
+
pub struct DependencyTracker {
10
10
+
/// Path to the .d file
11
11
+
d_file_path: Option<PathBuf>,
12
12
+
/// Map of dependency paths to their last modification times
13
13
+
dependencies: HashMap<PathBuf, SystemTime>,
14
14
+
}
15
15
+
16
16
+
/// Find the target directory using multiple strategies
17
17
+
///
18
18
+
/// This function tries multiple approaches to locate the target directory:
19
19
+
/// 1. CARGO_TARGET_DIR / CARGO_BUILD_TARGET_DIR environment variables
20
20
+
/// 2. Local ./target/debug directory
21
21
+
/// 3. Workspace root target/debug directory (walking up to find [workspace])
22
22
+
/// 4. Fallback to relative "target/debug" path
23
23
+
pub fn find_target_dir() -> Result<PathBuf, std::io::Error> {
24
24
+
// 1. Check CARGO_TARGET_DIR and CARGO_BUILD_TARGET_DIR environment variables
25
25
+
for env_var in ["CARGO_TARGET_DIR", "CARGO_BUILD_TARGET_DIR"] {
26
26
+
if let Ok(target_dir) = std::env::var(env_var) {
27
27
+
// Try with /debug appended
28
28
+
let path = PathBuf::from(&target_dir).join("debug");
29
29
+
if path.exists() {
30
30
+
debug!("Using target directory from {}: {:?}", env_var, path);
31
31
+
return Ok(path);
32
32
+
}
33
33
+
// If the env var points directly to debug or release
34
34
+
let path_no_debug = PathBuf::from(&target_dir);
35
35
+
if path_no_debug.exists()
36
36
+
&& (path_no_debug.ends_with("debug") || path_no_debug.ends_with("release"))
37
37
+
{
38
38
+
debug!(
39
39
+
"Using target directory from {} (direct): {:?}",
40
40
+
env_var, path_no_debug
41
41
+
);
42
42
+
return Ok(path_no_debug);
43
43
+
}
44
44
+
}
45
45
+
}
46
46
+
47
47
+
// 2. Look for target directory in current directory
48
48
+
let local_target = PathBuf::from("target/debug");
49
49
+
if local_target.exists() {
50
50
+
debug!("Using local target directory: {:?}", local_target);
51
51
+
return Ok(local_target);
52
52
+
}
53
53
+
54
54
+
// 3. Try to find workspace root by looking for Cargo.toml with [workspace]
55
55
+
let mut current = std::env::current_dir()?;
56
56
+
loop {
57
57
+
let cargo_toml = current.join("Cargo.toml");
58
58
+
if cargo_toml.exists() {
59
59
+
if let Ok(content) = fs::read_to_string(&cargo_toml) {
60
60
+
if content.contains("[workspace]") {
61
61
+
let workspace_target = current.join("target").join("debug");
62
62
+
if workspace_target.exists() {
63
63
+
debug!("Using workspace target directory: {:?}", workspace_target);
64
64
+
return Ok(workspace_target);
65
65
+
}
66
66
+
}
67
67
+
}
68
68
+
}
69
69
+
70
70
+
// Move up to parent directory
71
71
+
if !current.pop() {
72
72
+
break;
73
73
+
}
74
74
+
}
75
75
+
76
76
+
// 4. Final fallback to relative path
77
77
+
debug!("Falling back to relative target/debug path");
78
78
+
Ok(PathBuf::from("target/debug"))
79
79
+
}
80
80
+
81
81
+
impl DependencyTracker {
82
82
+
#[allow(dead_code)]
83
83
+
pub fn new() -> Self {
84
84
+
Self {
85
85
+
d_file_path: None,
86
86
+
dependencies: HashMap::new(),
87
87
+
}
88
88
+
}
89
89
+
90
90
+
/// Locate and load the .d file for the current binary
91
91
+
/// The .d file is typically at target/debug/<binary-name>.d
92
92
+
pub fn load_from_binary_name(binary_name: &str) -> Result<Self, std::io::Error> {
93
93
+
let target_dir = find_target_dir()?;
94
94
+
let d_file_path = target_dir.join(format!("{}.d", binary_name));
95
95
+
96
96
+
if !d_file_path.exists() {
97
97
+
return Err(std::io::Error::new(
98
98
+
std::io::ErrorKind::NotFound,
99
99
+
format!(".d file not found at {:?}", d_file_path),
100
100
+
));
101
101
+
}
102
102
+
103
103
+
let mut tracker = Self {
104
104
+
d_file_path: Some(d_file_path.clone()),
105
105
+
dependencies: HashMap::new(),
106
106
+
};
107
107
+
108
108
+
tracker.reload_dependencies()?;
109
109
+
Ok(tracker)
110
110
+
}
111
111
+
112
112
+
/// Reload dependencies from the .d file
113
113
+
pub fn reload_dependencies(&mut self) -> Result<(), std::io::Error> {
114
114
+
let Some(d_file_path) = &self.d_file_path else {
115
115
+
return Err(std::io::Error::new(
116
116
+
std::io::ErrorKind::NotFound,
117
117
+
"No .d file path set",
118
118
+
));
119
119
+
};
120
120
+
121
121
+
let content = fs::read_to_string(d_file_path)?;
122
122
+
123
123
+
// Parse the .d file format: "target: dep1 dep2 dep3 ..."
124
124
+
// The first line contains the target and dependencies, separated by ':'
125
125
+
let deps = if let Some(colon_pos) = content.find(':') {
126
126
+
// Everything after the colon is dependencies
127
127
+
&content[colon_pos + 1..]
128
128
+
} else {
129
129
+
// Malformed .d file
130
130
+
warn!("Malformed .d file at {:?}", d_file_path);
131
131
+
return Ok(());
132
132
+
};
133
133
+
134
134
+
// Dependencies are space-separated and may span multiple lines (with line continuations)
135
135
+
let dep_paths: Vec<PathBuf> = deps
136
136
+
.split_whitespace()
137
137
+
.filter(|s| !s.is_empty() && *s != "\\") // Filter out line continuation characters
138
138
+
.map(PathBuf::from)
139
139
+
.collect();
140
140
+
141
141
+
// Clear old dependencies and load new ones with their modification times
142
142
+
self.dependencies.clear();
143
143
+
144
144
+
for dep_path in dep_paths {
145
145
+
match fs::metadata(&dep_path) {
146
146
+
Ok(metadata) => {
147
147
+
if let Ok(modified) = metadata.modified() {
148
148
+
self.dependencies.insert(dep_path.clone(), modified);
149
149
+
debug!("Tracking dependency: {:?}", dep_path);
150
150
+
}
151
151
+
}
152
152
+
Err(e) => {
153
153
+
// Dependency file doesn't exist or can't be read - this is okay,
154
154
+
// it might have been deleted or moved
155
155
+
debug!("Could not read dependency {:?}: {}", dep_path, e);
156
156
+
}
157
157
+
}
158
158
+
}
159
159
+
160
160
+
debug!(
161
161
+
"Loaded {} dependencies from {:?}",
162
162
+
self.dependencies.len(),
163
163
+
d_file_path
164
164
+
);
165
165
+
Ok(())
166
166
+
}
167
167
+
168
168
+
/// Check if any of the given paths require recompilation
169
169
+
/// Returns true if any path is a tracked dependency that has been modified
170
170
+
pub fn needs_recompile(&self, changed_paths: &[PathBuf]) -> bool {
171
171
+
for changed_path in changed_paths {
172
172
+
// Normalize the changed path to handle relative vs absolute paths
173
173
+
let changed_path_canonical = changed_path.canonicalize().ok();
174
174
+
175
175
+
for (dep_path, last_modified) in &self.dependencies {
176
176
+
// Try to match both exact path and canonical path
177
177
+
let matches = changed_path == dep_path
178
178
+
|| changed_path_canonical.as_ref() == Some(dep_path)
179
179
+
|| dep_path.canonicalize().ok().as_ref() == changed_path_canonical.as_ref();
180
180
+
181
181
+
if matches {
182
182
+
// Check if the file was modified after we last tracked it
183
183
+
if let Ok(metadata) = fs::metadata(changed_path) {
184
184
+
if let Ok(current_modified) = metadata.modified() {
185
185
+
if current_modified > *last_modified {
186
186
+
debug!(
187
187
+
"Dependency {:?} was modified, recompile needed",
188
188
+
changed_path
189
189
+
);
190
190
+
return true;
191
191
+
}
192
192
+
}
193
193
+
} else {
194
194
+
// File was deleted or can't be read, assume recompile is needed
195
195
+
debug!(
196
196
+
"Dependency {:?} no longer exists, recompile needed",
197
197
+
changed_path
198
198
+
);
199
199
+
return true;
200
200
+
}
201
201
+
}
202
202
+
}
203
203
+
}
204
204
+
205
205
+
false
206
206
+
}
207
207
+
208
208
+
/// Get the list of tracked dependency paths
209
209
+
pub fn get_dependencies(&self) -> Vec<&Path> {
210
210
+
self.dependencies.keys().map(|p| p.as_path()).collect()
211
211
+
}
212
212
+
213
213
+
/// Check if we have any dependencies loaded
214
214
+
pub fn has_dependencies(&self) -> bool {
215
215
+
!self.dependencies.is_empty()
216
216
+
}
217
217
+
}
218
218
+
219
219
+
#[cfg(test)]
220
220
+
mod tests {
221
221
+
use super::*;
222
222
+
use std::fs;
223
223
+
use std::io::Write;
224
224
+
use tempfile::TempDir;
225
225
+
226
226
+
#[test]
227
227
+
fn test_parse_d_file() {
228
228
+
let temp_dir = TempDir::new().unwrap();
229
229
+
let d_file_path = temp_dir.path().join("test.d");
230
230
+
231
231
+
// Create a mock .d file
232
232
+
let mut d_file = fs::File::create(&d_file_path).unwrap();
233
233
+
writeln!(
234
234
+
d_file,
235
235
+
"/path/to/target: /path/to/dep1.rs /path/to/dep2.rs \\"
236
236
+
)
237
237
+
.unwrap();
238
238
+
writeln!(d_file, " /path/to/dep3.rs").unwrap();
239
239
+
240
240
+
// Create a tracker and point it to our test file
241
241
+
let mut tracker = DependencyTracker::new();
242
242
+
tracker.d_file_path = Some(d_file_path);
243
243
+
244
244
+
// This will fail to load the actual files, but we can check the parsing logic
245
245
+
let _ = tracker.reload_dependencies();
246
246
+
247
247
+
// We won't have any dependencies because the files don't exist,
248
248
+
// but we've verified the parsing doesn't crash
249
249
+
}
250
250
+
}