Skip to content

Commit c88bad4

Browse files
committed
Cleanup the logic for "merging" package "patches" (config setting)
The old logic was pretty complex and only necessary to build the correct relative paths to the patch files (the paths in `pkg.toml` must be prepended with the relative path to the directory containing the `pkg.toml` file). Since the recently released version 0.14.0 of the `config` crate we can access the "origin" of a configuration value (`Value::origin()`) so we can use that information to avoid having to check if the `patches` have changed every time we merge another `pkg.toml` file. Unfortunately this does currently require a dedicated source implementation (`PkgTomlSource` but actually why not) since `config::File::from_str()` always sets the URI/origin to `None` and the new `set_patches_base_dir()` function is a bit of a hack... IMO the new code is much more readable, more efficient, and overall still cleaner though (most of the new code is for error handling and the custom `Source` implementation). [0]: https://github.com/mehcode/config-rs/blob/0.14.0/CHANGELOG.md#0140---2024-02-01 Signed-off-by: Michael Weiss <[email protected]>
1 parent 2ec9497 commit c88bad4

File tree

4 files changed

+115
-89
lines changed

4 files changed

+115
-89
lines changed

src/package/package.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
//
1010

1111
use std::collections::HashMap;
12+
use std::path::Path;
1213
use std::path::PathBuf;
1314

1415
use getset::Getters;
@@ -100,6 +101,14 @@ impl Package {
100101
}
101102
}
102103

104+
// A function to prepend the path of the base directory to the relative paths of the patches
105+
// (it usually only makes sense to call this function once!):
106+
pub fn set_patches_base_dir(&mut self, dir: &Path) {
107+
for patch in self.patches.iter_mut() {
108+
*patch = dir.join(patch.as_path());
109+
}
110+
}
111+
103112
#[cfg(test)]
104113
pub fn set_dependencies(&mut self, dependencies: Dependencies) {
105114
self.dependencies = dependencies;

src/repository/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ mod repository;
1313
pub use repository::*;
1414

1515
mod fs;
16+
mod pkg_toml_source;

src/repository/pkg_toml_source.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright (c) 2020-2024 science+computing ag and other contributors
2+
//
3+
// This program and the accompanying materials are made
4+
// available under the terms of the Eclipse Public License 2.0
5+
// which is available at https://www.eclipse.org/legal/epl-2.0/
6+
//
7+
// SPDX-License-Identifier: EPL-2.0
8+
9+
// A custom `Source` implementation for the `config` crate to tack the `pkg.toml` file path as URI/origin
10+
// in addition to the content (basically a replacement for `config::File::from_str(str, format)`).
11+
12+
use std::path::Path;
13+
14+
use config::ConfigError;
15+
use config::FileFormat;
16+
use config::Format;
17+
use config::Map;
18+
use config::Source;
19+
use config::Value;
20+
21+
#[derive(Clone, Debug)]
22+
pub struct PkgTomlSource {
23+
content: String,
24+
uri: String,
25+
}
26+
27+
impl PkgTomlSource {
28+
pub fn new(path: &Path, content: String) -> Self {
29+
// We could also use `path.to_str()` for proper error handling:
30+
let path = path.to_string_lossy().to_string();
31+
PkgTomlSource { content, uri: path }
32+
}
33+
}
34+
35+
impl Source for PkgTomlSource {
36+
fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
37+
Box::new((*self).clone())
38+
}
39+
40+
fn collect(&self) -> Result<Map<String, Value>, ConfigError> {
41+
FileFormat::Toml
42+
.parse(Some(&self.uri), &self.content)
43+
.map_err(|cause| ConfigError::FileParse {
44+
uri: Some(self.uri.clone()),
45+
cause,
46+
})
47+
}
48+
}

src/repository/repository.rs

Lines changed: 57 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,6 @@ use anyhow::anyhow;
1616
use anyhow::Context;
1717
use anyhow::Error;
1818
use anyhow::Result;
19-
use resiter::AndThen;
20-
use resiter::FilterMap;
21-
use resiter::Map;
2219
use tracing::trace;
2320

