|  | // Copyright 2019 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 std::collections::HashSet; | 
|  | use std::hash::Hash; | 
|  | use std::iter::{FromIterator, Iterator}; | 
|  |  | 
|  | use structopt::StructOpt; | 
|  |  | 
|  | use sdk_metadata::{ | 
|  | BanjoLibrary, CcPrebuiltLibrary, CcSourceLibrary, DartLibrary, Data, DeviceProfile, | 
|  | Documentation, ElementType, FidlLibrary, HostTool, JsonObject, LoadableModule, Manifest, Part, | 
|  | Sysroot, | 
|  | }; | 
|  |  | 
|  | mod app; | 
|  | mod file_provider; | 
|  | mod flags; | 
|  | #[macro_use] | 
|  | mod immutable; | 
|  | mod merge_banjo_library; | 
|  | mod merge_cc_prebuilt_library; | 
|  | mod merge_cc_source_library; | 
|  | mod merge_dart_library; | 
|  | mod merge_data; | 
|  | mod merge_device_profile; | 
|  | mod merge_documentation; | 
|  | mod merge_fidl_library; | 
|  | mod merge_host_tool; | 
|  | mod merge_loadable_module; | 
|  | mod merge_sysroot; | 
|  | mod tarball; | 
|  | #[cfg(test)] | 
|  | mod testing; | 
|  |  | 
|  | use crate::app::{Error, Result}; | 
|  | use crate::file_provider::FileProvider; | 
|  | use crate::merge_banjo_library::merge_banjo_library; | 
|  | use crate::merge_cc_prebuilt_library::merge_cc_prebuilt_library; | 
|  | use crate::merge_cc_source_library::merge_cc_source_library; | 
|  | use crate::merge_dart_library::merge_dart_library; | 
|  | use crate::merge_data::merge_data; | 
|  | use crate::merge_device_profile::merge_device_profile; | 
|  | use crate::merge_documentation::merge_documentation; | 
|  | use crate::merge_fidl_library::merge_fidl_library; | 
|  | use crate::merge_host_tool::merge_host_tool; | 
|  | use crate::merge_loadable_module::merge_loadable_module; | 
|  | use crate::merge_sysroot::merge_sysroot; | 
|  | use crate::tarball::{InputTarball, OutputTarball, ResultTarball, SourceTarball, TarballContent}; | 
|  |  | 
|  | const MANIFEST_PATH: &str = "meta/manifest.json"; | 
|  |  | 
|  | /// Merges two given lists, removing duplicates and sorting the resulting list. | 
|  | fn merge_lists<T>(one: &[T], two: &[T]) -> Vec<T> | 
|  | where | 
|  | T: Ord + Clone + Eq + Hash, | 
|  | { | 
|  | let mut joined: Vec<T> = one.to_vec().clone(); | 
|  | joined.extend(two.iter().cloned()); | 
|  | joined.sort_unstable(); | 
|  | joined.dedup(); | 
|  | joined | 
|  | } | 
|  |  | 
|  | fn merge_manifests(base: &Manifest, complement: &Manifest) -> Result<Manifest> { | 
|  | let mut result = Manifest::default(); | 
|  |  | 
|  | // Host architecture. | 
|  | let has_host_content = |manifest: &Manifest| -> bool { | 
|  | manifest.parts.iter().any(|part: &Part| part.kind == ElementType::HostTool) | 
|  | }; | 
|  | let mut host_archs = HashSet::new(); | 
|  | if has_host_content(&base) { | 
|  | host_archs.insert(base.arch.host.clone()); | 
|  | } | 
|  | if has_host_content(&complement) { | 
|  | host_archs.insert(complement.arch.host.clone()); | 
|  | } | 
|  | if host_archs.is_empty() { | 
|  | // The archives do not have any host content. The architecture is not meaningful in that | 
|  | // case but is still needed: just pick one. | 
|  | result.arch.host = base.arch.host.clone(); | 
|  | } else if host_archs.len() == 1 { | 
|  | result.arch.host = host_archs.iter().next().expect("Should have 1 host arch").clone(); | 
|  | } else { | 
|  | let error = format!("Host architecture mismatch: {:?}", host_archs.iter()); | 
|  | return Err(Error::CannotMerge { error })?; | 
|  | } | 
|  |  | 
|  | // Target architecture. | 
|  | result.arch.target = merge_lists(&base.arch.target, &complement.arch.target); | 
|  |  | 
|  | // Id. | 
|  | if base.id == complement.id { | 
|  | result.id = base.id.clone(); | 
|  | } else { | 
|  | if base.id.is_empty() { | 
|  | result.id = complement.id.clone() | 
|  | } else if complement.id.is_empty() { | 
|  | result.id = base.id.clone(); | 
|  | } else { | 
|  | let error = format!("Id mismatch: {} vs. {}", &base.id, &complement.id); | 
|  | return Err(Error::CannotMerge { error })?; | 
|  | } | 
|  | } | 
|  |  | 
|  | // Parts. | 
|  | result.parts = merge_lists(&base.parts, &complement.parts); | 
|  |  | 
|  | // Schema version. | 
|  | if base.schema_version != complement.schema_version { | 
|  | let error = format!( | 
|  | "Schema version mismatch: {} vs. {}", | 
|  | &base.schema_version, &complement.schema_version | 
|  | ); | 
|  | return Err(Error::CannotMerge { error })?; | 
|  | } | 
|  | result.schema_version = base.schema_version.clone(); | 
|  |  | 
|  | result.validate()?; | 
|  | Ok(result) | 
|  | } | 
|  |  | 
|  | fn merge_common_part<F: TarballContent>( | 
|  | part: &Part, | 
|  | base: &impl InputTarball<F>, | 
|  | complement: &impl InputTarball<F>, | 
|  | output: &mut impl OutputTarball<F>, | 
|  | ) -> Result<()> { | 
|  | match part.kind { | 
|  | ElementType::BanjoLibrary => merge_banjo_library(&part.meta, base, complement, output), | 
|  | ElementType::CcPrebuiltLibrary => { | 
|  | merge_cc_prebuilt_library(&part.meta, base, complement, output) | 
|  | } | 
|  | ElementType::CcSourceLibrary => { | 
|  | merge_cc_source_library(&part.meta, base, complement, output) | 
|  | } | 
|  | ElementType::Config => merge_data(&part.meta, base, complement, output), | 
|  | ElementType::DartLibrary => merge_dart_library(&part.meta, base, complement, output), | 
|  | ElementType::DeviceProfile => merge_device_profile(&part.meta, base, complement, output), | 
|  | ElementType::Documentation => merge_documentation(&part.meta, base, complement, output), | 
|  | ElementType::FidlLibrary => merge_fidl_library(&part.meta, base, complement, output), | 
|  | ElementType::HostTool => merge_host_tool(&part.meta, base, complement, output), | 
|  | ElementType::License => merge_data(&part.meta, base, complement, output), | 
|  | ElementType::LoadableModule => merge_loadable_module(&part.meta, base, complement, output), | 
|  | ElementType::Sysroot => merge_sysroot(&part.meta, base, complement, output), | 
|  | } | 
|  | } | 
|  |  | 
|  | fn copy_part_as_is<F: TarballContent>( | 
|  | part: &Part, | 
|  | source: &impl InputTarball<F>, | 
|  | output: &mut impl OutputTarball<F>, | 
|  | ) -> Result<()> { | 
|  | let provider: Box<dyn FileProvider> = match part.kind { | 
|  | ElementType::BanjoLibrary => Box::new(source.get_metadata::<BanjoLibrary>(&part.meta)?), | 
|  | ElementType::CcPrebuiltLibrary => { | 
|  | Box::new(source.get_metadata::<CcPrebuiltLibrary>(&part.meta)?) | 
|  | } | 
|  | ElementType::CcSourceLibrary => { | 
|  | Box::new(source.get_metadata::<CcSourceLibrary>(&part.meta)?) | 
|  | } | 
|  | ElementType::Config => Box::new(source.get_metadata::<Data>(&part.meta)?), | 
|  | ElementType::DartLibrary => Box::new(source.get_metadata::<DartLibrary>(&part.meta)?), | 
|  | ElementType::DeviceProfile => Box::new(source.get_metadata::<DeviceProfile>(&part.meta)?), | 
|  | ElementType::Documentation => Box::new(source.get_metadata::<Documentation>(&part.meta)?), | 
|  | ElementType::FidlLibrary => Box::new(source.get_metadata::<FidlLibrary>(&part.meta)?), | 
|  | ElementType::HostTool => Box::new(source.get_metadata::<HostTool>(&part.meta)?), | 
|  | ElementType::License => Box::new(source.get_metadata::<Data>(&part.meta)?), | 
|  | ElementType::LoadableModule => Box::new(source.get_metadata::<LoadableModule>(&part.meta)?), | 
|  | ElementType::Sysroot => Box::new(source.get_metadata::<Sysroot>(&part.meta)?), | 
|  | }; | 
|  | let mut paths = provider.get_all_files(); | 
|  | paths.push(part.meta.clone()); | 
|  | for path in &paths { | 
|  | source.get_file(path, |file| output.write_file(path, file))?; | 
|  | } | 
|  |  | 
|  | Ok(()) | 
|  | } | 
|  |  | 
|  | fn main() -> Result<()> { | 
|  | let flags = flags::Flags::from_args(); | 
|  |  | 
|  | let base = SourceTarball::new(&flags.base)?; | 
|  | let complement = SourceTarball::new(&flags.complement)?; | 
|  | let mut output = ResultTarball::new(&flags.output)?; | 
|  |  | 
|  | let base_manifest: Manifest = base.get_metadata(MANIFEST_PATH)?; | 
|  | let complement_manifest: Manifest = complement.get_metadata(MANIFEST_PATH)?; | 
|  |  | 
|  | let base_parts: HashSet<Part> = HashSet::from_iter(base_manifest.parts.iter().cloned()); | 
|  | let complement_parts: HashSet<Part> = | 
|  | HashSet::from_iter(complement_manifest.parts.iter().cloned()); | 
|  |  | 
|  | for part in base_parts.intersection(&complement_parts) { | 
|  | merge_common_part(&part, &base, &complement, &mut output)?; | 
|  | } | 
|  |  | 
|  | for part in base_parts.difference(&complement_parts) { | 
|  | copy_part_as_is(&part, &base, &mut output)?; | 
|  | } | 
|  |  | 
|  | for part in complement_parts.difference(&base_parts) { | 
|  | copy_part_as_is(&part, &complement, &mut output)?; | 
|  | } | 
|  |  | 
|  | let merged_manifest = merge_manifests(&base_manifest, &complement_manifest)?; | 
|  |  | 
|  | output.write_json(MANIFEST_PATH, &merged_manifest)?; | 
|  | output.export()?; | 
|  |  | 
|  | Ok(()) | 
|  | } | 
|  |  | 
|  | #[cfg(test)] | 
|  | mod tests { | 
|  | use serde_json::value::Value; | 
|  | use serde_json::{from_value, json}; | 
|  |  | 
|  | use sdk_metadata::Manifest; | 
|  |  | 
|  | use super::*; | 
|  |  | 
|  | type Verifier = dyn FnOnce(&Manifest) -> bool; | 
|  |  | 
|  | macro_rules! test_merge { | 
|  | ( | 
|  | name = $name:ident, | 
|  | base = $base:expr, | 
|  | complement = $complement:expr, | 
|  | success = $success:expr, | 
|  | ) => { | 
|  | #[test] | 
|  | fn $name() { | 
|  | merge_test($base, $complement, $success, None); | 
|  | } | 
|  | }; | 
|  | ( | 
|  | name = $name:ident, | 
|  | base = $base:expr, | 
|  | complement = $complement:expr, | 
|  | success = $success:expr, | 
|  | verifier = $verifier:expr, | 
|  | ) => { | 
|  | #[test] | 
|  | fn $name() { | 
|  | merge_test($base, $complement, $success, Some(Box::new($verifier))); | 
|  | } | 
|  | }; | 
|  | } | 
|  |  | 
|  | fn merge_test(base: Value, complement: Value, success: bool, verifier: Option<Box<Verifier>>) { | 
|  | let base_manifest: Manifest = from_value(base).unwrap(); | 
|  | base_manifest.validate().unwrap(); | 
|  | let complement_manifest: Manifest = from_value(complement).unwrap(); | 
|  | complement_manifest.validate().unwrap(); | 
|  | let merged_manifest = merge_manifests(&base_manifest, &complement_manifest); | 
|  | assert_eq!(merged_manifest.is_ok(), success); | 
|  | if success { | 
|  | if let Some(verify) = verifier { | 
|  | assert!(verify(&merged_manifest.unwrap())); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | test_merge!( | 
|  | name = test_clean_merge, | 
|  | base = json!({ | 
|  | "arch": { "host": "x86_64-linux-gnu", "target": ["x64"] }, | 
|  | "parts": [ | 
|  | { | 
|  | "meta": "foo/bar.json", | 
|  | "type": "dart_library", | 
|  | }, | 
|  | { | 
|  | "meta": "alpha/beta.json", | 
|  | "type": "host_tool", | 
|  | }, | 
|  | ], | 
|  | "id": "bleh", | 
|  | "schema_version": "1", | 
|  | }), | 
|  | complement = json!({ | 
|  | "arch": { "host": "x86_64-linux-gnu", "target": ["arm64"] }, | 
|  | "parts": [ | 
|  | { | 
|  | "meta": "ping/pong.json", | 
|  | "type": "cc_prebuilt_library", | 
|  | }, | 
|  | { | 
|  | "meta": "one/two.json", | 
|  | "type": "host_tool", | 
|  | }, | 
|  | ], | 
|  | "id": "bleh", | 
|  | "schema_version": "1", | 
|  | }), | 
|  | success = true, | 
|  | verifier = |manifest: &Manifest| { | 
|  | (manifest.arch.host == "x86_64-linux-gnu") | 
|  | & (manifest.arch.target.len() == 2) | 
|  | & (manifest.id == "bleh") | 
|  | & (manifest.schema_version == "1") | 
|  | & (manifest.parts.len() == 4) | 
|  | }, | 
|  | ); | 
|  |  | 
|  | test_merge!( | 
|  | name = test_different_schema_versions, | 
|  | base = json!({ | 
|  | "arch": { "host": "x86_64-linux-gnu", "target": ["x64"] }, | 
|  | "parts": [ | 
|  | { | 
|  | "meta": "pkg/foo/meta.json", | 
|  | "type": "cc_source_library" | 
|  | } | 
|  | ], | 
|  | "id": "bleh", | 
|  | "schema_version": "1", | 
|  | }), | 
|  | complement = json!({ | 
|  | "arch": { "host": "x86_64-linux-gnu", "target": ["x64"] }, | 
|  | "parts": [ | 
|  | { | 
|  | "meta": "pkg/foo/meta.json", | 
|  | "type": "cc_source_library" | 
|  | } | 
|  | ], | 
|  | "id": "bleh", | 
|  | "schema_version": "2", | 
|  | }), | 
|  | success = false, | 
|  | ); | 
|  |  | 
|  | test_merge!( | 
|  | name = test_different_ids, | 
|  | base = json!({ | 
|  | "arch": { "host": "x86_64-linux-gnu", "target": ["x64"] }, | 
|  | "parts": [ | 
|  | { | 
|  | "meta": "pkg/foo/meta.json", | 
|  | "type": "cc_source_library" | 
|  | } | 
|  | ], | 
|  | "id": "whoops", | 
|  | "schema_version": "1", | 
|  | }), | 
|  | complement = json!({ | 
|  | "arch": { "host": "x86_64-linux-gnu", "target": ["x64"] }, | 
|  | "parts": [ | 
|  | { | 
|  | "meta": "pkg/foo/meta.json", | 
|  | "type": "cc_source_library" | 
|  | } | 
|  | ], | 
|  | "id": "bleh", | 
|  | "schema_version": "1", | 
|  | }), | 
|  | success = false, | 
|  | ); | 
|  |  | 
|  | test_merge!( | 
|  | name = test_empty_id, | 
|  | base = json!({ | 
|  | "arch": { "host": "x86_64-linux-gnu", "target": ["x64"] }, | 
|  | "parts": [ | 
|  | { | 
|  | "meta": "pkg/foo/meta.json", | 
|  | "type": "cc_source_library" | 
|  | } | 
|  | ], | 
|  | "id": "whoops", | 
|  | "schema_version": "1", | 
|  | }), | 
|  | complement = json!({ | 
|  | "arch": { "host": "x86_64-linux-gnu", "target": ["x64"] }, | 
|  | "parts": [ | 
|  | { | 
|  | "meta": "pkg/foo/meta.json", | 
|  | "type": "cc_source_library" | 
|  | } | 
|  | ], | 
|  | "id": "", | 
|  | "schema_version": "1", | 
|  | }), | 
|  | success = true, | 
|  | verifier = |manifest: &Manifest| { manifest.id == "whoops" }, | 
|  | ); | 
|  |  | 
|  | test_merge!( | 
|  | name = test_two_empty_ids, | 
|  | base = json!({ | 
|  | "arch": { "host": "x86_64-linux-gnu", "target": ["x64"] }, | 
|  | "parts": [ | 
|  | { | 
|  | "meta": "pkg/foo/meta.json", | 
|  | "type": "cc_source_library" | 
|  | } | 
|  | ], | 
|  | "id": "", | 
|  | "schema_version": "1", | 
|  | }), | 
|  | complement = json!({ | 
|  | "arch": { "host": "x86_64-linux-gnu", "target": ["x64"] }, | 
|  | "parts": [ | 
|  | { | 
|  | "meta": "pkg/foo/meta.json", | 
|  | "type": "cc_source_library" | 
|  | } | 
|  | ], | 
|  | "id": "", | 
|  | "schema_version": "1", | 
|  | }), | 
|  | success = true, | 
|  | verifier = |manifest: &Manifest| { manifest.id == "" }, | 
|  | ); | 
|  |  | 
|  | test_merge!( | 
|  | name = test_different_host_architectures, | 
|  | base = json!({ | 
|  | "arch": { "host": "arm64-linux-gnu", "target": ["x64"] }, | 
|  | "parts": [ | 
|  | { | 
|  | "meta": "foo/bar.json", | 
|  | "type": "host_tool", | 
|  | }, | 
|  | ], | 
|  | "id": "bleh", | 
|  | "schema_version": "1", | 
|  | }), | 
|  | complement = json!({ | 
|  | "arch": { "host": "x86_64-linux-gnu", "target": ["x64"] }, | 
|  | "parts": [ | 
|  | { | 
|  | "meta": "ping/pong.json", | 
|  | "type": "host_tool", | 
|  | }, | 
|  | ], | 
|  | "id": "bleh", | 
|  | "schema_version": "1", | 
|  | }), | 
|  | success = false, | 
|  | ); | 
|  |  | 
|  | test_merge!( | 
|  | name = test_different_host_architectures_one_with_host_content, | 
|  | base = json!({ | 
|  | "arch": { "host": "arm64-linux-gnu", "target": ["x64"] }, | 
|  | "parts": [ | 
|  | { | 
|  | "meta": "ping/pong.json", | 
|  | "type": "dart_library", | 
|  | }, | 
|  | ], | 
|  | "id": "bleh", | 
|  | "schema_version": "1", | 
|  | }), | 
|  | complement = json!({ | 
|  | "arch": { "host": "x86_64-linux-gnu", "target": ["x64"] }, | 
|  | "parts": [ | 
|  | { | 
|  | "meta": "foo/bar.json", | 
|  | "type": "host_tool", | 
|  | }, | 
|  | ], | 
|  | "id": "bleh", | 
|  | "schema_version": "1", | 
|  | }), | 
|  | success = true, | 
|  | verifier = |manifest: &Manifest| { manifest.arch.host == "x86_64-linux-gnu" }, | 
|  | ); | 
|  |  | 
|  | test_merge!( | 
|  | name = test_different_host_architectures_none_with_host_content, | 
|  | base = json!({ | 
|  | "arch": { "host": "arm64-linux-gnu", "target": ["x64"] }, | 
|  | "parts": [ | 
|  | { | 
|  | "meta": "foo/bar.json", | 
|  | "type": "cc_prebuilt_library", | 
|  | }, | 
|  | ], | 
|  | "id": "bleh", | 
|  | "schema_version": "1", | 
|  | }), | 
|  | complement = json!({ | 
|  | "arch": { "host": "x86_64-linux-gnu", "target": ["x64"] }, | 
|  | "parts": [ | 
|  | { | 
|  | "meta": "ping/pong.json", | 
|  | "type": "dart_library", | 
|  | }, | 
|  | ], | 
|  | "id": "bleh", | 
|  | "schema_version": "1", | 
|  | }), | 
|  | success = true, | 
|  | verifier = |manifest: &Manifest| { manifest.arch.host == "arm64-linux-gnu" }, | 
|  | ); | 
|  |  | 
|  | test_merge!( | 
|  | name = test_parts, | 
|  | base = json!({ | 
|  | "arch": { "host": "x86_64-linux-gnu", "target": ["x64"] }, | 
|  | "parts": [ | 
|  | { | 
|  | "meta": "foo/bar.json", | 
|  | "type": "cc_prebuilt_library", | 
|  | }, | 
|  | { | 
|  | "meta": "ping/pong.json", | 
|  | "type": "dart_library", | 
|  | }, | 
|  | ], | 
|  | "id": "bleh", | 
|  | "schema_version": "1", | 
|  | }), | 
|  | complement = json!({ | 
|  | "arch": { "host": "x86_64-linux-gnu", "target": ["x64"] }, | 
|  | "parts": [ | 
|  | { | 
|  | "meta": "ping/pong.json", | 
|  | "type": "dart_library", | 
|  | }, | 
|  | { | 
|  | "meta": "one/two.json", | 
|  | "type": "cc_source_library", | 
|  | }, | 
|  | ], | 
|  | "id": "bleh", | 
|  | "schema_version": "1", | 
|  | }), | 
|  | success = true, | 
|  | verifier = |manifest: &Manifest| { manifest.parts.len() == 3 }, | 
|  | ); | 
|  |  | 
|  | test_merge!( | 
|  | name = test_same_target_architecture, | 
|  | base = json!({ | 
|  | "arch": { "host": "x86_64-linux-gnu", "target": ["x64"] }, | 
|  | "parts": [ | 
|  | { | 
|  | "meta": "pkg/foo/meta.json", | 
|  | "type": "cc_source_library" | 
|  | } | 
|  | ], | 
|  | "id": "whoops", | 
|  | "schema_version": "1", | 
|  | }), | 
|  | complement = json!({ | 
|  | "arch": { "host": "x86_64-linux-gnu", "target": ["x64"] }, | 
|  | "parts": [ | 
|  | { | 
|  | "meta": "pkg/foo/meta.json", | 
|  | "type": "cc_source_library" | 
|  | } | 
|  | ], | 
|  | "id": "whoops", | 
|  | "schema_version": "1", | 
|  | }), | 
|  | success = true, | 
|  | verifier = |manifest: &Manifest| { manifest.arch.target.len() == 1 }, | 
|  | ); | 
|  |  | 
|  | test_merge!( | 
|  | name = test_different_target_architectures, | 
|  | base = json!({ | 
|  | "arch": { "host": "x86_64-linux-gnu", "target": ["arm64"] }, | 
|  | "parts": [ | 
|  | { | 
|  | "meta": "pkg/foo/meta.json", | 
|  | "type": "cc_source_library" | 
|  | } | 
|  | ], | 
|  | "id": "whoops", | 
|  | "schema_version": "1", | 
|  | }), | 
|  | complement = json!({ | 
|  | "arch": { "host": "x86_64-linux-gnu", "target": ["x64"] }, | 
|  | "parts": [ | 
|  | { | 
|  | "meta": "pkg/foo/meta.json", | 
|  | "type": "cc_source_library" | 
|  | } | 
|  | ], | 
|  | "id": "whoops", | 
|  | "schema_version": "1", | 
|  | }), | 
|  | success = true, | 
|  | verifier = |manifest: &Manifest| { manifest.arch.target.len() == 2 }, | 
|  | ); | 
|  | } |