Skip to content

Commit 04b4fdc

Browse files
committed
Reload objects when changed externally
Uses file modification timestamp polling for project config and objects to avoid unneeded complexity from the filesystem notification watcher. Allows disabling `build_base` as well for projects using an external build system.
1 parent 803eaaf commit 04b4fdc

File tree

8 files changed

+190
-76
lines changed

8 files changed

+190
-76
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ dirs = "5.0.1"
3232
eframe = { version = "0.23.0", features = ["persistence"] }
3333
egui = "0.23.0"
3434
egui_extras = "0.23.0"
35+
filetime = "0.2.22"
3536
flagset = "0.4.4"
3637
globset = { version = "0.4.13", features = ["serde1"] }
3738
log = "0.4.20"

src/app.rs

Lines changed: 65 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::{
22
default::Default,
3+
fs,
34
path::{Path, PathBuf},
45
rc::Rc,
56
sync::{
@@ -9,16 +10,18 @@ use std::{
910
time::Duration,
1011
};
1112

12-
use globset::{Glob, GlobSet, GlobSetBuilder};
13+
use filetime::FileTime;
14+
use globset::{Glob, GlobSet};
1315
use notify::{RecursiveMode, Watcher};
1416
use time::UtcOffset;
1517

1618
use crate::{
1719
app_config::{deserialize_config, AppConfigVersion},
18-
config::{
19-
build_globset, load_project_config, ProjectObject, ProjectObjectNode, CONFIG_FILENAMES,
20+
config::{build_globset, load_project_config, ProjectObject, ProjectObjectNode},
21+
jobs::{
22+
objdiff::{start_build, ObjDiffConfig},
23+
Job, JobQueue, JobResult, JobStatus,
2024
},
21-
jobs::{objdiff::start_build, Job, JobQueue, JobResult, JobStatus},
2225
views::{
2326
appearance::{appearance_window, Appearance},
2427
config::{config_ui, project_window, ConfigViewState, DEFAULT_WATCH_PATTERNS},
@@ -51,6 +54,12 @@ pub struct ObjectConfig {
5154
pub complete: Option<bool>,
5255
}
5356

57+
#[derive(Clone, Eq, PartialEq)]
58+
pub struct ProjectConfigInfo {
59+
pub path: PathBuf,
60+
pub timestamp: FileTime,
61+
}
62+
5463
#[inline]
5564
fn bool_true() -> bool { true }
5665

@@ -77,6 +86,8 @@ pub struct AppConfig {
7786
pub base_obj_dir: Option<PathBuf>,
7887
#[serde(default)]
7988
pub selected_obj: Option<ObjectConfig>,
89+
#[serde(default = "bool_true")]
90+
pub build_base: bool,
8091
#[serde(default)]
8192
pub build_target: bool,
8293
#[serde(default = "bool_true")]
@@ -101,7 +112,9 @@ pub struct AppConfig {
101112
#[serde(skip)]
102113
pub queue_build: bool,
103114
#[serde(skip)]
104-
pub project_config_loaded: bool,
115+
pub queue_reload: bool,
116+
#[serde(skip)]
117+
pub project_config_info: Option<ProjectConfigInfo>,
105118
}
106119

107120
impl Default for AppConfig {
@@ -114,6 +127,7 @@ impl Default for AppConfig {
114127
target_obj_dir: None,
115128
base_obj_dir: None,
116129
selected_obj: None,
130+
build_base: true,
117131
build_target: false,
118132
rebuild_on_changes: true,
119133
auto_update_check: true,
@@ -125,7 +139,8 @@ impl Default for AppConfig {
125139
config_change: false,
126140
obj_change: false,
127141
queue_build: false,
128-
project_config_loaded: false,
142+
queue_reload: false,
143+
project_config_info: None,
129144
}
130145
}
131146
}
@@ -148,7 +163,7 @@ impl AppConfig {
148163
self.config_change = true;
149164
self.obj_change = true;
150165
self.queue_build = false;
151-
self.project_config_loaded = false;
166+
self.project_config_info = None;
152167
}
153168

154169
pub fn set_target_obj_dir(&mut self, path: PathBuf) {
@@ -180,7 +195,6 @@ pub struct App {
180195
view_state: ViewState,
181196
config: AppConfigRef,
182197
modified: Arc<AtomicBool>,
183-
config_modified: Arc<AtomicBool>,
184198
watcher: Option<notify::RecommendedWatcher>,
185199
relaunch_path: Rc<Mutex<Option<PathBuf>>>,
186200
should_relaunch: bool,
@@ -286,8 +300,10 @@ impl App {
286300
};
287301
let config = &mut *config;
288302

289-
if self.config_modified.swap(false, Ordering::Relaxed) {
290-
config.config_change = true;
303+
if let Some(info) = &config.project_config_info {
304+
if file_modified(&info.path, info.timestamp) {
305+
config.config_change = true;
306+
}
291307
}
292308

293309
if config.config_change {
@@ -305,21 +321,14 @@ impl App {
305321
drop(self.watcher.take());
306322

307323
if let Some(project_dir) = &config.project_dir {
308-
if !config.watch_patterns.is_empty() {
309-
match build_globset(&config.watch_patterns)
310-
.map_err(anyhow::Error::new)
311-
.and_then(|globset| {
312-
create_watcher(
313-
self.modified.clone(),
314-
self.config_modified.clone(),
315-
project_dir,
316-
globset,
317-
)
324+
match build_globset(&config.watch_patterns).map_err(anyhow::Error::new).and_then(
325+
|globset| {
326+
create_watcher(self.modified.clone(), project_dir, globset)
318327
.map_err(anyhow::Error::new)
319-
}) {
320-
Ok(watcher) => self.watcher = Some(watcher),
321-
Err(e) => log::error!("Failed to create watcher: {e}"),
322-
}
328+
},
329+
) {
330+
Ok(watcher) => self.watcher = Some(watcher),
331+
Err(e) => log::error!("Failed to create watcher: {e}"),
323332
}
324333
config.watcher_change = false;
325334
}
@@ -337,11 +346,32 @@ impl App {
337346
config.queue_build = true;
338347
}
339348

349+
if let Some(result) = &diff_state.build {
350+
if let Some(obj) = &result.first_obj {
351+
if file_modified(&obj.path, obj.timestamp) {
352+
config.queue_reload = true;
353+
}
354+
}
355+
if let Some(obj) = &result.second_obj {
356+
if file_modified(&obj.path, obj.timestamp) {
357+
config.queue_reload = true;
358+
}
359+
}
360+
}
361+
340362
// Don't clear `queue_build` if a build is running. A file may have been modified during
341363
// the build, so we'll start another build after the current one finishes.
342364
if config.queue_build && config.selected_obj.is_some() && !jobs.is_running(Job::ObjDiff) {
343-
jobs.push(start_build(self.config.clone()));
365+
jobs.push(start_build(ObjDiffConfig::from_config(config)));
344366
config.queue_build = false;
367+
config.queue_reload = false;
368+
} else if config.queue_reload && !jobs.is_running(Job::ObjDiff) {
369+
let mut diff_config = ObjDiffConfig::from_config(config);
370+
// Don't build, just reload the current files
371+
diff_config.build_base = false;
372+
diff_config.build_target = false;
373+
jobs.push(start_build(diff_config));
374+
config.queue_reload = false;
345375
}
346376
}
347377
}
@@ -486,16 +516,9 @@ impl eframe::App for App {
486516

487517
fn create_watcher(
488518
modified: Arc<AtomicBool>,
489-
config_modified: Arc<AtomicBool>,
490519
project_dir: &Path,
491520
patterns: GlobSet,
492521
) -> notify::Result<notify::RecommendedWatcher> {
493-
let mut config_patterns = GlobSetBuilder::new();
494-
for filename in CONFIG_FILENAMES {
495-
config_patterns.add(Glob::new(filename).unwrap());
496-
}
497-
let config_patterns = config_patterns.build().unwrap();
498-
499522
let base_dir = project_dir.to_owned();
500523
let mut watcher =
501524
notify::recommended_watcher(move |res: notify::Result<notify::Event>| match res {
@@ -510,9 +533,7 @@ fn create_watcher(
510533
let Ok(path) = path.strip_prefix(&base_dir) else {
511534
continue;
512535
};
513-
if config_patterns.is_match(path) {
514-
config_modified.store(true, Ordering::Relaxed);
515-
} else if patterns.is_match(path) {
536+
if patterns.is_match(path) {
516537
modified.store(true, Ordering::Relaxed);
517538
}
518539
}
@@ -523,3 +544,12 @@ fn create_watcher(
523544
watcher.watch(project_dir, RecursiveMode::Recursive)?;
524545
Ok(watcher)
525546
}
547+
548+
#[inline]
549+
fn file_modified(path: &Path, last_ts: FileTime) -> bool {
550+
if let Ok(metadata) = fs::metadata(path) {
551+
FileTime::from_last_modification_time(&metadata) != last_ts
552+
} else {
553+
false
554+
}
555+
}

src/config.rs

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,54 @@
11
use std::{
22
fs::File,
3+
io::Read,
34
path::{Component, Path, PathBuf},
45
};
56

6-
use anyhow::{bail, Context, Result};
7+
use anyhow::{bail, Result};
8+
use filetime::FileTime;
79
use globset::{Glob, GlobSet, GlobSetBuilder};
810

9-
use crate::{app::AppConfig, views::config::DEFAULT_WATCH_PATTERNS};
11+
use crate::{
12+
app::{AppConfig, ProjectConfigInfo},
13+
views::config::DEFAULT_WATCH_PATTERNS,
14+
};
15+
16+
#[inline]
17+
fn bool_true() -> bool { true }
1018

1119
#[derive(Default, Clone, serde::Deserialize)]
12-
#[serde(default)]
1320
pub struct ProjectConfig {
21+
#[serde(default)]
1422
pub min_version: Option<String>,
23+
#[serde(default)]
1524
pub custom_make: Option<String>,
25+
#[serde(default)]
1626
pub target_dir: Option<PathBuf>,
27+
#[serde(default)]
1728
pub base_dir: Option<PathBuf>,
29+
#[serde(default = "bool_true")]
30+
pub build_base: bool,
31+
#[serde(default)]
1832
pub build_target: bool,
33+
#[serde(default)]
1934
pub watch_patterns: Option<Vec<Glob>>,
20-
#[serde(alias = "units")]
35+
#[serde(default, alias = "units")]
2136
pub objects: Vec<ProjectObject>,
2237
}
2338

2439
#[derive(Default, Clone, serde::Deserialize)]
2540
pub struct ProjectObject {
41+
#[serde(default)]
2642
pub name: Option<String>,
43+
#[serde(default)]
2744
pub path: Option<PathBuf>,
45+
#[serde(default)]
2846
pub target_path: Option<PathBuf>,
47+
#[serde(default)]
2948
pub base_path: Option<PathBuf>,
49+
#[serde(default)]
3050
pub reverse_fn_order: Option<bool>,
51+
#[serde(default)]
3152
pub complete: Option<bool>,
3253
}
3354

@@ -120,7 +141,7 @@ pub fn load_project_config(config: &mut AppConfig) -> Result<()> {
120141
let Some(project_dir) = &config.project_dir else {
121142
return Ok(());
122143
};
123-
if let Some(result) = try_project_config(project_dir) {
144+
if let Some((result, info)) = try_project_config(project_dir) {
124145
let project_config = result?;
125146
if let Some(min_version) = &project_config.min_version {
126147
let version_str = env!("CARGO_PKG_VERSION");
@@ -133,6 +154,7 @@ pub fn load_project_config(config: &mut AppConfig) -> Result<()> {
133154
config.custom_make = project_config.custom_make;
134155
config.target_obj_dir = project_config.target_dir.map(|p| project_dir.join(p));
135156
config.base_obj_dir = project_config.base_dir.map(|p| project_dir.join(p));
157+
config.build_base = project_config.build_base;
136158
config.build_target = project_config.build_target;
137159
config.watch_patterns = project_config.watch_patterns.unwrap_or_else(|| {
138160
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
@@ -141,34 +163,39 @@ pub fn load_project_config(config: &mut AppConfig) -> Result<()> {
141163
config.objects = project_config.objects;
142164
config.object_nodes =
143165
build_nodes(&config.objects, project_dir, &config.target_obj_dir, &config.base_obj_dir);
144-
config.project_config_loaded = true;
166+
config.project_config_info = Some(info);
145167
}
146168
Ok(())
147169
}
148170

149-
fn try_project_config(dir: &Path) -> Option<Result<ProjectConfig>> {
171+
fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> {
150172
for filename in CONFIG_FILENAMES.iter() {
151173
let config_path = dir.join(filename);
152-
if config_path.is_file() {
153-
return match filename.contains("json") {
154-
true => Some(read_json_config(&config_path)),
155-
false => Some(read_yml_config(&config_path)),
174+
let Ok(mut file) = File::open(&config_path) else {
175+
continue;
176+
};
177+
let metadata = file.metadata();
178+
if let Ok(metadata) = metadata {
179+
if !metadata.is_file() {
180+
continue;
181+
}
182+
let ts = FileTime::from_last_modification_time(&metadata);
183+
let config = match filename.contains("json") {
184+
true => read_json_config(&mut file),
185+
false => read_yml_config(&mut file),
156186
};
187+
return Some((config, ProjectConfigInfo { path: config_path, timestamp: ts }));
157188
}
158189
}
159190
None
160191
}
161192

162-
fn read_yml_config(config_path: &Path) -> Result<ProjectConfig> {
163-
let mut reader = File::open(config_path)
164-
.with_context(|| format!("Failed to open config file '{}'", config_path.display()))?;
165-
Ok(serde_yaml::from_reader(&mut reader)?)
193+
fn read_yml_config<R: Read>(reader: &mut R) -> Result<ProjectConfig> {
194+
Ok(serde_yaml::from_reader(reader)?)
166195
}
167196

168-
fn read_json_config(config_path: &Path) -> Result<ProjectConfig> {
169-
let mut reader = File::open(config_path)
170-
.with_context(|| format!("Failed to open config file '{}'", config_path.display()))?;
171-
Ok(serde_json::from_reader(&mut reader)?)
197+
fn read_json_config<R: Read>(reader: &mut R) -> Result<ProjectConfig> {
198+
Ok(serde_json::from_reader(reader)?)
172199
}
173200

174201
pub fn build_globset(vec: &[Glob]) -> std::result::Result<GlobSet, globset::Error> {

0 commit comments

Comments
 (0)