2421
use crate::package::Package;
@@ -52,30 +49,6 @@ impl Repository {
5249
trace!("Loading files from filesystem");
5350
let fsr = FileSystemRepresentation::load(path.to_path_buf())?;
5451

55-
// Helper function to extract the `patches` array from a package config/definition:
56-
fn get_patches(
57-
config: &config::ConfigBuilder<config::builder::DefaultState>,
58-
path: &Path,
59-
) -> Result<Vec<PathBuf>> {
60-
// TODO: Avoid unnecessary config building (inefficient):
61-
let config = config.build_cloned().context(anyhow!(
62-
"Failed to load the following TOML file: {}",
63-
path.display()
64-
))?;
65-
match config.get_array("patches") {
66-
Ok(v) => v
67-
.into_iter()
68-
.map(config::Value::into_string)
69-
.map_err(Error::from)
70-
.map_err(|e| e.context("patches must be strings"))
71-
.map_err(Error::from)
72-
.map_ok(PathBuf::from)
73-
.collect(),
74-
Err(config::ConfigError::NotFound(_)) => Ok(Vec::with_capacity(0)),
75-
Err(e) => Err(Error::from(e)),
76-
}
77-
}
78-
7952
let leaf_files = fsr
8053
.files()
8154
.par_iter()
@@ -86,74 +59,69 @@ impl Repository {
8659
Err(e) => Some(Err(e)),
8760
});
8861
progress.set_length(leaf_files.clone().count().try_into()?);
89-
leaf_files.inspect(|r| trace!("Loading files for {:?}", r))
62+
leaf_files
63+
.inspect(|r| trace!("Loading files for {:?}", r))
9064
.map(|path| {
9165
progress.inc(1);
9266
let path = path?;
93-
fsr.get_files_for(path)?
67+
let config = fsr.get_files_for(path)?
9468
.iter()
69+
// Load all "layers":
9570
.inspect(|(path, _)| trace!("Loading layer at {}", path.display()))
96-
.fold(Ok(Config::builder()) as Result<_>, |config, (path, content)| {
97-
let mut config = config?;
98-
99-
let patches_before_merge = get_patches(&config, path)?;
100-
config = config.add_source(config::File::from_str(content, config::FileFormat::Toml));
101-
let patches_after_merge = get_patches(&config, path)?;
102-
103-
// TODO: Get rid of the unnecessarily complex handling of the `patches` configuration setting:
104-
// Ideally this would be handled by the `config` crate (this is
105-
// already the case for all other "settings" but in this case we also need
106-
// to prepend the corresponding directory path).
107-
let patches = if patches_before_merge == patches_after_merge {
108-
patches_before_merge
109-
} else {
110-
// The patches have changed since the `config.merge()` of the next
111-
// `pkg.toml` file so we have to build the paths to the patch files
112-
// by prepending the path to the directory of the `pkg.toml` file since
113-
// `path` is only available in this "iteration".
114-
patches_after_merge
115-
.into_iter()
116-
// Prepend the path of the directory of the `pkg.toml` file to the name of the patch:
117-
.map(|p| if let Some(current_dir) = path.parent() {
118-
Ok(current_dir.join(p))
119-
} else {
120-
Err(anyhow!("Path should point to path with parent, but doesn't: {}", path.display()))
121-
})
122-
.inspect(|patch| trace!("Patch: {:?}", patch))
123-
// If the patch file exists, use it (as config::Value).
124-
// Otherwise we have an error here, because we're referring to a non-existing file:
125-
.and_then_ok(|patch| if patch.exists() {
126-
Ok(Some(patch))
127-
} else {
128-
Err(anyhow!("Patch does not exist: {}", patch.display()))
129-
.with_context(|| anyhow!("The patch is declared here: {}", path.display()))
130-
})
131-
.filter_map_ok(|o| o)
132-
.collect::<Result<Vec<_>>>()?
133-
};
134-
135-
trace!("Patches after postprocessing merge: {:?}", patches);
136-
let patches = patches
137-
.into_iter()
138-
.map(|p| p.display().to_string())
139-
.map(config::Value::from)
140-
.collect::<Vec<_>>();
141-
{
142-
// Update the `patches` configuration setting:
143-
let mut builder = Config::builder();
144-
builder = builder.set_default("patches", config::Value::from(patches))?;
145-
config = config.add_source(builder.build()?);
146-
// Ideally we'd use `config.set()` but that is a permanent override (so
147-
// subsequent `config.merge()` merges won't have an effect on
148-
// "patches"). There's also `config.set_once()` but that only lasts
149-
// until the next `config.merge()` and `config.set_default()` only sets
150-
// a default value.
151-
}
152-
Ok(config)
71+
.fold(Config::builder(), |config_builder, (path, content)| {
72+
use crate::repository::pkg_toml_source::PkgTomlSource;
73+
config_builder.add_source(PkgTomlSource::new(path, (*content).to_string()))
15374
})
154-
.and_then(|c| c.build()?.try_deserialize::<Package>().map_err(Error::from)
155-
.with_context(|| anyhow!("Could not load package configuration: {}", path.display())))
156-
.map(|pkg| ((pkg.name().clone(), pkg.version().clone()), pkg))
75+
.build()?;
76+
77+
let patches_value = config.get_array("patches");
78+
let mut pkg = config
79+
.try_deserialize::<Package>()
80+
.map_err(Error::from)
81+
.with_context(|| {
82+
anyhow!("Could not load package configuration: {}", path.display())
83+
})?;
84+
85+
if !pkg.patches().is_empty() {
86+
// We have to build the full relative paths to the patch files by
87+
// prepending the path to the directory of the `pkg.toml` file they've
88+
// been defined in so that they can be found later.
89+
let patches = patches_value.context(anyhow!(
90+
"Bug: Could not get the \"patches\" value for: {}",
91+
path.display()
92+
))?;
93+
let first_patch_value = patches.first().ok_or(anyhow!(
94+
"Bug: Could not get the first \"patches\" entry for: {}",
95+
path.display()
96+
))?;
97+
// Get the origin (path to the `pkg.toml` file) for the "patches"
98+
// setting (it must currently be the same for all array entries):
99+
let origin_path = first_patch_value.origin().map(PathBuf::from).ok_or(anyhow!(
100+
"Bug: Could not get the origin of the first \"patches\" entry for: {}",
101+
path.display()
102+
))?;
103+
// Note: `parent()` only "Returns None if the path terminates in a root
104+
// or prefix, or if it’s the empty string." so this should never happen:
105+
let origin_dir_path = origin_path.parent().ok_or(anyhow!(
106+
"Bug: Could not get the origin's parent of the first \"patches\" entry for: {}",
107+
path.display()
108+
))?;
109+
pkg.set_patches_base_dir(origin_dir_path);
110+
// Check if the patches exist:
111+
for patch in pkg.patches() {
112+
if !patch.exists() {
113+
return Err(anyhow!(
114+
"Patch does not exist: {}",
115+
patch.display()
116+
))
117+
.with_context(|| {
118+
anyhow!("The patch is declared here: {}", path.display())
119+
});
120+
}
121+
}
122+
}
123+
124+
Ok(((pkg.name().clone(), pkg.version().clone()), pkg))
157125
})
158126
.collect::<Result<BTreeMap<_, _>>>()
159127
.map(Repository::new)

0 commit comments

Comments
 (0)