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
use cargo_metadata::MetadataCommand;
use kinetics_parser::Package;
use std::path::{Path, PathBuf};
use crate::project::ConfigFile;
// Workspace definition for a project.
//
// A non-workspace project would have one member
// with it's path identical to workspace_root.
//
// Otherwise it's a real workspace.
#[derive(Debug, Default, Clone)]
pub struct Workspace {
pub root_path: PathBuf,
pub packages: Vec<Package>,
}
impl Workspace {
pub fn from_path(path: &Path) -> eyre::Result<Self> {
let metadata = MetadataCommand::new().current_dir(path).exec()?;
// Convert [cargo_metadata::Package] into a simpler representation
// keeping only necessary data (in order to avoid these transforms at consuming code):
// - the name from Cargo.toml
// - the relative path from workspace root to the package dir.
//
// The closure is used instead of impl From
// in order to access metadata from outer scope.
let convert_package = |pkg: &cargo_metadata::Package| -> Option<Package> {
Some(Package {
name: ConfigFile::cargo_toml_name(pkg.manifest_path.parent()?.as_std_path())
.ok()?,
relative_path: pkg
.manifest_path
.strip_prefix(&metadata.workspace_root)
.ok()?
.parent()? // Remove filename and keep only the dir name.
.into(),
})
};
// Validate workspace rules for kinetics:
// 1. Workspace as a single kinetics project:
// - the workspace root MUST contain kinetics.toml;
// - workspace members cannot have kinetics.toml - throw an error.
// 2. Standalone kinetics project:
// - the project can contain kinetics.toml;
// - for the name fall back to Cargo.toml.
// 3. Workspace member is a kinetics project:
// - workspace member from where the command is called MUST have kinetics.toml.
// - the workspace root cannot have kinetics.toml - throw an error.
let workspace_config = metadata.workspace_root.join("kinetics.toml");
// Option 3. A call within a workspace member which is a kinetics project
if metadata.workspace_root != path && path.join("kinetics.toml").exists() {
if workspace_config.exists() {
eyre::bail!("Workspace is not allowed to have `kinetics.toml` within its root and within its members at the same time.");
}
// Return `Workspace` with only one package in the `packages` list - current project.
let cwd_manifest = path.join("Cargo.toml");
return Ok(Self {
packages: metadata
.workspace_members
.into_iter()
.filter_map(|member| {
metadata
.packages
.iter()
.find(|pkg| pkg.id == member)
.and_then(|pkg| {
// Take only the member corresponding to cwd - current project
// and discard all other members.
if pkg.manifest_path == cwd_manifest {
convert_package(pkg)
} else {
None
}
})
})
.collect(),
root_path: metadata.workspace_root.into_std_path_buf(),
});
}
// Options 1 and 2. A call from root.
let members_configs: Vec<_> = metadata
.workspace_members
.iter()
.filter_map(|member| {
metadata
.packages
.iter()
.find(|pkg| pkg.id == *member)
.and_then(|pkg| pkg.manifest_path.parent())
.and_then(|dir| {
let config = dir.join("kinetics.toml");
if config.exists() {
Some(config)
} else {
None
}
})
})
.collect();
// For a standalone crate workspace construct errors if kinetics.toml exists.
// The reason is that in this case the config is present at root and in the member
// (since they are the same entity), and a check for no config conflicts fails.
let is_standalone_crate = members_configs.len() == 1
&& members_configs
.first()
.is_some_and(|c| *c == workspace_config);
if !is_standalone_crate && workspace_config.exists() && !members_configs.is_empty() {
eyre::bail!("Workspace is not allowed to have `kinetics.toml` within its root and within its members at the same time.");
}
Ok(Self {
packages: metadata
.workspace_members
.into_iter()
.filter_map(|member| {
metadata
.packages
.iter()
.find(|pkg| pkg.id == member)
.and_then(convert_package)
})
.collect(),
root_path: metadata.workspace_root.into_std_path_buf(),
})
}
}