refactor(xtask): parse Cargo manifests with toml
This commit is contained in:
+91
-76
@@ -895,12 +895,11 @@ fn validate_cargo_metadata_dependency_closures(
|
|||||||
collect_cargo_manifests(root, &mut manifests)?;
|
collect_cargo_manifests(root, &mut manifests)?;
|
||||||
let mut deps_by_package = BTreeMap::new();
|
let mut deps_by_package = BTreeMap::new();
|
||||||
for manifest in manifests {
|
for manifest in manifests {
|
||||||
let text = fs::read_to_string(&manifest)
|
let policy = load_cargo_manifest_policy(&manifest)?;
|
||||||
.map_err(|err| format!("{}: {err}", manifest.display()))?;
|
let Some(package) = policy.package_name() else {
|
||||||
let Some(package) = parse_package_name(&text) else {
|
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
deps_by_package.insert(package, parse_manifest_dependencies(&text));
|
deps_by_package.insert(package.to_string(), policy.dependency_names());
|
||||||
}
|
}
|
||||||
|
|
||||||
validate_package_closure_excludes("fparkan-headless", &deps_by_package, failures);
|
validate_package_closure_excludes("fparkan-headless", &deps_by_package, failures);
|
||||||
@@ -1109,9 +1108,8 @@ fn validate_workspace_license(root: &Path, failures: &mut Vec<String>) -> Result
|
|||||||
manifests.dedup();
|
manifests.dedup();
|
||||||
|
|
||||||
for manifest in manifests {
|
for manifest in manifests {
|
||||||
let text = fs::read_to_string(&manifest)
|
let policy = load_cargo_manifest_policy(&manifest)?;
|
||||||
.map_err(|err| format!("{}: {err}", manifest.display()))?;
|
let explicit_license = policy.license();
|
||||||
let explicit_license = parse_manifest_license(&text);
|
|
||||||
let is_root = manifest == root.join("Cargo.toml");
|
let is_root = manifest == root.join("Cargo.toml");
|
||||||
if is_root {
|
if is_root {
|
||||||
if explicit_license.as_deref() != Some(expected) {
|
if explicit_license.as_deref() != Some(expected) {
|
||||||
@@ -1136,9 +1134,8 @@ fn validate_dependency_boundaries(root: &Path, failures: &mut Vec<String>) -> Re
|
|||||||
let mut manifests = Vec::new();
|
let mut manifests = Vec::new();
|
||||||
collect_cargo_manifests(root, &mut manifests)?;
|
collect_cargo_manifests(root, &mut manifests)?;
|
||||||
for manifest in manifests {
|
for manifest in manifests {
|
||||||
let text = fs::read_to_string(&manifest)
|
let policy = load_cargo_manifest_policy(&manifest)?;
|
||||||
.map_err(|err| format!("{}: {err}", manifest.display()))?;
|
let Some(package) = policy.package_name() else {
|
||||||
let Some(package) = parse_package_name(&text) else {
|
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if is_removed_legacy_adapter_manifest(root, &manifest) {
|
if is_removed_legacy_adapter_manifest(root, &manifest) {
|
||||||
@@ -1148,8 +1145,8 @@ fn validate_dependency_boundaries(root: &Path, failures: &mut Vec<String>) -> Re
|
|||||||
));
|
));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let dependencies = parse_manifest_dependencies(&text);
|
let dependencies = policy.dependency_names();
|
||||||
if !is_adapter_like_package(&package) && !is_app_package(&package) {
|
if !is_adapter_like_package(package) && !is_app_package(package) {
|
||||||
for dependency in &dependencies {
|
for dependency in &dependencies {
|
||||||
if is_forbidden_gui_dependency(dependency) {
|
if is_forbidden_gui_dependency(dependency) {
|
||||||
failures.push(format!(
|
failures.push(format!(
|
||||||
@@ -1159,7 +1156,7 @@ fn validate_dependency_boundaries(root: &Path, failures: &mut Vec<String>) -> Re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if is_app_package(&package) {
|
if is_app_package(package) {
|
||||||
if let Some(forbidden) = first_forbidden_parser_dependency(&dependencies) {
|
if let Some(forbidden) = first_forbidden_parser_dependency(&dependencies) {
|
||||||
failures.push(format!(
|
failures.push(format!(
|
||||||
"{}: app package {package} depends on parser crate {forbidden}",
|
"{}: app package {package} depends on parser crate {forbidden}",
|
||||||
@@ -1328,71 +1325,75 @@ fn collect_cargo_manifests(dir: &Path, out: &mut Vec<PathBuf>) -> Result<(), Str
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_manifest_license(manifest: &str) -> Option<String> {
|
/// Typed Cargo manifest subset used by the architecture policy.
|
||||||
let mut in_package = false;
|
///
|
||||||
let mut in_workspace_package = false;
|
/// Dependency values deliberately remain `toml::Value`: the policy only needs
|
||||||
for line in manifest.lines() {
|
/// keys and Cargo accepts several value forms (version string, inline table,
|
||||||
let trimmed = line.trim();
|
/// or workspace inheritance).
|
||||||
if trimmed.starts_with('[') {
|
#[derive(Debug, Default, Deserialize)]
|
||||||
in_package = trimmed == "[package]";
|
struct CargoManifestPolicy {
|
||||||
in_workspace_package = trimmed == "[workspace.package]";
|
package: Option<CargoPackagePolicy>,
|
||||||
continue;
|
workspace: Option<CargoWorkspacePolicy>,
|
||||||
}
|
dependencies: Option<BTreeMap<String, toml::Value>>,
|
||||||
if (in_package || in_workspace_package) && trimmed.starts_with("license") {
|
#[serde(rename = "dev-dependencies")]
|
||||||
return parse_toml_string_value(trimmed);
|
dev_dependencies: Option<BTreeMap<String, toml::Value>>,
|
||||||
}
|
#[serde(rename = "build-dependencies")]
|
||||||
}
|
build_dependencies: Option<BTreeMap<String, toml::Value>>,
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_package_name(manifest: &str) -> Option<String> {
|
#[derive(Debug, Deserialize)]
|
||||||
let mut in_package = false;
|
struct CargoPackagePolicy {
|
||||||
for line in manifest.lines() {
|
name: String,
|
||||||
let trimmed = line.trim();
|
license: Option<toml::Value>,
|
||||||
if trimmed.starts_with('[') {
|
|
||||||
in_package = trimmed == "[package]";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if in_package && trimmed.starts_with("name") {
|
|
||||||
return parse_toml_string_value(trimmed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_manifest_dependencies(manifest: &str) -> BTreeSet<String> {
|
#[derive(Debug, Deserialize)]
|
||||||
let mut dependencies = BTreeSet::new();
|
struct CargoWorkspacePolicy {
|
||||||
let mut in_dependency_section = false;
|
package: Option<CargoWorkspacePackagePolicy>,
|
||||||
for line in manifest.lines() {
|
|
||||||
let trimmed = line.trim();
|
|
||||||
if trimmed.starts_with('[') {
|
|
||||||
in_dependency_section = matches!(
|
|
||||||
trimmed,
|
|
||||||
"[dependencies]" | "[dev-dependencies]" | "[build-dependencies]"
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if !in_dependency_section || trimmed.is_empty() || trimmed.starts_with('#') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let Some((name, _)) = trimmed.split_once('=') else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let dependency = name.trim().trim_matches('"');
|
|
||||||
if !dependency.is_empty() {
|
|
||||||
dependencies.insert(dependency.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dependencies
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_toml_string_value(line: &str) -> Option<String> {
|
#[derive(Debug, Deserialize)]
|
||||||
let (_, value) = line.split_once('=')?;
|
struct CargoWorkspacePackagePolicy {
|
||||||
let value = value.trim();
|
license: Option<toml::Value>,
|
||||||
if !(value.starts_with('"') && value.ends_with('"')) {
|
}
|
||||||
return None;
|
|
||||||
|
impl CargoManifestPolicy {
|
||||||
|
fn package_name(&self) -> Option<&str> {
|
||||||
|
self.package.as_ref().map(|package| package.name.as_str())
|
||||||
}
|
}
|
||||||
Some(value.trim_matches('"').to_string())
|
|
||||||
|
fn license(&self) -> Option<String> {
|
||||||
|
self.package
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|package| cargo_license_string(package.license.as_ref()))
|
||||||
|
.or_else(|| {
|
||||||
|
self.workspace
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|workspace| workspace.package.as_ref())
|
||||||
|
.and_then(|package| cargo_license_string(package.license.as_ref()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dependency_names(&self) -> BTreeSet<String> {
|
||||||
|
[
|
||||||
|
self.dependencies.as_ref(),
|
||||||
|
self.dev_dependencies.as_ref(),
|
||||||
|
self.build_dependencies.as_ref(),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.flat_map(|dependencies| dependencies.keys().cloned())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cargo_license_string(value: Option<&toml::Value>) -> Option<String> {
|
||||||
|
value.and_then(toml::Value::as_str).map(ToString::to_string)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_cargo_manifest_policy(path: &Path) -> Result<CargoManifestPolicy, String> {
|
||||||
|
let text = fs::read_to_string(path).map_err(|err| format!("{}: {err}", path.display()))?;
|
||||||
|
toml::from_str(&text).map_err(|err| format!("{}: invalid Cargo TOML: {err}", path.display()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_removed_legacy_adapter_manifest(root: &Path, manifest: &Path) -> bool {
|
fn is_removed_legacy_adapter_manifest(root: &Path, manifest: &Path) -> bool {
|
||||||
@@ -3494,16 +3495,30 @@ fparkan-render = { path = "../fparkan-render" }
|
|||||||
fparkan-render-vulkan = { path = "../../adapters/fparkan-render-vulkan" }
|
fparkan-render-vulkan = { path = "../../adapters/fparkan-render-vulkan" }
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
assert_eq!(
|
let policy = toml::from_str::<CargoManifestPolicy>(manifest)
|
||||||
parse_package_name(manifest),
|
.expect("typed policy manifest should parse");
|
||||||
Some("fparkan-example".to_string())
|
assert_eq!(policy.package_name(), Some("fparkan-example"));
|
||||||
);
|
let deps = policy.dependency_names();
|
||||||
let deps = parse_manifest_dependencies(manifest);
|
|
||||||
assert!(deps.contains("fparkan-render"));
|
assert!(deps.contains("fparkan-render"));
|
||||||
assert!(deps.contains("quoted-dep"));
|
assert!(deps.contains("quoted-dep"));
|
||||||
assert!(deps.contains("fparkan-render-vulkan"));
|
assert!(deps.contains("fparkan-render-vulkan"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn typed_manifest_policy_accepts_workspace_inherited_license() {
|
||||||
|
let manifest = r#"
|
||||||
|
[package]
|
||||||
|
name = "fparkan-example"
|
||||||
|
license.workspace = true
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let policy = toml::from_str::<CargoManifestPolicy>(manifest)
|
||||||
|
.expect("workspace inheritance should be valid Cargo TOML");
|
||||||
|
|
||||||
|
assert_eq!(policy.package_name(), Some("fparkan-example"));
|
||||||
|
assert_eq!(policy.license(), None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn workspace_manifest_closure_detects_transitive_platform_bridge() {
|
fn workspace_manifest_closure_detects_transitive_platform_bridge() {
|
||||||
let deps_by_package = [
|
let deps_by_package = [
|
||||||
|
|||||||
Reference in New Issue
Block a user