feat: configuration of project sources#130
feat: configuration of project sources#130victor-linroth-sensmetry wants to merge 26 commits intomainfrom
Conversation
|
Is there a particular reason for expanding |
No particular reason, just the first thing that came to mind. Do you think that would be a better solution? |
It might be easier to test/mock, and be a bit easier to reuse elsewhere. You can simply have No very strong preference though, so don't bother if it's a lot of work to refactor. |
|
Everything looks good to me in principle, would be a good to have a proper code review from @andrius-puksta-sensmetry before merging though. |
Well, this was precisely the type of feedback I was looking for, so I'll definitely consider it.
It's still a draft so far. I want to add some quality of life command line stuff also. |
67ca989 to
4173804
Compare
4173804 to
a418bd2
Compare
a418bd2 to
bf73d16
Compare
bd2ad17 to
0fa5369
Compare
|
Currently The problem stem from the fact that reference of types in macros should be absolute but the absolute paths vary depending on if you are inside or outside the crate. E.g. it's One could try to add them as associated types to Another solution would be to refactor the crates so that they are always used outside of the crate where they are exported. Would essentially mean that all the project stuff goes into it's own crate (except for Edit: I think the appropriate course of action is to factor out the projects into their own crate, but this would be a pretty wide reaching change so best to do as a separate PR after this is merged. |
|
Unsure if |
|
Still left to make sure Windows paths are handled properly (don't think they are at the moment). Edit: Since the sources only have paths that are |
49299d9 to
7a835d1
Compare
Signed-off-by: victor.linroth.sensmetry <victor.linroth@sensmetry.com>
Signed-off-by: victor.linroth.sensmetry <victor.linroth@sensmetry.com>
Signed-off-by: victor.linroth.sensmetry <victor.linroth@sensmetry.com>
Signed-off-by: victor.linroth.sensmetry <victor.linroth@sensmetry.com>
Signed-off-by: victor.linroth.sensmetry <victor.linroth@sensmetry.com>
| /// re-interpreted as filesystem-native paths relative to `project_path`. | ||
| #[derive(Clone, Debug)] | ||
| pub struct LocalSrcProject { | ||
| pub nominal_path: Option<Utf8PathBuf>, |
There was a problem hiding this comment.
Why is this an Option?
There was a problem hiding this comment.
Only needed if .sources() is called, otherwise unnecessary.
| - `--editable <EDITABLE>`: Path to local editable interchange project | ||
| - `--path <PATH>`: Path to local interchange project | ||
| - `--url-src <URL_SRC>`: URL to remote interchange project | ||
| - `--url-kpar <URL_KPAR>`: URL to remote interchange project archive (KPAR) |
There was a problem hiding this comment.
Update flag names
|
|
||
| let mut local_project = LocalSrcProject { project_path }; | ||
| let mut local_project = LocalSrcProject { | ||
| nominal_path: Some(project_path.clone()), |
There was a problem hiding this comment.
This is incorrect. project_path is absolute (line 77), and nominal_path must be relative to workspace root.
There was a problem hiding this comment.
Unless nominal_path here means something different.
There was a problem hiding this comment.
It's basically whatever path you want to be returned when calling .sources() on the project, so not necessarily relative to workspace root (although that might be the only usage at the moment).
I think this would be better solved if we had some form of Global Context object that could be used when calling .sources() so the caller gets some control over what type of path they want project represented by. Would make try_from_source simpler too. Such an object would require quite the bit of refactoring though so better done as a separate PR.
There was a problem hiding this comment.
Also, this particular seems to be unnecessary and can just be None.
| /// Path to local editable interchange project | ||
| #[arg(long, group = "source")] | ||
| pub as_editable: Option<String>, | ||
| /// Path to local interchange project | ||
| #[arg(long, group = "source")] | ||
| pub as_path: Option<String>, | ||
| /// URL to remote interchange project | ||
| #[arg(long, group = "source")] | ||
| pub as_url_src: Option<String>, | ||
| /// URL to remote interchange project archive (KPAR) | ||
| #[arg(long, group = "source")] | ||
| pub as_url_kpar: Option<String>, |
There was a problem hiding this comment.
Should mention here that these add a custom source to config file.
| MemoryResolver::from(overrides), | ||
| standard_resolver( | ||
| cwd, | ||
| if local_env_path.is_dir() { |
There was a problem hiding this comment.
This is eating errors. Maybe we should add is_file() and is_dir() to wrapfs, but make them e.g. fn is_file() -> Result<bool, SomeError>? This is mostly a nice-to-have, so can be done later.
| let mut overrides = Vec::new(); | ||
| for config_project in &config.projects { | ||
| for identifier in &config_project.identifiers { | ||
| let mut projects = Vec::new(); | ||
| for source in &config_project.sources { | ||
| projects.push(ProjectReference::new(AnyProject::try_from_source( | ||
| source.clone(), | ||
| project_root.clone(), | ||
| auth_policy.clone(), | ||
| client.clone(), | ||
| runtime.clone(), | ||
| )?)); | ||
| } | ||
| overrides.push((Iri::parse(identifier.as_str())?.into(), projects)); | ||
| } | ||
| } |
There was a problem hiding this comment.
Move this out into a function, it's done almost identically in 4 places
| nominal_path: Some(path.as_str().into()), | ||
| project_path: path.as_str().into(), |
There was a problem hiding this comment.
nominal_path should be made relative to sysand_env parent. Since it's not currently done in most places, a comment would be fine for now.
| let override_resolver = PriorityResolver::new( | ||
| MemoryResolver::from(overrides), | ||
| MemoryResolver { | ||
| iri_predicate: AcceptAll {}, | ||
| projects: memory_projects, | ||
| }, | ||
| ); | ||
| // TODO: Move out the runtime | ||
| let resolver = PriorityResolver::new( | ||
| override_resolver, | ||
| standard_resolver( |
There was a problem hiding this comment.
Ok for now, but if we add one more layer of PriorityResolver, something will have to be done.
| Ok(src_path) | ||
| } | ||
|
|
||
| fn relativize(path: &Utf8Path, root: &Utf8Path) -> Utf8PathBuf { |
There was a problem hiding this comment.
| fn relativize(path: &Utf8Path, root: &Utf8Path) -> Utf8PathBuf { | |
| /// Create a relative path from `root` to `path`. Both of them must either | |
| /// be absolute or otherwise start with the same prefix. | |
| fn relativize(path: &Utf8Path, root: &Utf8Path) -> Utf8PathBuf { |
There was a problem hiding this comment.
Also maybe return Option<Utf8PathBuf> so that caller can decide what to do in case relative path cannot be constructed.
There was a problem hiding this comment.
This function is useful (i.e. should be used, but currently isn't) in lots of places, so it should be moved to some utils file.
| // If prefixes (e.g. C: vs D: on Windows) differ, no relative path is possible. | ||
| if path.components().next() != root.components().next() { | ||
| return path.to_path_buf(); | ||
| } |
There was a problem hiding this comment.
Not sure what to do about Windows UNC vs non-UNC paths, as they always have different first components. Relevant here is that std's canonicalize returns UNC paths on Windows, but calling that on both inputs here would be kind of wasteful.
There was a problem hiding this comment.
Windows root-relative paths are also not handled correctly here.
There was a problem hiding this comment.
It may be a good idea after all to canonicalize both paths (or ensuring only canonical paths are given as args here) to avoid subtle bugs due to symlinks and Windows weirndess.
| Source::Editable { editable } => { | ||
| let nominal_path = editable.to_path_buf(); | ||
| let project = LocalSrcProject { | ||
| nominal_path: Some(nominal_path.to_string().into()), | ||
| project_path: project_root.as_ref().join(nominal_path.as_str()), | ||
| }; | ||
| Ok(AnyProject::Editable( | ||
| EditableProject::<LocalSrcProject>::new(nominal_path.as_str().into(), project), | ||
| )) | ||
| } |
There was a problem hiding this comment.
| Source::Editable { editable } => { | |
| let nominal_path = editable.to_path_buf(); | |
| let project = LocalSrcProject { | |
| nominal_path: Some(nominal_path.to_string().into()), | |
| project_path: project_root.as_ref().join(nominal_path.as_str()), | |
| }; | |
| Ok(AnyProject::Editable( | |
| EditableProject::<LocalSrcProject>::new(nominal_path.as_str().into(), project), | |
| )) | |
| } | |
| Source::Editable { | |
| editable: nominal_path, | |
| } => { | |
| let project = LocalSrcProject { | |
| nominal_path: Some(nominal_path.to_string().into()), | |
| project_path: project_root.as_ref().join(nominal_path.as_str()), | |
| }; | |
| Ok(AnyProject::Editable( | |
| EditableProject::<LocalSrcProject>::new( | |
| nominal_path.into_string().into(), | |
| project, | |
| ), | |
| )) | |
| } |
| .map_err(TryFromSourceError::LocalKpar)?, | ||
| )), | ||
| Source::LocalSrc { src_path } => { | ||
| let nominal_path = src_path.as_str().into(); |
There was a problem hiding this comment.
| let nominal_path = src_path.as_str().into(); | |
| let nominal_path = src_path.into_string().into(); |
| _ => Err(TryFromSourceError::UnsupportedSource(format!( | ||
| "{:?}", | ||
| source | ||
| ))), |
There was a problem hiding this comment.
| _ => Err(TryFromSourceError::UnsupportedSource(format!( | |
| "{:?}", | |
| source | |
| ))), | |
| _ => Err(TryFromSourceError::UnsupportedSource(format!("{source:?}"))), |
| return Ok(ResolutionOutcome::Unresolvable(format!( | ||
| "failed to resolve as file: {:?}", | ||
| msg, | ||
| ))); |
There was a problem hiding this comment.
| return Ok(ResolutionOutcome::Unresolvable(format!( | |
| "failed to resolve as file: {:?}", | |
| msg, | |
| ))); | |
| return Ok(ResolutionOutcome::Unresolvable(format!( | |
| "failed to resolve as file: {msg}", | |
| ))); |
|
|
||
| Sometimes you may wish to use a project that isn't resolvable through an | ||
| available index or you want to override the dependency resolution for other | ||
| reasons. In any case you can do this by adding the appropriate IRI and `Source` |
There was a problem hiding this comment.
| reasons. In any case you can do this by adding the appropriate IRI and `Source` | |
| reasons. You may also just want to give the usage a different identifier for readability. In any case you can do this by adding the appropriate IRI and `sources` |
| reasons. In any case you can do this by adding the appropriate IRI and `Source` | ||
| to a `project` entry in the `sysand.toml` configuration file at the root of | ||
| your project. This follows the same structure as found in the lockfile, where | ||
| `identifiers` are given as a list of IRI:s and `sources` are a list of sources. |
There was a problem hiding this comment.
| `identifiers` are given as a list of IRI:s and `sources` are a list of sources. | |
| `identifiers` are given as a list of IRIs and `sources` are a list of sources. |
Not sure what the correct way is.
| A project may have multiple identifiers in case it is referred to differently | ||
| by different projects, and multiple sources where the additional ones after the | ||
| first serve as backups in case the previous ones fail to resolve. Note that | ||
| these should be sources of the exact same project as determined by it's |
There was a problem hiding this comment.
| these should be sources of the exact same project as determined by it's | |
| these should be sources of the exact same project as determined by its |
| checksum, as otherwise you are likely to run into problems when syncing against | ||
| a lockfile. | ||
|
|
||
| Below we describe how add overriding sources directly to the configuration |
There was a problem hiding this comment.
| Below we describe how add overriding sources directly to the configuration | |
| Below we describe how to add overriding sources directly to the configuration |
|
|
||
| Below we describe how add overriding sources directly to the configuration | ||
| file, but it is also possible to do through the command line interface with the | ||
| [`sysand add`](../commands/add.md) command. |
There was a problem hiding this comment.
| [`sysand add`](../commands/add.md) command. | |
| [`sysand add`](../commands/add.md) command by using one of the `--as-*` flags. |
| file, but it is also possible to do through the command line interface with the | ||
| [`sysand add`](../commands/add.md) command. | ||
|
|
||
| ## Local projects |
There was a problem hiding this comment.
Maybe mention the exact CLI flag to use for each type here?
|
|
||
| ## Local editable projects | ||
|
|
||
| Normally when you add a project as a usage, `sysand` will copy and install it, |
There was a problem hiding this comment.
| Normally when you add a project as a usage, `sysand` will copy and install it, | |
| Normally when you add a project as a usage, Sysand will copy and install it, |
sysand should refer to the CLI specifically, whereas Sysand refers to the product in general
| ## Local editable projects | ||
|
|
||
| Normally when you add a project as a usage, `sysand` will copy and install it, | ||
| so any changes made to the project after will not affect the installed project. |
There was a problem hiding this comment.
| so any changes made to the project after will not affect the installed project. | |
| so any changes made to the original project afterwards will not affect the installed project. |
| the following entry to your `sysand.toml`. | ||
|
|
||
| ```toml | ||
| [[project]] |
There was a problem hiding this comment.
Maybe rename this to [[source]] to give us space to put the current project metadata here (if we ever decide to do this)?
| #[proc_macro_derive(ProjectRead)] | ||
| pub fn project_read_derive(input: TokenStream) -> TokenStream { |
There was a problem hiding this comment.
Add a doc comment describing what this derive generates (i.e. what the derived impl does given an example arg).
| #[proc_macro_derive(ProjectMut)] | ||
| pub fn project_mut_derive(input: TokenStream) -> TokenStream { |
There was a problem hiding this comment.
Description also needed here
| } else { | ||
| src_path.into() | ||
| }; |
There was a problem hiding this comment.
Why is it assumed here that src_path is relative? What's stopping me from doing --as-path /path/to/project and breaking this assumption?
| fn get_relative<P: Into<Utf8PathBuf>>(src_path: P, project_root: &Utf8Path) -> Result<Utf8PathBuf> { | ||
| let src_path = if wrapfs::current_dir()? != project_root { | ||
| let path = relativize( | ||
| &Utf8Path::new(&src_path.into()).canonicalize_utf8()?, |
There was a problem hiding this comment.
| &Utf8Path::new(&src_path.into()).canonicalize_utf8()?, | |
| fn get_relative<P: Into<Utf8PathBuf> + AsRef<Utf8Path>>( | |
| src_path: P, | |
| project_root: &Utf8Path, | |
| ) -> Result<Utf8PathBuf> { | |
| let src_path = if wrapfs::current_dir()? != project_root { | |
| let path = relativize(&src_path.as_ref().canonicalize_utf8()?, project_root); |
| pub fn command_add<S: AsRef<str>, Policy: HTTPAuthentication>( | ||
| iri: S, | ||
| versions_constraint: Option<String>, | ||
| no_lock: bool, | ||
| no_sync: bool, | ||
| resolution_opts: ResolutionOptions, | ||
| config: &Config, | ||
| source_opts: ProjectSourceOptions, | ||
| mut config: Config, | ||
| config_file: Option<String>, | ||
| no_config: bool, | ||
| current_project: Option<LocalSrcProject>, | ||
| client: reqwest_middleware::ClientWithMiddleware, | ||
| runtime: Arc<tokio::runtime::Runtime>, | ||
| auth_policy: Arc<Policy>, | ||
| ) -> Result<()> { |
There was a problem hiding this comment.
This is really getting out of hand, but fixing is out of scope here.
|
|
||
| let config_path = config_file | ||
| .map(Utf8PathBuf::from) | ||
| .or((!no_config).then(|| project_root.join(CONFIG_FILE))); |
There was a problem hiding this comment.
Move this to where it's used.
| for r in root_iter { | ||
| if let Utf8Component::Normal(_) = r { | ||
| result.push(".."); | ||
| } | ||
| } |
There was a problem hiding this comment.
This is not always correct due to symlinks, unless both paths are already canonical.
There was a problem hiding this comment.
This will also return an empty path if path and base are the same.
There was a problem hiding this comment.
And the paths will use platform-dependent separator: \ on Windows and / everywhere? else. Is config.toml intended to be portable?
Main new features added by this PR:
sysand.toml(or given config). Allows for portable specification of local and remote sources.--editable,--path,--url-srcand--url-kparforsysand addthat automatically updates config file. These also get removed withsysand remove.In order to simplify implementing the above the
corelibrary was build out with the following changes:ProjectReadandProjectMutderive macros for easy enum composition of project structs.CachedProject<Local, Remote>struct for when theLocalproject is easier to access (e.g. on disk or in memory) vsRemote, but you still want.sources()to give the remote sources.CombinedResolversimplified usingderive(ProjectRead)andCachedProject.ProjectReference<Project>struct. When working withReadEnvironmentthe projects need to be clonable, but this might not always be a good idea, e.g. some project hold ownership of temp dirs.ProjectReferenceis basically andArcwrapper for a project to be used as a compatibility layer.AnyProjectstruct combining the different projects supported for source configuration. Should be extended to include Git projects in future.LocalSrcProjectandLocalKparProjectnow has optional nominal paths in addition to the regular path. This comes from the choice to have configured source always be relative to the project root but this not always being available everywhere. (Could perhaps be simplified if we added a Global Context).In addition there where some small changes that came about when experimenting with different solutions:
MemoryEnvcan now be used with a generic project.MemoryResolvercan be conveniently constructed from iterator/array/Vec.These ended up not being used here, but they could be useful at some later point so where left in.
Lastly, the
verbose/quietoptions were removed from the config. They were mostly placeholders to begin with and now that there are more real options present in the config they don't serve much of a purpose. Removing them also means that the logger can be initialized almost immediately on start, opening up the potential for logging more things, like e.g. the loading of the config files.