blob: cfe1e58abee9cddb5d7ea1a75b1980bda6dc5bd2 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
crate::error::Error,
crate::merge::merge_json,
crate::util::{json_or_json5_from_file, write_depfile},
serde_json::Value,
std::{
collections::HashSet,
fs,
io::{BufRead, BufReader, Write},
iter::FromIterator,
path::PathBuf,
},
};
/// Read in the provided JSON file and add includes.
/// If the JSON file is an object with a key "include" that references an array of strings then
/// the strings are treated as paths to JSON files to be merged with the input file.
/// Returns any includes encountered.
/// If a depfile is provided, also writes includes encountered to the depfile.
pub fn merge_includes(
file: &PathBuf,
output: Option<&PathBuf>,
depfile: Option<&PathBuf>,
includepath: &PathBuf,
) -> Result<(), Error> {
let includes = transitive_includes(&file, &includepath)?;
let mut v: Value = json_or_json5_from_file(&file)?;
v.as_object_mut().and_then(|v| v.remove("include"));
for include in &includes {
let path = includepath.join(&include);
let mut includev: Value = json_or_json5_from_file(&path).map_err(|e| {
Error::parse(
format!("Couldn't read include {}: {}", &path.display(), e),
None,
Some(&file),
)
})?;
includev.as_object_mut().and_then(|v| v.remove("include"));
merge_json(&mut v, &includev).map_err(|e| {
Error::parse(format!("Failed to merge with {:?}: {}", path, e), None, Some(&file))
})?;
}
// Write postprocessed JSON
if let Some(output_path) = output.as_ref() {
fs::OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(output_path)?
.write_all(format!("{:#}", v).as_bytes())?;
} else {
println!("{:#}", v);
}
// Write includes to depfile
if let Some(depfile_path) = depfile {
write_depfile(&depfile_path, output, &includes, &includepath)?;
}
Ok(())
}
const CHECK_INCLUDES_URL: &str =
"https://fuchsia.dev/fuchsia-src/development/components/build#component-manifest-includes";
/// Read in the provided JSON file and ensure that it contains all expected includes.
pub fn check_includes(
file: &PathBuf,
mut expected_includes: Vec<String>,
// If specified, this is a path to newline-delimited `expected_includes`
fromfile: Option<&PathBuf>,
includepath: &PathBuf,
) -> Result<(), Error> {
if let Some(path) = fromfile {
let reader = BufReader::new(fs::File::open(path)?);
for line in reader.lines() {
match line {
Ok(value) => expected_includes.push(String::from(value)),
Err(e) => return Err(Error::invalid_args(format!("Invalid --fromfile: {}", e))),
}
}
}
if expected_includes.is_empty() {
return Ok(()); // Nothing to do
}
let actual = transitive_includes(&file, &includepath)?;
for expected in expected_includes {
if !actual.contains(&expected) {
return Err(Error::Validate {
schema_name: None,
err: format!(
"{:?} must include {}.\nSee: {}",
&file, &expected, CHECK_INCLUDES_URL
),
filename: file.to_str().map(String::from),
});
}
}
Ok(())
}
/// Returns all includes of a file relative to `includepath`.
/// Follows transitive includes.
/// Detects cycles.
/// Includes are returned in sorted order.
pub fn transitive_includes(file: &PathBuf, includepath: &PathBuf) -> Result<Vec<String>, Error> {
fn helper(
file: &PathBuf,
includepath: &PathBuf,
doc: &Value,
entered: &mut HashSet<String>,
exited: &mut HashSet<String>,
) -> Result<(), Error> {
if let Some(includes) = doc.get("include").and_then(|v| v.as_array()) {
for include in includes.into_iter().filter_map(|v| v.as_str().map(String::from)) {
// Avoid visiting the same include more than once
if !entered.insert(include.clone()) {
if !exited.contains(&include) {
return Err(Error::parse(
format!("Includes cycle at {}", include),
None,
Some(&file),
));
}
} else {
let path = includepath.join(&include);
let include_doc = json_or_json5_from_file(&path).map_err(|e| {
Error::parse(
format!("Couldn't read include {}: {}", &path.display(), e),
None,
Some(&file),
)
})?;
helper(&file, &includepath, &include_doc, entered, exited)?;
exited.insert(include);
}
}
}
Ok(())
}
let root = json_or_json5_from_file(&file)?;
let mut entered = HashSet::new();
let mut exited = HashSet::new();
helper(&file, &includepath, &root, &mut entered, &mut exited)?;
let mut includes = Vec::from_iter(exited);
includes.sort();
Ok(includes)
}
#[cfg(test)]
mod tests {
use super::*;
use matches::assert_matches;
use serde_json::json;
use std::fmt::Display;
use std::fs::File;
use std::io::{LineWriter, Read};
use tempfile::TempDir;
fn tmp_file(tmp_dir: &TempDir, name: &str, contents: impl Display) -> PathBuf {
let path = tmp_dir.path().join(name);
File::create(tmp_dir.path().join(name))
.unwrap()
.write_all(format!("{:#}", contents).as_bytes())
.unwrap();
return path;
}
fn assert_eq_file(file: PathBuf, contents: impl Display) {
let mut out = String::new();
File::open(file).unwrap().read_to_string(&mut out).unwrap();
assert_eq!(out, format!("{:#}", contents));
}
#[test]
fn test_include_cmx() {
let tmp_dir = TempDir::new().unwrap();
let include_path = tmp_dir.path().to_path_buf();
let cmx_path = tmp_file(
&tmp_dir,
"some.cmx",
json!({
"include": ["shard.cmx"],
"program": {
"binary": "bin/hello_world"
}
}),
);
tmp_file(
&tmp_dir,
"shard.cmx",
json!({
"sandbox": {
"services": ["fuchsia.foo.Bar"]
}
}),
);
let out_cmx_path = tmp_dir.path().join("out.cmx");
let cmx_depfile_path = tmp_dir.path().join("cmx.d");
merge_includes(&cmx_path, Some(&out_cmx_path), Some(&cmx_depfile_path), &include_path)
.unwrap();
assert_eq_file(
out_cmx_path,
json!({
"program": {
"binary": "bin/hello_world"
},
"sandbox": {
"services": ["fuchsia.foo.Bar"]
}
}),
);
let mut deps = String::new();
File::open(&cmx_depfile_path).unwrap().read_to_string(&mut deps).unwrap();
assert_eq!(
deps,
format!("{tmp}/out.cmx: {tmp}/shard.cmx\n", tmp = tmp_dir.path().display())
);
}
#[test]
fn test_include_cml() {
let tmp_dir = TempDir::new().unwrap();
let include_path = tmp_dir.path().to_path_buf();
let cml_path = tmp_file(
&tmp_dir,
"some.cml",
"{include: [\"shard.cml\"], program: {binary: \"bin/hello_world\"}}",
);
tmp_file(&tmp_dir, "shard.cml", "{use: [{ protocol: [\"fuchsia.foo.Bar\"]}]}");
let out_cml_path = tmp_dir.path().join("out.cml");
let cml_depfile_path = tmp_dir.path().join("cml.d");
merge_includes(&cml_path, Some(&out_cml_path), Some(&cml_depfile_path), &include_path)
.unwrap();
assert_eq_file(
out_cml_path,
json!({
"program": {
"binary": "bin/hello_world"
},
"use": [{
"protocol": ["fuchsia.foo.Bar"]
}]
}),
);
let mut deps = String::new();
File::open(&cml_depfile_path).unwrap().read_to_string(&mut deps).unwrap();
assert_eq!(
deps,
format!("{tmp}/out.cml: {tmp}/shard.cml\n", tmp = tmp_dir.path().display())
);
}
#[test]
fn test_include_multiple_shards() {
let tmp_dir = TempDir::new().unwrap();
let include_path = tmp_dir.path().to_path_buf();
let cmx_path = tmp_file(
&tmp_dir,
"some.cmx",
json!({
"include": ["shard1.cmx", "shard2.cmx"],
"program": {
"binary": "bin/hello_world"
}
}),
);
tmp_file(
&tmp_dir,
"shard1.cmx",
json!({
"sandbox": {
"services": ["fuchsia.foo.Bar"]
}
}),
);
tmp_file(
&tmp_dir,
"shard2.cmx",
json!({
"sandbox": {
"services": ["fuchsia.foo.Qux"]
}
}),
);
let out_cmx_path = tmp_dir.path().join("out.cmx");
let cmx_depfile_path = tmp_dir.path().join("cmx.d");
merge_includes(&cmx_path, Some(&out_cmx_path), Some(&cmx_depfile_path), &include_path)
.unwrap();
assert_eq_file(
out_cmx_path,
json!({
"program": {
"binary": "bin/hello_world"
},
"sandbox": {
"services": ["fuchsia.foo.Bar", "fuchsia.foo.Qux"]
}
}),
);
let mut deps = String::new();
File::open(&cmx_depfile_path).unwrap().read_to_string(&mut deps).unwrap();
assert_eq!(
deps,
format!(
"{tmp}/out.cmx: {tmp}/shard1.cmx {tmp}/shard2.cmx\n",
tmp = tmp_dir.path().display()
)
);
}
#[test]
fn test_include_recursively() {
let tmp_dir = TempDir::new().unwrap();
let include_path = tmp_dir.path().to_path_buf();
let cmx_path = tmp_file(
&tmp_dir,
"some.cmx",
json!({
"include": ["shard1.cmx"],
"program": {
"binary": "bin/hello_world"
}
}),
);
tmp_file(
&tmp_dir,
"shard1.cmx",
json!({
"include": ["shard2.cmx"],
"sandbox": {
"services": ["fuchsia.foo.Bar"]
}
}),
);
tmp_file(
&tmp_dir,
"shard2.cmx",
json!({
"sandbox": {
"services": ["fuchsia.foo.Qux"]
}
}),
);
let out_cmx_path = tmp_dir.path().join("out.cmx");
let cmx_depfile_path = tmp_dir.path().join("cmx.d");
merge_includes(&cmx_path, Some(&out_cmx_path), Some(&cmx_depfile_path), &include_path)
.unwrap();
assert_eq_file(
out_cmx_path,
json!({
"program": {
"binary": "bin/hello_world"
},
"sandbox": {
"services": ["fuchsia.foo.Bar", "fuchsia.foo.Qux"]
}
}),
);
let mut deps = String::new();
File::open(&cmx_depfile_path).unwrap().read_to_string(&mut deps).unwrap();
assert_eq!(
deps,
format!(
"{tmp}/out.cmx: {tmp}/shard1.cmx {tmp}/shard2.cmx\n",
tmp = tmp_dir.path().display()
)
);
}
#[test]
fn test_include_nothing() {
let tmp_dir = TempDir::new().unwrap();
let include_path = tmp_dir.path().to_path_buf();
let cmx_path = tmp_file(
&tmp_dir,
"some.cmx",
json!({
"include": [],
"program": {
"binary": "bin/hello_world"
}
}),
);
let out_cmx_path = tmp_dir.path().join("out.cmx");
let cmx_depfile_path = tmp_dir.path().join("cmx.d");
merge_includes(&cmx_path, Some(&out_cmx_path), Some(&cmx_depfile_path), &include_path)
.unwrap();
assert_eq_file(
out_cmx_path,
json!({
"program": {
"binary": "bin/hello_world"
}
}),
);
assert_eq!(cmx_depfile_path.exists(), false);
}
#[test]
fn test_no_includes() {
let tmp_dir = TempDir::new().unwrap();
let include_path = tmp_dir.path().to_path_buf();
let cmx_path = tmp_file(
&tmp_dir,
"some.cmx",
json!({
"program": {
"binary": "bin/hello_world"
}
}),
);
let out_cmx_path = tmp_dir.path().join("out.cmx");
let cmx_depfile_path = tmp_dir.path().join("cmx.d");
merge_includes(&cmx_path, Some(&out_cmx_path), Some(&cmx_depfile_path), &include_path)
.unwrap();
assert_eq_file(
out_cmx_path,
json!({
"program": {
"binary": "bin/hello_world"
}
}),
);
assert_eq!(cmx_depfile_path.exists(), false);
}
#[test]
fn test_invalid_include() {
let tmp_dir = TempDir::new().unwrap();
let include_path = tmp_dir.path().to_path_buf();
let cmx_path = tmp_file(
&tmp_dir,
"some.cmx",
json!({
"include": ["doesnt_exist.cmx"],
"program": {
"binary": "bin/hello_world"
}
}),
);
let out_cmx_path = tmp_dir.path().join("out.cmx");
let cmx_depfile_path = tmp_dir.path().join("cmx.d");
let result =
merge_includes(&cmx_path, Some(&out_cmx_path), Some(&cmx_depfile_path), &include_path);
assert_matches!(result, Err(Error::Parse { err, .. })
if err.starts_with("Couldn't read include ") && err.contains("doesnt_exist.cmx"));
}
#[test]
fn test_include_detect_cycle() {
let tmp_dir = TempDir::new().unwrap();
let include_path = tmp_dir.path().to_path_buf();
let cmx_path = tmp_file(
&tmp_dir,
"some1.cmx",
json!({
"include": ["some2.cmx"],
}),
);
tmp_file(
&tmp_dir,
"some2.cmx",
json!({
"include": ["some1.cmx"],
}),
);
let out_cmx_path = tmp_dir.path().join("out.cmx");
let result = merge_includes(&cmx_path, Some(&out_cmx_path), None, &include_path);
assert_matches!(result, Err(Error::Parse { err, .. }) if err.contains("Includes cycle"));
}
#[test]
fn test_include_a_diamond_is_not_a_cycle() {
// A
// / \
// B C
// \ /
// D
// The above is fine.
let tmp_dir = TempDir::new().unwrap();
let include_path = tmp_dir.path().to_path_buf();
let a_path = tmp_file(
&tmp_dir,
"a.cmx",
json!({
"include": ["b.cmx", "c.cmx"],
}),
);
tmp_file(
&tmp_dir,
"b.cmx",
json!({
"include": ["d.cmx"],
}),
);
tmp_file(
&tmp_dir,
"c.cmx",
json!({
"include": ["d.cmx"],
}),
);
tmp_file(&tmp_dir, "d.cmx", json!({}));
let out_cmx_path = tmp_dir.path().join("out.cmx");
let result = merge_includes(&a_path, Some(&out_cmx_path), None, &include_path);
assert_matches!(result, Ok(()));
}
#[test]
fn test_expect_nothing() {
let tmp_dir = TempDir::new().unwrap();
let include_path = tmp_dir.path().to_path_buf();
let cmx1_path = tmp_file(
&tmp_dir,
"some1.cmx",
json!({
"program": {
"binary": "bin/hello_world"
}
}),
);
assert_matches!(check_includes(&cmx1_path, vec![], None, &include_path), Ok(()));
let cmx2_path = tmp_file(
&tmp_dir,
"some2.cmx",
json!({
"include": [],
"program": {
"binary": "bin/hello_world"
}
}),
);
assert_matches!(check_includes(&cmx2_path, vec![], None, &include_path), Ok(()));
let cmx3_path = tmp_file(
&tmp_dir,
"some3.cmx",
json!({
"include": [ "foo.cmx" ],
"program": {
"binary": "bin/hello_world"
}
}),
);
assert_matches!(check_includes(&cmx3_path, vec![], None, &include_path), Ok(()));
}
#[test]
fn test_expect_something_present() {
let tmp_dir = TempDir::new().unwrap();
let include_path = tmp_dir.path().to_path_buf();
let cmx_path = tmp_file(
&tmp_dir,
"some.cmx",
json!({
"include": [ "foo.cmx", "bar.cmx" ],
"program": {
"binary": "bin/hello_world"
}
}),
);
tmp_file(&tmp_dir, "foo.cmx", json!({}));
tmp_file(&tmp_dir, "bar.cmx", json!({}));
assert_matches!(
check_includes(&cmx_path, vec!["bar.cmx".into()], None, &include_path),
Ok(())
);
}
#[test]
fn test_expect_something_missing() {
let tmp_dir = TempDir::new().unwrap();
let include_path = tmp_dir.path().to_path_buf();
let cmx1_path = tmp_file(
&tmp_dir,
"some1.cmx",
json!({
"include": [ "foo.cmx", "bar.cmx" ],
"program": {
"binary": "bin/hello_world"
}
}),
);
tmp_file(&tmp_dir, "foo.cmx", json!({}));
tmp_file(&tmp_dir, "bar.cmx", json!({}));
assert_matches!(check_includes(&cmx1_path, vec!["qux.cmx".into()], None, &include_path),
Err(Error::Validate { filename, .. }) if filename == cmx1_path.to_str().map(String::from));
let cmx2_path = tmp_file(
&tmp_dir,
"some2.cmx",
json!({
// No includes
"program": {
"binary": "bin/hello_world"
}
}),
);
assert_matches!(check_includes(&cmx2_path, vec!["qux.cmx".into()], None, &include_path),
Err(Error::Validate { filename, .. }) if filename == cmx2_path.to_str().map(String::from));
}
#[test]
fn test_expect_something_transitive() {
let tmp_dir = TempDir::new().unwrap();
let include_path = tmp_dir.path().to_path_buf();
let cmx_path = tmp_file(
&tmp_dir,
"some.cmx",
json!({
"include": [ "foo.cmx" ],
"program": {
"binary": "bin/hello_world"
}
}),
);
tmp_file(&tmp_dir, "foo.cmx", json!({"include": [ "bar.cmx" ]}));
tmp_file(&tmp_dir, "bar.cmx", json!({}));
assert_matches!(
check_includes(&cmx_path, vec!["bar.cmx".into()], None, &include_path),
Ok(())
);
}
#[test]
fn test_expect_fromfile() {
let tmp_dir = TempDir::new().unwrap();
let include_path = tmp_dir.path().to_path_buf();
let cmx_path = tmp_file(
&tmp_dir,
"some.cmx",
json!({
"include": [ "foo.cmx", "bar.cmx" ],
"program": {
"binary": "bin/hello_world"
}
}),
);
tmp_file(&tmp_dir, "foo.cmx", json!({}));
tmp_file(&tmp_dir, "bar.cmx", json!({}));
let fromfile_path = tmp_dir.path().join("fromfile");
let mut fromfile = LineWriter::new(File::create(fromfile_path.clone()).unwrap());
writeln!(fromfile, "foo.cmx").unwrap();
writeln!(fromfile, "bar.cmx").unwrap();
assert_matches!(
check_includes(&cmx_path, vec![], Some(&fromfile_path), &include_path),
Ok(())
);
// Add another include that's missing
writeln!(fromfile, "qux.cmx").unwrap();
assert_matches!(check_includes(&cmx_path, vec![], Some(&fromfile_path), &include_path),
Err(Error::Validate { filename, .. }) if filename == cmx_path.to_str().map(String::from));
}
}