[cmx2cml] Experimental tool to convert v1->v2 manifests.

See tools/cmx2cml/README.md for more information and limitations.

Only intended to work for fuchsia.git to start. It might be
possible to use this for manifests in different repos but needs
to demonstrate value before putting in the work to port it.

DID YOU KNOW: the asteroid that killed the dinosaurs is named
the Chicxulub impactor which if you squint looks like CMXulub.

Depends on https://github.com/google/serde_json5/pull/2.

Change-Id: I8522fe9d35818353d87f67ecfe40b04e0859964d
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/689802
Reviewed-by: Shai Barack <shayba@google.com>
Reviewed-by: Xyan Bhatnagar <xbhatnag@google.com>
Commit-Queue: Adam Perry <adamperry@google.com>
Fuchsia-Auto-Submit: Adam Perry <adamperry@google.com>
diff --git a/src/sys/lib/cm_types/src/lib.rs b/src/sys/lib/cm_types/src/lib.rs
index 09e67b1..3948619 100644
--- a/src/sys/lib/cm_types/src/lib.rs
+++ b/src/sys/lib/cm_types/src/lib.rs
@@ -101,7 +101,8 @@
     /// fails validation. The string must be non-empty, no more than 100
     /// characters in length, and consist of one or more of the
     /// following characters: `a-z`, `0-9`, `_`, `.`, `-`.
-    pub fn new(name: String) -> Result<Self, ParseError> {
+    pub fn new(name: impl Into<String>) -> Result<Self, ParseError> {
+        let name = name.into();
         Self::validate(Cow::Owned(name.clone()), MAX_NAME_LENGTH)?;
         Ok(Self(name))
     }
@@ -133,6 +134,12 @@
     }
 }
 
+impl PartialEq<String> for Name {
+    fn eq(&self, o: &String) -> bool {
+        self.0 == *o
+    }
+}
+
 impl fmt::Display for Name {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         <String as fmt::Display>::fmt(&self.0, f)
@@ -203,7 +210,8 @@
     /// fails validation. The string must be non-empty, no more than 1024
     /// characters in length, start with a leading `/`, and contain no empty
     /// path segments.
-    pub fn new(path: String) -> Result<Self, ParseError> {
+    pub fn new(path: impl Into<String>) -> Result<Self, ParseError> {
+        let path = path.into();
         Self::validate(&path)?;
         Ok(Path(path))
     }
@@ -367,14 +375,15 @@
 
 /// A component URL. The URL is validated, but represented as a string to avoid
 /// normalization and retain the original representation.
-#[derive(Serialize, Clone, Debug, PartialEq, Eq)]
+#[derive(Serialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
 pub struct Url(String);
 
 impl Url {
     /// Creates a `Url` from a `String`, returning an `Err` if the string fails
     /// validation. The string must be non-empty, no more than 4096 characters
     /// in length, and be a valid URL. See the [`url`](../../url/index.html) crate.
-    pub fn new(url: String) -> Result<Self, ParseError> {
+    pub fn new(url: impl Into<String>) -> Result<Self, ParseError> {
+        let url = url.into();
         Self::from_str_impl(Cow::Owned(url))
     }
 
diff --git a/tools/BUILD.gn b/tools/BUILD.gn
index 444d41e..8047c5e 100644
--- a/tools/BUILD.gn
+++ b/tools/BUILD.gn
@@ -14,6 +14,7 @@
     "//tools/bundle_fetcher($host_toolchain)",
     "//tools/clidoc:clidoc",
     "//tools/cmc:install($host_toolchain)",
+    "//tools/cmx2cml:install($host_toolchain)",
     "//tools/component_id_index($host_toolchain)",
     "//tools/component_manager_config($host_toolchain)",
     "//tools/configc:install($host_toolchain)",
@@ -157,6 +158,7 @@
     "//tools/check-licenses:tests",
     "//tools/clidoc:tests",
     "//tools/cmc:tests",
+    "//tools/cmx2cml:tests",
     "//tools/component_id_index:tests($host_toolchain)",
     "//tools/component_manager_config:tests($host_toolchain)",
     "//tools/configc:tests",
diff --git a/tools/cmx2cml/BUILD.gn b/tools/cmx2cml/BUILD.gn
new file mode 100644
index 0000000..43e81ed
--- /dev/null
+++ b/tools/cmx2cml/BUILD.gn
@@ -0,0 +1,76 @@
+# Copyright 2022 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.
+
+import("//build/host.gni")
+import("//build/rust/rustc_binary.gni")
+
+rustc_binary("cmx2cml") {
+  edition = "2018"
+
+  deps = [
+    "//src/lib/fuchsia",
+    "//third_party/rust_crates:anyhow",
+    "//third_party/rust_crates:argh",
+    "//third_party/rust_crates:json5format",
+    "//third_party/rust_crates:once_cell",
+    "//third_party/rust_crates:serde",
+    "//third_party/rust_crates:serde_json",
+    "//third_party/rust_crates:serde_json5",
+    "//tools/lib/cml",
+  ]
+
+  sources = [
+    "src/facets.rs",
+    "src/features.rs",
+    "src/main.rs",
+    "src/program.rs",
+    "src/runner.rs",
+    "src/sandbox.rs",
+    "src/warnings.rs",
+  ]
+
+  inputs = [ "injected_services_map.json5" ]
+}
+
+install_host_tools("install") {
+  deps = [ ":cmx2cml" ]
+  outputs = [ "cmx2cml" ]
+}
+
+# NOTE: If changing these target paths to match a new location, please also update
+# `injected_services_map.json5` so users of this converter will get the correct label.
+group("injected_service_v2_providers") {
+  testonly = true
+  deps = [
+    "//src/connectivity/location/regulatory_region:regulatory_region_component($default_toolchain)",
+    "//src/connectivity/lowpan/service:lowpanservice-cv2($default_toolchain)",
+    "//src/connectivity/network/dns:component($default_toolchain)",
+    "//src/connectivity/network/http-client:component($default_toolchain)",
+    "//src/connectivity/network/netstack:component($default_toolchain)",
+    "//src/connectivity/network/tun/network-tun:component-v2($default_toolchain)",
+    "//src/developer/build_info/testing:fake-build-info-component($default_toolchain)",
+    "//src/developer/memory/monitor:component($default_toolchain)",
+    "//src/developer/tracing/bin/trace_manager:component_cfv2($default_toolchain)",
+    "//src/diagnostics/archivist:archivist-for-embedding-v2($default_toolchain)",
+    "//src/fonts/fake:fake-fonts-cm($default_toolchain)",
+    "//src/fonts/tests/integration:mock_font_resolver_cm($default_toolchain)",
+    "//src/media/audio/audio_core/v2:audio_core_audio_core_component($default_toolchain)",
+    "//src/media/codec/factory:fake_codec_factory($default_toolchain)",
+    "//src/sys/stash:stash_v2($default_toolchain)",
+    "//src/testing/fidl/intl_property_manager:intl_property_manager_component($default_toolchain)",
+    "//src/ui/a11y/bin/a11y_manager:component_v2($default_toolchain)",
+    "//src/ui/bin/hardware_display_controller_provider:fake-hardware-display-controller-provider-cmv2-component($default_toolchain)",
+    "//src/ui/bin/root_presenter:component_v2($default_toolchain)",
+    "//src/ui/bin/text:text_manager_comp_v2($default_toolchain)",
+    "//src/ui/scenic:component_v2($default_toolchain)",
+  ]
+}
+
+group("tests") {
+  testonly = true
+  deps = [
+    ":injected_service_v2_providers",
+    "tests",
+  ]
+}
diff --git a/tools/cmx2cml/OWNERS b/tools/cmx2cml/OWNERS
new file mode 100644
index 0000000..caeb2f1
--- /dev/null
+++ b/tools/cmx2cml/OWNERS
@@ -0,0 +1 @@
+adamperry@google.com
diff --git a/tools/cmx2cml/README.md b/tools/cmx2cml/README.md
new file mode 100644
index 0000000..af3fb1e
--- /dev/null
+++ b/tools/cmx2cml/README.md
@@ -0,0 +1,96 @@
+# cmx2cml
+
+An experimental tool to make it easier to migrate fuchsia.git v1 components to
+v2.
+
+This is an aid to save time during go/cmxtinction, and users should expect to
+perform manual work to make the generated files acceptable. It is still the
+user's responsibility to:
+
+* select the correct runner for the new manifest
+* integrate the new file with the build
+* declare any `expose`d capabilities
+* add inspect shards if applicable
+* (for system components) add the component to the v2 topology
+* route any required capabilities in the surrounding topology
+* update the storage index and/or diagnostics selectors
+
+See comments in the generated CML for more detailed follow-up actions.
+
+## Usage
+
+Run a build and then `fx cmx2cml --runner RUNNER PATH_TO_CMX`. Runner options:
+
+* `elf`
+* `elf-test`
+* `rust-test`
+
+If conversion is successful a CML file will be written alongside the original
+CMX. You can override the output location with `--output path/to.cml`.
+
+## Getting Help
+
+See the overall docs for the [Components v2 migration].
+
+As with all aspects of the components migration, reach out to the Component
+Framework team if you have issues.
+
+## Known limitations of produced CML
+
+v1 manifests do not require components to list which protocols they expose to
+their parent, which means that this tool cannot statically know what to put in
+a CML file's `expose` section.
+
+The only test runner supported is the ELF test runner, because it allows
+transparently migrating args and env vars. Users can follow up and choose a more
+featureful test runner for their language as a follow-up.
+
+The converter only reasons about children when migrating `injected-services`
+declarations, otherwise it is assumed that any existing v1 children will stay
+as v1 components with their lifecycle managed dynamically using `fuchsia.sys.*`.
+
+Children added to replace injected services may need additional capabilities
+routed to them.
+
+## Known CMX feature gaps
+
+> Note: while there are significant limitations to this approach, this tool was
+able to convert ~620 of ~900 CMX files in fuchsia.git at time of writing without
+encountering any of the below gaps.
+
+The initial version of this tool does not support converting Dart components.
+
+This tool does not expand shards before converting, so CMX files which rely on
+shards to populate key portions of e.g. `program` will not work.
+
+Not all providers of `injected-services` have known v2 equivalents to use.
+
+Tests which specify args for the components listed in `injected-services` are
+not supported, as v2 static children must be referenced solely by URL.
+
+Components which request `sandbox.boot`, `sandbox.pkgfs`, or `sandbox.system`
+are not supported.
+
+Components which use the `hub` feature will need to manually add the appropriate
+uses for events and migrate their tests' code to make use of the new protocols.
+
+Components which use the `deprecated-ambient-replace-as-executable`,
+`deprecated-shell` or `deprecated-global-dev` features are not supported.
+
+Components which request device directories other than those offered by
+test_manager are not supported.
+
+## Testing
+
+See `tests/goldens` for some example CMX files that are converted to CML by
+the build.
+
+When making changes that introduce new errors or panics it can be useful to run
+this tool on ~all CMX files available. Try running `./try_convert_all_cmx.sh` to
+see the tool in action. Beware it will leave several hundred possibly-valid CML
+files in your tree. If that script prints errors that are due to known
+limitations, add the CMX file to the list of exceptions at the top of the
+script. Consider adding new CMX->CML goldens if your change changes behavior
+that is only executed by this script.
+
+[Components v2 migration]: https://fuchsia.dev/fuchsia-src/development/components/v2/migration?hl=en
diff --git a/tools/cmx2cml/injected_services_map.json5 b/tools/cmx2cml/injected_services_map.json5
new file mode 100644
index 0000000..8d28e44
--- /dev/null
+++ b/tools/cmx2cml/injected_services_map.json5
@@ -0,0 +1,134 @@
+{
+    protocols: {
+        "fuchsia.accessibility.semantics.SemanticsManager": "#meta/a11y-manager.cm",
+        "fuchsia.buildinfo.Provider": "#meta/build-info.cm",
+        "fuchsia.diagnostics.ArchiveAccessor": "parent",
+        "fuchsia.factory.lowpan.FactoryLookup": "#meta/lowpanservice.cm",
+        "fuchsia.factory.lowpan.FactoryRegister": "#meta/lowpanservice.cm",
+        "fuchsia.fonts.Provider": "#meta/fonts.cm",
+        "fuchsia.hardware.display.Provider": "#meta/hdcp.cm",
+        "fuchsia.intl.PropertyProvider": "#meta/intl_property_manager.cm",
+        "fuchsia.kms.KeyManager": "#meta/key_manager.cm",
+        "fuchsia.location.namedplace.RegulatoryRegionConfigurator": "#meta/regulatory_region_v1.cm",
+        "fuchsia.location.namedplace.RegulatoryRegionWatcher": "#meta/regulatory_region_v1.cm",
+        "fuchsia.logger.Log": "parent",
+        "fuchsia.logger.LogSink": "parent",
+        "fuchsia.lowpan.device.CountersConnector": "#meta/lowpanservice.cm",
+        "fuchsia.lowpan.device.DeviceConnector": "#meta/lowpanservice.cm",
+        "fuchsia.lowpan.device.DeviceExtraConnector": "#meta/lowpanservice.cm",
+        "fuchsia.lowpan.device.EnergyScanConnector": "#meta/lowpanservice.cm",
+        "fuchsia.lowpan.DeviceWatcher": "#meta/lowpanservice.cm",
+        "fuchsia.lowpan.driver.Register": "#meta/lowpanservice.cm",
+        "fuchsia.lowpan.experimental.DeviceConnector": "#meta/lowpanservice.cm",
+        "fuchsia.lowpan.experimental.DeviceExtraConnector": "#meta/lowpanservice.cm",
+        "fuchsia.lowpan.experimental.DeviceRouterConnector": "#meta/lowpanservice.cm",
+        "fuchsia.lowpan.experimental.DeviceRouterExtraConnector": "#meta/lowpanservice.cm",
+        "fuchsia.lowpan.experimental.LegacyJoiningConnector": "#meta/lowpanservice.cm",
+        "fuchsia.lowpan.test.DeviceTestConnector": "#meta/lowpanservice.cm",
+        "fuchsia.lowpan.thread.DatasetConnector": "#meta/lowpanservice.cm",
+        "fuchsia.lowpan.thread.MeshcopConnector": "#meta/lowpanservice.cm",
+        "fuchsia.media.AudioCore": "#meta/audio_core.cm",
+        "fuchsia.mediacodec.CodecFactory": "#meta/codec_factory.cm",
+        "fuchsia.memorypressure.Provider": "#meta/memory_monitor.cm",
+        "fuchsia.net.http.Loader": "#meta/http-client.cm",
+        "fuchsia.net.interfaces.State": "#meta/netstack.cm",
+        "fuchsia.net.name.Lookup": "#meta/dns-resolver.cm",
+        "fuchsia.net.routes.State": "#meta/netstack.cm",
+        "fuchsia.net.stack.Stack": "#meta/netstack.cm",
+        "fuchsia.net.tun.Control": "#meta/network-tun.cm",
+        "fuchsia.netstack.Netstack": "#meta/netstack.cm",
+        "fuchsia.openthread.devmgr.IsolatedDevmgr": "#meta/ot-devmgr-component-integration.cm",
+        "fuchsia.pkg.FontResolver": "#meta/mock_font_resolver.cm",
+        "fuchsia.posix.socket.Provider": "#meta/netstack.cm",
+        "fuchsia.stash.Store": "#meta/stash.cm",
+        "fuchsia.tracing.provider.Registry": "#meta/trace_manager.cm",
+        "fuchsia.ui.input.ImeService": "#meta/text_manager.cm",
+        "fuchsia.ui.input.InputDeviceRegistry": "#meta/root_presenter.cm",
+        "fuchsia.ui.pointerinjector.Registry": "#meta/scenic.cm",
+        "fuchsia.ui.policy.Presenter": "#meta/root_presenter.cm",
+        "fuchsia.ui.scenic.Scenic": "#meta/scenic.cm",
+        "fuchsia.web.ContextProvider": "#meta/context_provider.cm",
+    },
+    components: {
+        "#meta/a11y-manager.cm": {
+            name: "a11y_manager",
+            gn_target: "//src/ui/a11y/bin/a11y_manager:component_v2",
+        },
+        "#meta/audio_core.cm": {
+            name: "audio_core",
+            gn_target: "//src/media/audio/audio_core/v2:audio_core_audio_core_component",
+        },
+        "#meta/build-info.cm": {
+            name: "build_info",
+            gn_target: "//src/developer/build_info/testing:fake-build-info-component",
+        },
+        "#meta/codec_factory.cm": {
+            name: "codec_factory",
+            gn_target: "//src/media/codec/factory:fake_codec_factory",
+        },
+        "#meta/dns-resolver.cm": {
+            name: "dns_resolver",
+            gn_target: "//src/connectivity/network/dns:component",
+        },
+        "#meta/fonts.cm": {
+            name: "fonts",
+            gn_target: "//src/fonts/fake:fake-fonts-cm",
+        },
+        "#meta/hdcp.cm": {
+            name: "hdcp",
+            gn_target: "//src/ui/bin/hardware_display_controller_provider:fake-hardware-display-controller-provider-cmv2-component",
+        },
+        "#meta/http-client.cm": {
+            name: "http_client",
+            gn_target: "//src/connectivity/network/http-client:component",
+        },
+        "#meta/intl_property_manager.cm": {
+            name: "intl_property_manager",
+            gn_target: "//src/testing/fidl/intl_property_manager:intl_property_manager_component",
+        },
+        "#meta/lowpanservice.cm": {
+            name: "lowpan",
+            gn_target: "//src/connectivity/lowpan/service:lowpanservice-cv2",
+        },
+        "#meta/memory_monitor.cm": {
+            name: "memory_monitor",
+            gn_target: "//src/developer/memory/monitor:component",
+        },
+        "#meta/mock_font_resolver.cm": {
+            name: "font_resolver",
+            gn_target: "//src/fonts/tests/integration:mock_font_resolver_cm",
+        },
+        "#meta/netstack.cm": {
+            name: "netstack",
+            gn_target: "//src/connectivity/network/netstack:component",
+        },
+        "#meta/network-tun.cm": {
+            name: "tun",
+            gn_target: "//src/connectivity/network/tun/network-tun:component-v2",
+        },
+        "#meta/regulatory_region.cm": {
+            name: "regulatory_region",
+            gn_target: "//src/connectivity/location/regulatory_region:regulatory_region_component",
+        },
+        "#meta/root_presenter.cm": {
+            name: "root_presenter",
+            gn_target: "//src/ui/bin/root_presenter:component_v2",
+        },
+        "#meta/scenic.cm": {
+            name: "scenic",
+            gn_target: "//src/ui/scenic:component_v2_with_supplied_display_provider",
+        },
+        "#meta/stash.cm": {
+            name: "stash",
+            gn_target: "//src/sys/stash:stash_v2",
+        },
+        "#meta/text_manager.cm": {
+            name: "text_manager",
+            gn_target: "//src/ui/bin/text:text_manager_comp_v2",
+        },
+        "#meta/trace_manager.cm": {
+            name: "trace_manager",
+            gn_target: "//src/developer/tracing/bin/trace_manager:component_cfv2",
+        },
+    },
+}
diff --git a/tools/cmx2cml/src/facets.rs b/tools/cmx2cml/src/facets.rs
new file mode 100644
index 0000000..94ed352
--- /dev/null
+++ b/tools/cmx2cml/src/facets.rs
@@ -0,0 +1,189 @@
+// Copyright 2022 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::{warnings::Warning, SYSTEM_TEST_SHARD};
+use anyhow::{bail, Context, Error};
+use once_cell::sync::Lazy;
+use serde::Deserialize;
+use std::collections::{BTreeMap, BTreeSet};
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct CmxFacets {
+    #[serde(rename = "fuchsia.test")]
+    test: Option<TestFacets>,
+
+    #[serde(rename = "fuchsia.module")]
+    #[allow(unused)] // included for confidence in completeness of cmx parser but unsupported
+    module: Option<CmxModule>,
+}
+
+impl CmxFacets {
+    /// Convert the facets into the appropriate shards and `use`s in CML, returning the set of
+    /// protocols received from children.
+    pub fn convert(
+        &self,
+        include: &mut BTreeSet<String>,
+        uses: &mut Vec<cml::Use>,
+        children: &mut Vec<cml::Child>,
+        warnings: &mut BTreeSet<Warning>,
+    ) -> Result<BTreeSet<String>, Error> {
+        let mut injected = BTreeSet::new();
+        if let Some(test) = &self.test {
+            injected = test.convert(include, uses, children, warnings)?;
+        }
+
+        if self.module.is_some() {
+            bail!("fuchsia modules are not supported by this converter");
+        }
+
+        Ok(injected)
+    }
+}
+
+#[derive(Debug, Deserialize)]
+#[allow(unused)] // included for completeness of parser on in-tree cmx files but unsupported
+#[serde(deny_unknown_fields)]
+struct CmxModule {
+    #[serde(rename = "@version")]
+    version: u8,
+    intent_filters: Vec<serde_json::Value>,
+    suggestion_headline: String,
+    composition_pattern: String,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct TestFacets {
+    #[serde(rename = "injected-services")]
+    injected: Option<BTreeMap<String, InjectedService>>,
+    #[serde(rename = "system-services")]
+    system: Option<Vec<String>>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields, untagged)]
+enum InjectedService {
+    Url(String),
+    UrlWithArgs(Vec<String>),
+}
+
+impl TestFacets {
+    /// Convert test facets, returning the set of all protocols that come from injected children.
+    pub fn convert(
+        &self,
+        include: &mut BTreeSet<String>,
+        uses: &mut Vec<cml::Use>,
+        children: &mut Vec<cml::Child>,
+        warnings: &mut BTreeSet<Warning>,
+    ) -> Result<BTreeSet<String>, Error> {
+        let mut injected_protocols = BTreeSet::new();
+        if let Some(injected) = &self.injected {
+            // for each injected service, add a child and use the protocol from it
+            // do this in a separate loop from modifying cml so that we can group things
+            let mut children_by_name: BTreeMap<_, BTreeSet<_>> = BTreeMap::new();
+            for (protocol, v1_provider) in injected {
+                if let InjectedService::UrlWithArgs(..) = v1_provider {
+                    bail!("injected services that pass arguments are not supported");
+                }
+                let (url, InjectedServiceProvider { name, gn_target }) =
+                    if let Some(p) = InjectedServicesMap::provider(protocol)? {
+                        p
+                    } else {
+                        // this protocol doesn't need a static child in v2, get it from parent
+                        // v1 protocols needed to be be in sandbox already, so just skip this one
+                        // before we remove it from existing uses from parent
+                        continue;
+                    };
+
+                children_by_name.entry((name, url, gn_target)).or_default().insert(protocol);
+                injected_protocols.insert(protocol.to_owned());
+            }
+
+            for ((name, url, gn_target), protocols) in children_by_name {
+                let child_name =
+                    cml::Name::new(&name).with_context(|| format!("declaring child {name}"))?;
+                let url =
+                    cml::Url::new(&url).with_context(|| format!("parsing {url} as a CML URL"))?;
+
+                warnings.insert(Warning::ChildNeedsGnTargetAndRouting {
+                    child: name.clone(),
+                    gn_target,
+                });
+                children.push(cml::Child {
+                    name: child_name.clone(),
+                    url,
+                    startup: cml::StartupMode::Lazy,
+                    on_terminate: None,
+                    environment: None,
+                });
+
+                let protocols = protocols
+                    .into_iter()
+                    .map(|p| {
+                        cml::Name::new(p)
+                            .with_context(|| format!("defining name for use decl for {p}"))
+                    })
+                    .collect::<Result<Vec<_>, _>>()?;
+                uses.push(cml::Use {
+                    protocol: Some(cml::OneOrMany::Many(protocols)),
+                    from: Some(cml::UseFromRef::Named(child_name)),
+                    ..Default::default()
+                });
+            }
+        }
+
+        if self.system.is_some() {
+            // system tests get a different test realm, spell this as a shard
+            include.insert(SYSTEM_TEST_SHARD.to_string());
+
+            // in v1, system services need to be spelled twice, so we already have a `use` for them
+        }
+
+        Ok(injected_protocols)
+    }
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct InjectedServicesMap {
+    protocols: BTreeMap<String, String>,
+    components: BTreeMap<String, InjectedServiceProvider>,
+}
+
+static INJECTED_SERVICES_MAP: Lazy<InjectedServicesMap> = Lazy::new(|| {
+    static MAP_RAW: &str = include_str!("../injected_services_map.json5");
+    let parsed: InjectedServicesMap =
+        serde_json5::from_str(MAP_RAW).expect("injected services map must be valid json");
+    parsed
+});
+
+impl InjectedServicesMap {
+    /// Returns the URL and provider info for a given protocol's injected service provider if
+    /// available. Returns `None` if the protocol should be retrieved from `parent` in v2 tests.
+    pub fn provider(protocol: &str) -> Result<Option<(String, InjectedServiceProvider)>, Error> {
+        if let Some(url) = INJECTED_SERVICES_MAP.protocols.get(protocol) {
+            if url == "parent" {
+                return Ok(None);
+            }
+            if let Some(provider) = INJECTED_SERVICES_MAP.components.get(url) {
+                Ok(Some((url.to_owned(), provider.to_owned())))
+            } else {
+                bail!("`{protocol}`'s provider {url} does not have a registered implementation");
+            }
+        } else {
+            bail!("`{protocol}` does not have a registered provider for injection");
+        }
+    }
+}
+
+#[derive(Clone, Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct InjectedServiceProvider {
+    /// the name of the static child for the provider
+    pub name: String,
+
+    /// the GN target that must be added to a package to have access to the provider
+    pub gn_target: String,
+}
diff --git a/tools/cmx2cml/src/features.rs b/tools/cmx2cml/src/features.rs
new file mode 100644
index 0000000..88a49a0
--- /dev/null
+++ b/tools/cmx2cml/src/features.rs
@@ -0,0 +1,177 @@
+// Copyright 2022 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::{warnings::Warning, BUILD_INFO_PROTOCOL, SYSTEM_TEST_SHARD};
+use anyhow::{bail, Error};
+use serde::Deserialize;
+
+#[derive(Debug, Deserialize)]
+pub enum CmxFeature {
+    #[serde(rename = "isolated-persistent-storage")]
+    IsolatedPersistentStorage,
+    #[serde(rename = "isolated-cache-storage")]
+    IsolatedCacheStorage,
+    #[serde(rename = "isolated-temp")]
+    IsolatedTemp,
+
+    #[serde(rename = "factory-data")]
+    FactoryData,
+    #[serde(rename = "durable-data")]
+    DurableData,
+    #[serde(rename = "shell-commands")]
+    ShellCommands,
+    #[serde(rename = "root-ssl-certificates")]
+    RootSslCerts,
+
+    #[serde(rename = "config-data")]
+    ConfigData,
+
+    #[serde(rename = "dev")]
+    Dev,
+
+    #[serde(rename = "hub")]
+    Hub,
+
+    #[serde(rename = "build-info")]
+    BuildInfo,
+
+    #[serde(rename = "vulkan")]
+    Vulkan,
+
+    #[serde(rename = "deprecated-ambient-replace-as-executable")]
+    AmbientReplaceAsExecutable,
+
+    #[serde(rename = "deprecated-shell")]
+    Shell,
+
+    #[serde(rename = "deprecated-global-dev")]
+    GlobalDev,
+
+    #[serde(rename = "deprecated-misc-storage")]
+    MiscStorage,
+}
+
+impl CmxFeature {
+    /// Returns any use decl, warnings, or shards needed to convert the feature to v2.
+    pub fn uses(
+        &self,
+        is_test: bool,
+    ) -> Result<(Option<cml::Use>, Option<Warning>, Option<String>), Error> {
+        Ok(match self {
+            CmxFeature::IsolatedPersistentStorage => (
+                Some(cml::Use {
+                    storage: Some(cml::Name::new("data").unwrap()),
+                    path: Some(cml::Path::new("/data").unwrap()),
+                    ..Default::default()
+                }),
+                Some(Warning::StorageIndex),
+                None,
+            ),
+            CmxFeature::IsolatedCacheStorage => (
+                Some(cml::Use {
+                    storage: Some(cml::Name::new("cache").unwrap()),
+                    path: Some(cml::Path::new("/cache").unwrap()),
+                    ..Default::default()
+                }),
+                None,
+                None,
+            ),
+            CmxFeature::IsolatedTemp => (
+                Some(cml::Use {
+                    storage: Some(cml::Name::new("tmp").unwrap()),
+                    path: Some(cml::Path::new("/tmp").unwrap()),
+                    ..Default::default()
+                }),
+                None,
+                None,
+            ),
+            CmxFeature::FactoryData => (
+                Some(cml::Use {
+                    directory: Some(cml::Name::new("factory").unwrap()),
+                    rights: Some(cml::Rights(vec![cml::Right::ReadAlias])),
+                    path: Some(cml::Path::new("/factory").unwrap()),
+                    ..Default::default()
+                }),
+                None,
+                None,
+            ),
+            CmxFeature::DurableData => (
+                Some(cml::Use {
+                    directory: Some(cml::Name::new("durable").unwrap()),
+                    rights: Some(cml::Rights(vec![cml::Right::ReadAlias])),
+                    path: Some(cml::Path::new("/durable").unwrap()),
+                    ..Default::default()
+                }),
+                None,
+                None,
+            ),
+            CmxFeature::ShellCommands => (
+                Some(cml::Use {
+                    directory: Some(cml::Name::new("bin").unwrap()),
+                    rights: Some(cml::Rights(vec![cml::Right::ReadAlias])),
+                    path: Some(cml::Path::new("/bin").unwrap()),
+                    ..Default::default()
+                }),
+                None,
+                None,
+            ),
+            CmxFeature::RootSslCerts => (
+                Some(cml::Use {
+                    directory: Some(cml::Name::new("root-ssl-certificates").unwrap()),
+                    rights: Some(cml::Rights(vec![cml::Right::ReadAlias])),
+                    path: Some(cml::Path::new("/config/ssl").unwrap()),
+                    ..Default::default()
+                }),
+                None,
+                if is_test { Some(SYSTEM_TEST_SHARD.to_string()) } else { None },
+            ),
+            CmxFeature::ConfigData => (
+                Some(cml::Use {
+                    directory: Some(cml::Name::new("config-data").unwrap()),
+                    rights: Some(cml::Rights(vec![cml::Right::ReadAlias])),
+                    path: Some(cml::Path::new("/config/data").unwrap()),
+                    ..Default::default()
+                }),
+                if is_test { Some(Warning::ConfigDataInTest) } else { None },
+                None,
+            ),
+            CmxFeature::BuildInfo => (
+                Some(cml::Use {
+                    protocol: Some(cml::OneOrMany::One(
+                        cml::Name::new(BUILD_INFO_PROTOCOL).unwrap(),
+                    )),
+                    ..Default::default()
+                }),
+                Some(Warning::BuildInfoImpl),
+                None,
+            ),
+
+            // correctly replacing the hub with event streams does require `use`s but we can't
+            // statically know which events to request here, just let the user fill it in from docs
+            CmxFeature::Hub => (None, Some(Warning::UsesHub), None),
+
+            // sandbox requests actual paths, no uses needed here but does need system tests
+            CmxFeature::Dev => {
+                (None, None, if is_test { Some(SYSTEM_TEST_SHARD.to_string()) } else { None })
+            }
+
+            // unsupported features:
+            CmxFeature::Vulkan => {
+                bail!("vulkan feature is unsupported for conversion")
+            }
+            CmxFeature::AmbientReplaceAsExecutable => {
+                bail!("deprecated-ambient-replace-as-executable feature is unsupported for conversion")
+            }
+            CmxFeature::Shell => {
+                bail!("deprecated-shell feature is unsupported for conversion")
+            }
+            CmxFeature::GlobalDev => {
+                bail!("deprecated-global-dev feature is unsupported for conversion")
+            }
+            CmxFeature::MiscStorage => {
+                bail!("deprecated-misc-storage feature is unsupported for conversion")
+            }
+        })
+    }
+}
diff --git a/tools/cmx2cml/src/main.rs b/tools/cmx2cml/src/main.rs
new file mode 100644
index 0000000..08bf1b5
--- /dev/null
+++ b/tools/cmx2cml/src/main.rs
@@ -0,0 +1,278 @@
+// Copyright 2022 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 anyhow::{bail, ensure, Context, Error};
+use argh::FromArgs;
+use serde::Deserialize;
+use std::{
+    collections::BTreeSet,
+    fmt::Write,
+    path::{Path, PathBuf},
+};
+
+mod facets;
+mod features;
+mod program;
+mod runner;
+mod sandbox;
+mod warnings;
+
+use facets::CmxFacets;
+use program::CmxProgram;
+use runner::RunnerSelection;
+use sandbox::CmxSandbox;
+use warnings::Warning;
+
+/// EXPERIMENTAL convert CMX files to CML, see //tools/cmx2cml/README.md for details
+#[derive(FromArgs, Debug)]
+pub struct Opt {
+    /// file to process
+    #[argh(positional)]
+    cmx: Option<PathBuf>,
+
+    /// path to a newline-delimited file with multiple CMXes to convert, intended for use by scripts
+    #[argh(option, short = 'f')]
+    path_to_cmxes: Option<PathBuf>,
+
+    /// runner to use
+    #[argh(option, long = "runner")]
+    runner: RunnerSelection,
+
+    /// path to which to write generated CML, defaults to `$(basename $CMX).cml`
+    #[argh(option, long = "output")]
+    output: Option<PathBuf>,
+}
+
+#[fuchsia::main]
+fn main() -> Result<(), Error> {
+    let Opt { cmx, path_to_cmxes, runner, output } = argh::from_env();
+    let mut cmxes = vec![];
+    if let Some(cmx) = cmx {
+        cmxes.push(cmx);
+    }
+    if let Some(path) = path_to_cmxes {
+        let files = std::fs::read_to_string(path).expect("must be able to read cmx input file");
+        cmxes.extend(files.lines().map(PathBuf::from));
+    }
+
+    let mut errors = vec![];
+    for cmx in cmxes {
+        let out_path = if let Some(p) = &output {
+            p.to_path_buf()
+        } else {
+            let mut out_path = cmx.to_owned();
+            out_path.set_extension("cml");
+            out_path
+        };
+
+        if let Err(e) = convert_cmx(&cmx, runner, &out_path)
+            .with_context(|| format!("converting {}", cmx.display()))
+        {
+            errors.push(e);
+        }
+    }
+
+    if errors.is_empty() {
+        Ok(())
+    } else {
+        let mut err_msges = String::new();
+
+        for e in errors {
+            err_msges.push_str(&e.to_string());
+            let mut source = e.source();
+            while let Some(s) = source {
+                write!(err_msges, "\n    └── {}", s)?;
+                source = s.source();
+            }
+            err_msges.push_str("\n\n");
+        }
+
+        bail!("{}", err_msges)
+    }
+}
+
+const COPYRIGHT_HEADER: &str = "// Copyright 2022 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.\n";
+
+fn convert_cmx(cmx: &Path, runner: RunnerSelection, out_path: &Path) -> Result<(), Error> {
+    assert_eq!(cmx.extension().map(|s| s.to_str().unwrap()), Some("cmx"));
+    let in_bytes = std::fs::read_to_string(&cmx).context("reading cmx file")?;
+
+    // use JSON5 because some CMX files have JSON5 elements...sigh
+    let value: Cmx = serde_json5::from_str(&in_bytes).context("parsing as Cmx")?;
+
+    // convert the cmx to a cml::Document
+    let (cml, warnings) = value.convert(runner).context("converting to Cml")?;
+
+    // get the JSON5 representation and format it
+    let output = COPYRIGHT_HEADER.to_string()
+        + &serde_json5::to_string(&cml).context("serializing CML to JSON5 repr")?;
+    let formatted =
+        String::from_utf8(cml::format_cml(&output, &out_path).context("formatting generated CML")?)
+            .context("creating string from formatted CML bytes")?;
+
+    // split by lines and add comments for any warnings we encountered
+    let mut formatted_lines = formatted.lines().map(|s| s.to_string()).collect::<Vec<_>>();
+    for warning in warnings {
+        warning.apply(&mut formatted_lines);
+    }
+
+    // we format again so that indentation of inserted warning comments lines up
+    let reformatted = cml::format_cml(&formatted_lines.join("\n"), &out_path)
+        .context("formatting generated CML")?;
+
+    std::fs::write(&out_path, &reformatted)
+        .with_context(|| format!("writing to {}", out_path.display()))?;
+
+    Ok(())
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct Cmx {
+    include: Option<Vec<String>>,
+    program: Option<CmxProgram>,
+    runner: Option<String>,
+    sandbox: Option<CmxSandbox>,
+    facets: Option<CmxFacets>,
+}
+
+impl Cmx {
+    fn convert(self, runner: RunnerSelection) -> Result<(cml::Document, BTreeSet<Warning>), Error> {
+        let mut warnings = BTreeSet::new();
+        if let Some(w) = runner.warning() {
+            warnings.insert(w);
+        }
+
+        let mut include: BTreeSet<String> = if let Some(includes) = self.include {
+            includes
+                .iter()
+                .map(|i| {
+                    let mut p = PathBuf::from(i);
+                    p.set_extension("cml");
+                    p.display().to_string()
+                })
+                .collect()
+        } else {
+            Default::default()
+        };
+        // warn about shard renames if we don't recognize any of them
+        if include.iter().any(|i| !KNOWN_INCLUDES.contains(&&**i)) {
+            warnings.insert(Warning::IncludesRenamed);
+        }
+
+        runner.fix_includes(&mut include);
+        ensure!(self.runner.is_none(), "manually-defined runners in cmx files are not supported");
+
+        let program = if let Some(program) = self.program {
+            Some(program.convert(runner).context("converting program")?)
+        } else {
+            None
+        };
+
+        let mut uses = vec![];
+        let mut children = vec![];
+        let mut injected_protocols = BTreeSet::new();
+        if let Some(facets) = self.facets {
+            injected_protocols = facets
+                .convert(&mut include, &mut uses, &mut children, &mut warnings)
+                .context("converting test facets")?;
+        }
+
+        if let Some(sandbox) = self.sandbox {
+            uses.extend(sandbox.uses(
+                &mut warnings,
+                &mut include,
+                &injected_protocols,
+                runner.is_for_testing(),
+            )?);
+        };
+
+        Ok((
+            cml::Document {
+                program,
+                include: if include.is_empty() {
+                    None
+                } else {
+                    Some(include.into_iter().collect())
+                },
+                r#use: if uses.is_empty() { None } else { Some(uses) },
+                children: if children.is_empty() { None } else { Some(children) },
+
+                // we could try to wire up injected children's caps but that's probably more effort
+                // than it will take to do so manually for all of the cmx files with injection
+                offer: None,
+
+                // v1 components don't have this info, users must populate it
+                capabilities: None,
+                expose: None,
+
+                // v2 components can dynamically launch v1 children until those children are migrated
+                collections: None,
+
+                // v2 components support facets but all v1 facets are translated into other things in v2
+                facets: None,
+
+                // v1 components don't have equivalents for these
+                config: None,
+                environments: None,
+            },
+            warnings,
+        ))
+    }
+}
+
+const BUILD_INFO_PROTOCOL: &str = "fuchsia.buildinfo.Provider";
+const PROTOCOLS_FOR_HERMETIC_TESTS: &[&str] = &[
+    "fuchsia.boot.WriteOnlyLog",
+    "fuchsia.logger.LogSink",
+    "fuchsia.process.Launcher",
+    "fuchsia.sys2.EventSource",
+];
+const PROTOCOLS_FOR_SYSTEM_TESTS: &[&str] = &[
+    "fuchsia.boot.ReadOnlyLog",
+    "fuchsia.boot.RootResource",
+    "fuchsia.component.resolution.Resolver",
+    "fuchsia.exception.Handler",
+    "fuchsia.hwinfo.Board",
+    "fuchsia.hwinfo.Device",
+    "fuchsia.hwinfo.Product",
+    "fuchsia.kernel.Counter",
+    "fuchsia.kernel.CpuResource",
+    "fuchsia.kernel.DebugResource",
+    "fuchsia.kernel.HypervisorResource",
+    "fuchsia.kernel.InfoResource",
+    "fuchsia.kernel.IoportResource",
+    "fuchsia.kernel.IrqResource",
+    "fuchsia.kernel.MmioResource",
+    "fuchsia.kernel.PowerResource",
+    "fuchsia.kernel.RootJob",
+    "fuchsia.kernel.RootJobForInspect",
+    "fuchsia.kernel.SmcResource",
+    "fuchsia.kernel.Stats",
+    "fuchsia.kernel.VmexResource",
+    "fuchsia.net.http.Loader",
+    "fuchsia.scheduler.ProfileProvider",
+    "fuchsia.sys.Environment",
+    "fuchsia.sys.Loader",
+    "fuchsia.sysinfo.SysInfo",
+    "fuchsia.sysmem.Allocator",
+    "fuchsia.tracing.provider.Registry",
+    "fuchsia.vulkan.loader.Loader",
+];
+
+const KNOWN_INCLUDES: &[&str] = &[
+    SYSLOG_SHARD,
+    ELF_STDIO_SHARD,
+    ELF_TEST_RUNNER_SHARD,
+    RUST_TEST_RUNNER_SHARD,
+    SYSTEM_TEST_SHARD,
+];
+
+const SYSLOG_SHARD: &str = "syslog/client.shard.cml";
+const ELF_STDIO_SHARD: &str = "syslog/elf_stdio.shard.cml";
+const ELF_TEST_RUNNER_SHARD: &str = "//sdk/lib/sys/testing/elf_test_runner.shard.cml";
+const RUST_TEST_RUNNER_SHARD: &str = "//src/sys/test_runners/rust/default.shard.cml";
+const SYSTEM_TEST_SHARD: &str = "//src/sys/test_manager/system-test.shard.cml";
diff --git a/tools/cmx2cml/src/program.rs b/tools/cmx2cml/src/program.rs
new file mode 100644
index 0000000..b59c41f
--- /dev/null
+++ b/tools/cmx2cml/src/program.rs
@@ -0,0 +1,46 @@
+// Copyright 2022 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::runner::RunnerSelection;
+use anyhow::{bail, ensure, Error};
+use serde::Deserialize;
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields, untagged)]
+pub enum CmxProgram {
+    Elf {
+        binary: String,
+        args: Option<Vec<String>>,
+        env_vars: Option<Vec<String>>,
+    },
+    #[allow(unused)] // we want to deny unknown fields but don't support this
+    DartMaybe {
+        data: String,
+        args: Option<Vec<String>>,
+    },
+}
+
+impl CmxProgram {
+    pub fn convert(&self, runner: RunnerSelection) -> Result<cml::Program, Error> {
+        match self {
+            CmxProgram::Elf { binary, args, env_vars } => {
+                let mut info = serde_json::Map::new();
+                info.insert("binary".to_string(), binary.to_owned().into());
+
+                if let Some(args) = args {
+                    ensure!(runner.supports_args_and_env(), "runner must support argv");
+                    info.insert("args".to_string(), args.to_owned().into());
+                }
+
+                if let Some(env_vars) = env_vars {
+                    ensure!(runner.supports_args_and_env(), "runner must support env vars");
+                    info.insert("environ".to_string(), env_vars.to_owned().into());
+                }
+
+                Ok(cml::Program { runner: runner.runner_literal(), info })
+            }
+            CmxProgram::DartMaybe { .. } => bail!("dart v1 components not supported (yet?)"),
+        }
+    }
+}
diff --git a/tools/cmx2cml/src/runner.rs b/tools/cmx2cml/src/runner.rs
new file mode 100644
index 0000000..8f2237e
--- /dev/null
+++ b/tools/cmx2cml/src/runner.rs
@@ -0,0 +1,85 @@
+// Copyright 2022 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::{
+    warnings::Warning, ELF_STDIO_SHARD, ELF_TEST_RUNNER_SHARD, RUST_TEST_RUNNER_SHARD, SYSLOG_SHARD,
+};
+use anyhow::{bail, Error};
+use std::{collections::BTreeSet, str::FromStr};
+
+#[derive(Clone, Copy, Debug)]
+pub enum RunnerSelection {
+    Elf,
+    ElfTest,
+    RustTest,
+    // TODO support dart
+    // TODO maybe support gtest/gunit?
+}
+
+impl FromStr for RunnerSelection {
+    type Err = Error;
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        Ok(match s {
+            "elf" => Self::Elf,
+            "elf-test" => Self::ElfTest,
+            "rust-test" => Self::RustTest,
+            other => {
+                bail!("unrecognized runner {other}, options are `elf`, `elf-test`, `rust-test`")
+            }
+        })
+    }
+}
+
+impl RunnerSelection {
+    pub fn supports_args_and_env(&self) -> bool {
+        matches!(self, Self::Elf | Self::ElfTest)
+    }
+
+    pub fn is_for_testing(&self) -> bool {
+        matches!(self, Self::ElfTest | Self::RustTest)
+    }
+
+    /// Return any warnings that should be surfaced to the user based on their choice of runner.
+    pub fn warning(&self) -> Option<Warning> {
+        match self {
+            // ELF components usually expose some capabilities but this isn't written down in
+            // CMX files, so we insert a comment telling users to do so manually.
+            Self::Elf => Some(Warning::DeclareExpose),
+
+            // The ELF test runner is a lowest-common-denominator for C++ tests, warn users
+            // that they might want to pick a more specific runner.
+            Self::ElfTest => Some(Warning::ElfTestRunnerUsed),
+
+            Self::RustTest => None,
+        }
+    }
+
+    pub fn fix_includes(&self, include: &mut BTreeSet<String>) {
+        match self {
+            Self::Elf => {
+                // we need the stdio shard to make sure that stdout/stderr still go somewhere
+                // after migrating, and the stdio shard already includes the syslog shard
+                include.remove(SYSLOG_SHARD);
+                include.insert(ELF_STDIO_SHARD.to_owned());
+            }
+            Self::ElfTest => {
+                include.insert(ELF_TEST_RUNNER_SHARD.to_owned());
+            }
+            Self::RustTest => {
+                include.insert(RUST_TEST_RUNNER_SHARD.to_owned());
+            }
+        }
+    }
+
+    pub fn runner_literal(&self) -> Option<cml::Name> {
+        match self {
+            Self::Elf => {
+                Some(cml::Name::new("elf".to_string()).expect("elf is always a valid name"))
+            }
+
+            // handled by shards
+            Self::ElfTest | Self::RustTest => None,
+        }
+    }
+}
diff --git a/tools/cmx2cml/src/sandbox.rs b/tools/cmx2cml/src/sandbox.rs
new file mode 100644
index 0000000..044748c
--- /dev/null
+++ b/tools/cmx2cml/src/sandbox.rs
@@ -0,0 +1,142 @@
+// Copyright 2022 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::{
+    features::CmxFeature, warnings::Warning, PROTOCOLS_FOR_HERMETIC_TESTS,
+    PROTOCOLS_FOR_SYSTEM_TESTS, SYSTEM_TEST_SHARD,
+};
+use anyhow::{bail, Context, Error};
+use serde::Deserialize;
+use std::collections::BTreeSet;
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct CmxSandbox {
+    services: Option<Vec<String>>,
+    dev: Option<Vec<String>>,
+    features: Option<Vec<CmxFeature>>,
+
+    // unsupported
+    boot: Option<Vec<String>>,
+    pkgfs: Option<Vec<String>>,
+    system: Option<Vec<String>>,
+}
+
+impl CmxSandbox {
+    pub fn uses(
+        &self,
+        warnings: &mut BTreeSet<Warning>,
+        include: &mut BTreeSet<String>,
+        injected_protocols: &BTreeSet<String>,
+        is_test: bool,
+    ) -> Result<Vec<cml::Use>, Error> {
+        if self.boot.is_some() {
+            bail!("`sandbox.boot` key is not supported for automatic conversion");
+        }
+        if self.pkgfs.is_some() {
+            bail!("`sandbox.pkgfs` key is not supported for automatic conversion");
+        }
+        if self.system.is_some() {
+            bail!("`sandbox.system` key is not supported for automatic conversion");
+        }
+
+        // accumulate protocols-from-parent separately from general uses so we can merge protocols
+        let mut uses = vec![];
+        let mut protocols_from_parent = vec![];
+
+        // add everything from sandbox.services to use-from-parent except for protocols that came
+        // from injected services, since in v1 all protocols come from the component's parent
+        // (often the sys realm)
+        if let Some(services) = &self.services {
+            for protocol in services {
+                if injected_protocols.contains(&*protocol) {
+                    continue;
+                }
+
+                let available_for_hermetic = PROTOCOLS_FOR_HERMETIC_TESTS.contains(&&**protocol);
+                let available_for_system = PROTOCOLS_FOR_SYSTEM_TESTS.contains(&&**protocol);
+
+                // if the protocol isn't available to hermetic tests, run as system test
+                if is_test && !available_for_hermetic {
+                    include.insert(SYSTEM_TEST_SHARD.to_string());
+                }
+
+                // if the protocol isn't available to system tests, warn the user
+                if is_test && !available_for_hermetic && !available_for_system {
+                    warnings.insert(Warning::TestWithUnavailableProtocol(protocol.clone()));
+                }
+
+                protocols_from_parent.push(
+                    cml::Name::new(protocol)
+                        .with_context(|| format!("parsing {protocol} into a cml name"))?,
+                );
+            }
+        }
+
+        if let Some(devices) = &self.dev {
+            warnings.insert(Warning::DeviceDirectoryBestEffort);
+            if is_test {
+                // system components will get device directories from their parent but we need to
+                // ask to be run in the system test realm to get them in tests
+                include.insert(SYSTEM_TEST_SHARD.to_string());
+            }
+
+            for device in devices {
+                // these strings are in the form `class/CLASS`, we want a directory named
+                // `dev-CLASS` mounted at a component's `/dev/class/CLASS` path
+                let (_class_literal, device_class) = device.split_once("/").with_context(|| {
+                    format!("supported devices are under /dev/class/*, got `/dev/{}`", device)
+                })?;
+                uses.push(cml::Use {
+                    directory: Some(
+                        cml::Name::new(format!("dev-{}", device_class))
+                            .with_context(|| format!("creating a `use` name for {device_class}"))?,
+                    ),
+                    rights: Some(cml::Rights(vec![cml::Right::ReadAlias])),
+                    path: Some(
+                        cml::Path::new(format!("/dev/{}", device))
+                            .with_context(|| format!("creating a `use` path for {device_class}"))?,
+                    ),
+                    ..Default::default()
+                });
+            }
+        }
+
+        if let Some(features) = &self.features {
+            for feature in features {
+                let (use_decl, warning, shard) = feature.uses(is_test)?;
+                if let Some(u) = use_decl {
+                    if u.protocol.is_some() && u.from.is_none() {
+                        match u.protocol.expect("protocol is_some was just true") {
+                            cml::OneOrMany::One(p) => protocols_from_parent.push(p),
+                            cml::OneOrMany::Many(_) => {
+                                panic!("features should only produce a single protocol to use")
+                            }
+                        }
+                    } else {
+                        uses.push(u);
+                    }
+                }
+                if let Some(w) = warning {
+                    warnings.insert(w);
+                }
+                if let Some(s) = shard {
+                    include.insert(s);
+                }
+            }
+        }
+
+        // merge all protocol uses from parent into a single use with multiple protocols, put it 1st
+        if !protocols_from_parent.is_empty() {
+            let other_uses = uses;
+            uses = vec![cml::Use {
+                protocol: Some(cml::OneOrMany::Many(protocols_from_parent)),
+                ..Default::default()
+            }];
+            uses.extend(other_uses.into_iter());
+        }
+
+        Ok(uses)
+    }
+}
diff --git a/tools/cmx2cml/src/warnings.rs b/tools/cmx2cml/src/warnings.rs
new file mode 100644
index 0000000..532ee08
--- /dev/null
+++ b/tools/cmx2cml/src/warnings.rs
@@ -0,0 +1,177 @@
+// Copyright 2022 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::{BUILD_INFO_PROTOCOL, ELF_TEST_RUNNER_SHARD};
+
+#[derive(Debug, Eq, PartialEq, PartialOrd, Ord)]
+pub enum Warning {
+    /// Users must declare any capabilities that are provided by the component
+    DeclareExpose,
+
+    /// Users might want to use gtest/gunit/rust/gotest but we don't yet support them
+    ElfTestRunnerUsed,
+
+    /// CMX had includes that we naively converted to .cml
+    IncludesRenamed,
+
+    /// Component retrieved build info through directory API and must switch to protocol.
+    BuildInfoImpl,
+
+    /// Component asks for hub feature
+    UsesHub,
+
+    /// Child needs an additional GN target added to package, possibly capabilities routed to it
+    ChildNeedsGnTargetAndRouting { child: String, gn_target: String },
+
+    /// Component must be added to the storage index
+    StorageIndex,
+
+    /// Test will require a mock for config-data
+    ConfigDataInTest,
+
+    /// Component uses device directories but we can't translate them perfectly
+    DeviceDirectoryBestEffort,
+
+    /// Test asks for a protocol that's unavailable to hermetic & system tests
+    TestWithUnavailableProtocol(String),
+}
+
+const EXPOSE_WARNING: &str = r#"
+// WARNING: Components must declare capabilities they provide to parents.
+//          Either delete or uncomment and populate these lines:
+//
+// capabilities: [
+//     "fuchsia.example.Protocol",
+// ],
+// expose: [
+//     {
+//          protocol: [ "fuchsia.example.Protocol" ],
+//          from: "self",
+//     },
+// ],"#;
+
+const ELF_TEST_RUNNER_WARNING: &str = r#"
+// NOTE: You may want to choose a test runner that understands your language's tests. See
+// https://fuchsia.dev/fuchsia-src/development/testing/components/test_runner_framework?hl=en#inventory_of_test_runners
+// for details.
+"#;
+
+const INCLUDE_RENAME_WARNING: &str = r#"
+// WARNING: These includes have been mechanically renamed from .cmx to .cml, it's possible
+// that some of them do not yet have CML equivalents. Check with authors of the v1 shards
+// if you get build errors using this manifest."#;
+
+const BUILD_INFO_WARNING: &str = r#"
+// WARNING: Build info is delivered differently in v1 & v2. See
+// https://fuchsia.dev/fuchsia-src/development/components/v2/migration/features#build-info."#;
+
+const HUB_WARNING: &str = r#"
+// WARNING: Event streams replace the hub for testing in v2. For more information:
+// https://fuchsia.dev/fuchsia-src/development/components/v2/migration/features#events"#;
+
+const STORAGE_INDEX_WARNING: &str = r#"
+// NOTE: Using persistent storage requires updating the storage index. For more details:
+// https://fuchsia.dev/fuchsia-src/development/components/v2/migration/features#update_component_storage_index"#;
+
+const CONFIG_DATA_TEST_WARNING: &str = r#"
+// NOTE: config-data in tests requires specifying the package:
+// https://fuchsia.dev/fuchsia-src/development/components/v2/migration/features?hl=en#configuration_data_in_tests
+"#;
+
+const DEVICE_DIRECTORY_WARNING: &str = r#"
+// WARNING: Device directories are converted as best-effort and may need either different rights or
+// a different directory name to function in v2."#;
+
+const UNAVAILABLE_TEST_PROTOCOL_WARNING: &str = r#"
+// WARNING: This protocol is not normally available to tests, you may need to add it to the
+// system test realm or add a mock/fake implementation as a child.
+"#;
+
+impl Warning {
+    pub fn apply(&self, lines: &mut Vec<String>) {
+        match self {
+            Warning::DeclareExpose => {
+                let use_or_end_idx =
+                    lines.iter().position(|l| l == "    use: [").unwrap_or_else(|| {
+                        lines.iter().position(|l| l == "}").expect(
+                            "generated manifests all have a closing brace on their own line",
+                        )
+                    });
+                lines.insert(use_or_end_idx, EXPOSE_WARNING.to_string());
+            }
+            Warning::ElfTestRunnerUsed => {
+                let runner_shard_idx = lines
+                    .iter()
+                    .position(|l| l.contains(ELF_TEST_RUNNER_SHARD))
+                    .expect("files with the elf test runner warning must include the shard");
+                lines.insert(runner_shard_idx, ELF_TEST_RUNNER_WARNING.to_string());
+            }
+            Warning::IncludesRenamed => {
+                let includes_idx = lines
+                    .iter()
+                    .position(|l| l.starts_with("    include: ["))
+                    .expect("files with include conversion warnings must have an include block");
+                lines.insert(includes_idx, INCLUDE_RENAME_WARNING.to_string());
+            }
+            Warning::BuildInfoImpl => {
+                let build_info_proto_idx = lines
+                    .iter()
+                    .position(|l| l.contains(BUILD_INFO_PROTOCOL))
+                    .expect("files with build info warning list the build info protocol");
+                lines.insert(build_info_proto_idx, BUILD_INFO_WARNING.to_string());
+            }
+            Warning::UsesHub => {
+                let opening_brace_idx = lines
+                    .iter()
+                    .position(|l| l == "{")
+                    .expect("all files have an opening brace on its own line");
+                lines.insert(opening_brace_idx, HUB_WARNING.to_string());
+            }
+            Warning::StorageIndex => {
+                let storage_idx = lines
+                    .iter()
+                    .position(|l| l.contains("storage: \"data\","))
+                    .expect("files with storage index warnings have a persistent data directory");
+                lines.insert(storage_idx, STORAGE_INDEX_WARNING.to_string());
+            }
+            Warning::ChildNeedsGnTargetAndRouting { child, gn_target } => {
+                let child_name_idx = lines
+                    .iter()
+                    .position(|l| l.contains("name: ") && l.contains(&*child))
+                    .expect("child warnings are only emitted for children we have");
+                lines.insert(
+                    child_name_idx,
+                    format!(
+                        r#"
+// WARNING: This child must be packaged with your component. The package should depend on:
+//     {}
+// Note that you may need to route additional capabilities to this child."#,
+                        gn_target
+                    ),
+                );
+            }
+            Warning::ConfigDataInTest => {
+                let config_data_idx = lines
+                    .iter()
+                    .position(|l| l.contains("config-data"))
+                    .expect("config-data warnings are only emitted for cml's with the use");
+                lines.insert(config_data_idx, CONFIG_DATA_TEST_WARNING.to_string());
+            }
+            Warning::DeviceDirectoryBestEffort => {
+                let dev_idx = lines
+                    .iter()
+                    .position(|l| l.contains("directory: \"dev-"))
+                    .expect("device dir warnings are only emitted for cml's with devices");
+                lines.insert(dev_idx, DEVICE_DIRECTORY_WARNING.to_string());
+            }
+            Warning::TestWithUnavailableProtocol(protocol) => {
+                let proto_idx = lines
+                    .iter()
+                    .position(|l| l.contains(&*protocol))
+                    .expect("uses must have protocol listed if we're warning about it");
+                lines.insert(proto_idx, UNAVAILABLE_TEST_PROTOCOL_WARNING.to_string());
+            }
+        }
+    }
+}
diff --git a/tools/cmx2cml/tests/BUILD.gn b/tools/cmx2cml/tests/BUILD.gn
new file mode 100644
index 0000000..85adc44
--- /dev/null
+++ b/tools/cmx2cml/tests/BUILD.gn
@@ -0,0 +1,8 @@
+# Copyright 2022 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.
+
+group("tests") {
+  testonly = true
+  deps = [ "goldens" ]
+}
diff --git a/tools/cmx2cml/tests/goldens/BUILD.gn b/tools/cmx2cml/tests/goldens/BUILD.gn
new file mode 100644
index 0000000..510643b
--- /dev/null
+++ b/tools/cmx2cml/tests/goldens/BUILD.gn
@@ -0,0 +1,121 @@
+# Copyright 2022 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.
+
+import("//build/compiled_action.gni")
+import("//build/testing/golden_file.gni")
+
+template("cmx2cml_golden") {
+  assert(defined(invoker.cmx), "must provide a `cmx` file")
+  assert(defined(invoker.runner), "must provide a `runner`")
+  _converter_name = "${target_name}_converter"
+
+  _generated_cml = "${target_out_dir}/${target_name}.cml"
+  _golden_cml = string_replace(invoker.cmx, ".cmx", ".cml.golden")
+
+  compiled_action(_converter_name) {
+    testonly = true
+    tool = "//tools/cmx2cml"
+    tool_output_name = "cmx2cml"
+    inputs = [ invoker.cmx ]
+    outputs = [ _generated_cml ]
+    args = [
+      rebase_path(invoker.cmx, root_build_dir),
+      "--runner",
+      invoker.runner,
+      "--output",
+      rebase_path(_generated_cml, root_build_dir),
+    ]
+  }
+
+  golden_file(target_name) {
+    testonly = true
+    golden = _golden_cml
+    current = _generated_cml
+    deps = [ ":$_converter_name" ]
+  }
+}
+
+cmx2cml_golden("button_checker_unittest") {
+  cmx = "button_checker_unittest.cmx"
+  runner = "elf-test"
+}
+
+cmx2cml_golden("diagnostics_persistence") {
+  cmx = "diagnostics-persistence-tests.cmx"
+  runner = "rust-test"
+}
+
+cmx2cml_golden("echo_hub") {
+  cmx = "echo_hub.cmx"
+  runner = "elf"
+}
+
+cmx2cml_golden("fasync") {
+  cmx = "fuchsia_async_lib_test.cmx"
+  runner = "rust-test"
+}
+
+cmx2cml_golden("getenv") {
+  cmx = "getenv2.cmx"
+  runner = "elf"
+}
+
+cmx2cml_golden("h265") {
+  cmx = "h265_encoder_test.cmx"
+  runner = "elf"
+}
+
+cmx2cml_golden("insntrace") {
+  cmx = "insntrace_integration_tests.cmx"
+  runner = "elf-test"
+}
+
+cmx2cml_golden("lowpan") {
+  cmx = "lowpanctl-integration-test.cmx"
+  runner = "elf-test"
+}
+
+cmx2cml_golden("pkgctl_integration_test") {
+  cmx = "pkgctl-integration-test.cmx"
+  runner = "elf-test"
+}
+
+cmx2cml_golden("power_manager_test") {
+  cmx = "power_manager_bin_test.cmx"
+  runner = "rust-test"
+}
+
+cmx2cml_golden("recovery_fdr") {
+  cmx = "system_recovery_fdr.cmx"
+  runner = "elf"
+}
+
+cmx2cml_golden("tee_manager") {
+  cmx = "tee_manager.cmx"
+  runner = "elf"
+}
+
+cmx2cml_golden("test_driver") {
+  cmx = "test_driver.cmx"
+  runner = "elf-test"
+}
+
+group("goldens") {
+  testonly = true
+  deps = [
+    ":button_checker_unittest",
+    ":diagnostics_persistence",
+    ":echo_hub",
+    ":fasync",
+    ":getenv",
+    ":h265",
+    ":insntrace",
+    ":lowpan",
+    ":pkgctl_integration_test",
+    ":power_manager_test",
+    ":recovery_fdr",
+    ":tee_manager",
+    ":test_driver",
+  ]
+}
diff --git a/tools/cmx2cml/tests/goldens/button_checker_unittest.cml.golden b/tools/cmx2cml/tests/goldens/button_checker_unittest.cml.golden
new file mode 100644
index 0000000..840a533
--- /dev/null
+++ b/tools/cmx2cml/tests/goldens/button_checker_unittest.cml.golden
@@ -0,0 +1,33 @@
+// Copyright 2022 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.
+{
+    include: [
+        // NOTE: You may want to choose a test runner that understands your language's tests. See
+        // https://fuchsia.dev/fuchsia-src/development/testing/components/test_runner_framework?hl=en#inventory_of_test_runners
+        // for details.
+        "//sdk/lib/sys/testing/elf_test_runner.shard.cml",
+        "//src/sys/test_manager/system-test.shard.cml",
+        "syslog/client.shard.cml",
+    ],
+    program: {
+        binary: "bin/button_checker_unittest_bin",
+    },
+    use: [
+        {
+            protocol: [
+                // WARNING: This protocol is not normally available to tests, you may need to add it to the
+                // system test realm or add a mock/fake implementation as a child.
+                "fuchsia.hardware.input",
+                "fuchsia.process.Launcher",
+            ],
+        },
+        {
+            // WARNING: Device directories are converted as best-effort and may need either different rights or
+            // a different directory name to function in v2.
+            directory: "dev-input",
+            rights: [ "r*" ],
+            path: "/dev/class/input",
+        },
+    ],
+}
diff --git a/tools/cmx2cml/tests/goldens/button_checker_unittest.cmx b/tools/cmx2cml/tests/goldens/button_checker_unittest.cmx
new file mode 100644
index 0000000..409194d
--- /dev/null
+++ b/tools/cmx2cml/tests/goldens/button_checker_unittest.cmx
@@ -0,0 +1,17 @@
+{
+    "include": [
+        "syslog/client.shard.cmx"
+    ],
+    "program": {
+        "binary": "bin/button_checker_unittest_bin"
+    },
+    "sandbox": {
+        "dev": [
+            "class/input"
+        ],
+        "services": [
+            "fuchsia.hardware.input",
+            "fuchsia.process.Launcher"
+        ]
+    }
+}
diff --git a/tools/cmx2cml/tests/goldens/diagnostics-persistence-tests.cml.golden b/tools/cmx2cml/tests/goldens/diagnostics-persistence-tests.cml.golden
new file mode 100644
index 0000000..5f08a89
--- /dev/null
+++ b/tools/cmx2cml/tests/goldens/diagnostics-persistence-tests.cml.golden
@@ -0,0 +1,19 @@
+// Copyright 2022 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.
+{
+    include: [
+        "//src/sys/test_runners/rust/default.shard.cml",
+        "syslog/client.shard.cml",
+    ],
+    program: {
+        binary: "bin/persistence_lib_test",
+    },
+    use: [
+        {
+            // WARNING: Build info is delivered differently in v1 & v2. See
+            // https://fuchsia.dev/fuchsia-src/development/components/v2/migration/features#build-info.
+            protocol: [ "fuchsia.buildinfo.Provider" ],
+        },
+    ],
+}
diff --git a/tools/cmx2cml/tests/goldens/diagnostics-persistence-tests.cmx b/tools/cmx2cml/tests/goldens/diagnostics-persistence-tests.cmx
new file mode 100644
index 0000000..46f42b2
--- /dev/null
+++ b/tools/cmx2cml/tests/goldens/diagnostics-persistence-tests.cmx
@@ -0,0 +1,13 @@
+{
+    "include": [
+        "syslog/client.shard.cmx"
+    ],
+    "program": {
+        "binary": "bin/persistence_lib_test"
+    },
+    "sandbox": {
+        "features": [
+            "build-info"
+        ]
+    }
+}
diff --git a/tools/cmx2cml/tests/goldens/echo_hub.cml.golden b/tools/cmx2cml/tests/goldens/echo_hub.cml.golden
new file mode 100644
index 0000000..ce1e23d
--- /dev/null
+++ b/tools/cmx2cml/tests/goldens/echo_hub.cml.golden
@@ -0,0 +1,26 @@
+// Copyright 2022 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.
+
+// WARNING: Event streams replace the hub for testing in v2. For more information:
+// https://fuchsia.dev/fuchsia-src/development/components/v2/migration/features#events
+{
+    include: [ "syslog/elf_stdio.shard.cml" ],
+    program: {
+        runner: "elf",
+        binary: "bin/echo1",
+    },
+
+    // WARNING: Components must declare capabilities they provide to parents.
+    //          Either delete or uncomment and populate these lines:
+    //
+    // capabilities: [
+    //     "fuchsia.example.Protocol",
+    // ],
+    // expose: [
+    //     {
+    //          protocol: [ "fuchsia.example.Protocol" ],
+    //          from: "self",
+    //     },
+    // ],
+}
diff --git a/tools/cmx2cml/tests/goldens/echo_hub.cmx b/tools/cmx2cml/tests/goldens/echo_hub.cmx
new file mode 100644
index 0000000..827e311
--- /dev/null
+++ b/tools/cmx2cml/tests/goldens/echo_hub.cmx
@@ -0,0 +1,13 @@
+{
+    "include": [
+        "syslog/client.shard.cmx"
+    ],
+    "program": {
+        "binary": "bin/echo1"
+    },
+    "sandbox": {
+        "features": [
+            "hub"
+        ]
+    }
+}
diff --git a/tools/cmx2cml/tests/goldens/fuchsia_async_lib_test.cml.golden b/tools/cmx2cml/tests/goldens/fuchsia_async_lib_test.cml.golden
new file mode 100644
index 0000000..00bbbe4
--- /dev/null
+++ b/tools/cmx2cml/tests/goldens/fuchsia_async_lib_test.cml.golden
@@ -0,0 +1,27 @@
+// Copyright 2022 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.
+{
+    include: [
+        "//src/sys/test_runners/rust/default.shard.cml",
+        "syslog/client.shard.cml",
+    ],
+    program: {
+        binary: "bin/fuchsia_async_lib_test",
+    },
+    children: [
+        {
+            // WARNING: This child must be packaged with your component. The package should depend on:
+            //     //src/connectivity/network/netstack:component
+            // Note that you may need to route additional capabilities to this child.
+            name: "netstack",
+            url: "#meta/netstack.cm",
+        },
+    ],
+    use: [
+        {
+            protocol: [ "fuchsia.posix.socket.Provider" ],
+            from: "#netstack",
+        },
+    ],
+}
diff --git a/tools/cmx2cml/tests/goldens/fuchsia_async_lib_test.cmx b/tools/cmx2cml/tests/goldens/fuchsia_async_lib_test.cmx
new file mode 100644
index 0000000..d639466
--- /dev/null
+++ b/tools/cmx2cml/tests/goldens/fuchsia_async_lib_test.cmx
@@ -0,0 +1,20 @@
+{
+    "facets": {
+        "fuchsia.test": {
+            "injected-services": {
+                "fuchsia.posix.socket.Provider": "fuchsia-pkg://fuchsia.com/fuchsia-async-tests#meta/netstack.cmx"
+            }
+        }
+    },
+    "include": [
+        "syslog/client.shard.cmx"
+    ],
+    "program": {
+        "binary": "bin/fuchsia_async_lib_test"
+    },
+    "sandbox": {
+        "services": [
+            "fuchsia.posix.socket.Provider"
+        ]
+    }
+}
diff --git a/tools/cmx2cml/tests/goldens/getenv2.cml.golden b/tools/cmx2cml/tests/goldens/getenv2.cml.golden
new file mode 100644
index 0000000..bb88c42
--- /dev/null
+++ b/tools/cmx2cml/tests/goldens/getenv2.cml.golden
@@ -0,0 +1,31 @@
+// Copyright 2022 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.
+{
+    include: [ "syslog/elf_stdio.shard.cml" ],
+    program: {
+        runner: "elf",
+        binary: "bin/getenv",
+        args: [
+            "FOO",
+            "BAR",
+        ],
+        environ: [
+            "BAZ=bar",
+            "FOO=bar",
+        ],
+    },
+
+    // WARNING: Components must declare capabilities they provide to parents.
+    //          Either delete or uncomment and populate these lines:
+    //
+    // capabilities: [
+    //     "fuchsia.example.Protocol",
+    // ],
+    // expose: [
+    //     {
+    //          protocol: [ "fuchsia.example.Protocol" ],
+    //          from: "self",
+    //     },
+    // ],
+}
diff --git a/tools/cmx2cml/tests/goldens/getenv2.cmx b/tools/cmx2cml/tests/goldens/getenv2.cmx
new file mode 100644
index 0000000..9104630
--- /dev/null
+++ b/tools/cmx2cml/tests/goldens/getenv2.cmx
@@ -0,0 +1,16 @@
+{
+    "include": [
+        "syslog/client.shard.cmx"
+    ],
+    "program": {
+        "args": [
+            "FOO",
+            "BAR"
+        ],
+        "binary": "bin/getenv",
+        "env_vars": [
+            "BAZ=bar",
+            "FOO=bar"
+        ]
+    }
+}
diff --git a/tools/cmx2cml/tests/goldens/h265_encoder_test.cml.golden b/tools/cmx2cml/tests/goldens/h265_encoder_test.cml.golden
new file mode 100644
index 0000000..a833024
--- /dev/null
+++ b/tools/cmx2cml/tests/goldens/h265_encoder_test.cml.golden
@@ -0,0 +1,53 @@
+// Copyright 2022 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.
+{
+    include: [
+        "//src/sys/test_manager/system-test.shard.cml",
+        "syslog/elf_stdio.shard.cml",
+    ],
+    program: {
+        runner: "elf",
+        binary: "bin/h265_encoder_test",
+        args: [ "--test-threads=1" ],
+    },
+    children: [
+        {
+            // WARNING: This child must be packaged with your component. The package should depend on:
+            //     //src/media/codec/factory:fake_codec_factory
+            // Note that you may need to route additional capabilities to this child.
+            name: "codec_factory",
+            url: "#meta/codec_factory.cm",
+        },
+    ],
+
+    // WARNING: Components must declare capabilities they provide to parents.
+    //          Either delete or uncomment and populate these lines:
+    //
+    // capabilities: [
+    //     "fuchsia.example.Protocol",
+    // ],
+    // expose: [
+    //     {
+    //          protocol: [ "fuchsia.example.Protocol" ],
+    //          from: "self",
+    //     },
+    // ],
+    use: [
+        {
+            protocol: [ "fuchsia.mediacodec.CodecFactory" ],
+            from: "#codec_factory",
+        },
+        {
+            protocol: [
+                "fuchsia.sysinfo.SysInfo",
+                "fuchsia.sysmem.Allocator",
+                "fuchsia.tracing.provider.Registry",
+            ],
+        },
+        {
+            storage: "tmp",
+            path: "/tmp",
+        },
+    ],
+}
diff --git a/tools/cmx2cml/tests/goldens/h265_encoder_test.cmx b/tools/cmx2cml/tests/goldens/h265_encoder_test.cmx
new file mode 100644
index 0000000..77e76f1
--- /dev/null
+++ b/tools/cmx2cml/tests/goldens/h265_encoder_test.cmx
@@ -0,0 +1,33 @@
+{
+    "facets": {
+        "fuchsia.test": {
+            "injected-services": {
+                "fuchsia.mediacodec.CodecFactory": "fuchsia-pkg://fuchsia.com/codec_factory#meta/codec_factory.cmx"
+            },
+            "system-services": [
+                "fuchsia.sysinfo.SysInfo",
+                "fuchsia.sysmem.Allocator"
+            ]
+        }
+    },
+    "include": [
+        "syslog/client.shard.cmx"
+    ],
+    "program": {
+        "args": [
+            "--test-threads=1"
+        ],
+        "binary": "bin/h265_encoder_test"
+    },
+    "sandbox": {
+        "features": [
+            "isolated-temp"
+        ],
+        "services": [
+            "fuchsia.mediacodec.CodecFactory",
+            "fuchsia.sysinfo.SysInfo",
+            "fuchsia.sysmem.Allocator",
+            "fuchsia.tracing.provider.Registry"
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tools/cmx2cml/tests/goldens/insntrace_integration_tests.cml.golden b/tools/cmx2cml/tests/goldens/insntrace_integration_tests.cml.golden
new file mode 100644
index 0000000..5020984
--- /dev/null
+++ b/tools/cmx2cml/tests/goldens/insntrace_integration_tests.cml.golden
@@ -0,0 +1,61 @@
+// Copyright 2022 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.
+{
+    include: [
+        // NOTE: You may want to choose a test runner that understands your language's tests. See
+        // https://fuchsia.dev/fuchsia-src/development/testing/components/test_runner_framework?hl=en#inventory_of_test_runners
+        // for details.
+        "//sdk/lib/sys/testing/elf_test_runner.shard.cml",
+        "//src/sys/test_manager/system-test.shard.cml",
+        "syslog/client.shard.cml",
+    ],
+    program: {
+        binary: "bin/insntrace_integration_tests",
+    },
+    use: [
+        {
+            protocol: [
+                "fuchsia.process.Launcher",
+
+                // WARNING: This protocol is not normally available to tests, you may need to add it to the
+                // system test realm or add a mock/fake implementation as a child.
+                "fuchsia.process.Resolver",
+
+                // WARNING: This protocol is not normally available to tests, you may need to add it to the
+                // system test realm or add a mock/fake implementation as a child.
+                "fuchsia.sys.Launcher",
+
+                // WARNING: This protocol is not normally available to tests, you may need to add it to the
+                // system test realm or add a mock/fake implementation as a child.
+                "fuchsia.tracing.kernel.Controller",
+
+                // WARNING: This protocol is not normally available to tests, you may need to add it to the
+                // system test realm or add a mock/fake implementation as a child.
+                "fuchsia.tracing.kernel.Reader",
+            ],
+        },
+        {
+            // WARNING: Device directories are converted as best-effort and may need either different rights or
+            // a different directory name to function in v2.
+            directory: "dev-cpu-trace",
+            rights: [ "r*" ],
+            path: "/dev/sys/cpu-trace",
+        },
+        {
+            // NOTE: Using persistent storage requires updating the storage index. For more details:
+            // https://fuchsia.dev/fuchsia-src/development/components/v2/migration/features#update_component_storage_index
+            storage: "data",
+            path: "/data",
+        },
+        {
+            storage: "tmp",
+            path: "/tmp",
+        },
+        {
+            directory: "bin",
+            rights: [ "r*" ],
+            path: "/bin",
+        },
+    ],
+}
diff --git a/tools/cmx2cml/tests/goldens/insntrace_integration_tests.cmx b/tools/cmx2cml/tests/goldens/insntrace_integration_tests.cmx
new file mode 100644
index 0000000..b519f34
--- /dev/null
+++ b/tools/cmx2cml/tests/goldens/insntrace_integration_tests.cmx
@@ -0,0 +1,33 @@
+{
+    "facets": {
+        "fuchsia.test": {
+            "system-services": [
+                "fuchsia.tracing.kernel.Controller",
+                "fuchsia.tracing.kernel.Reader"
+            ]
+        }
+    },
+    "include": [
+        "syslog/client.shard.cmx"
+    ],
+    "program": {
+        "binary": "bin/insntrace_integration_tests"
+    },
+    "sandbox": {
+        "dev": [
+            "sys/cpu-trace"
+        ],
+        "features": [
+            "isolated-persistent-storage",
+            "isolated-temp",
+            "shell-commands"
+        ],
+        "services": [
+            "fuchsia.process.Launcher",
+            "fuchsia.process.Resolver",
+            "fuchsia.sys.Launcher",
+            "fuchsia.tracing.kernel.Controller",
+            "fuchsia.tracing.kernel.Reader"
+        ]
+    }
+}
diff --git a/tools/cmx2cml/tests/goldens/lowpanctl-integration-test.cml.golden b/tools/cmx2cml/tests/goldens/lowpanctl-integration-test.cml.golden
new file mode 100644
index 0000000..b214de6
--- /dev/null
+++ b/tools/cmx2cml/tests/goldens/lowpanctl-integration-test.cml.golden
@@ -0,0 +1,53 @@
+// Copyright 2022 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.
+{
+    include: [
+        // NOTE: You may want to choose a test runner that understands your language's tests. See
+        // https://fuchsia.dev/fuchsia-src/development/testing/components/test_runner_framework?hl=en#inventory_of_test_runners
+        // for details.
+        "//sdk/lib/sys/testing/elf_test_runner.shard.cml",
+        "//src/sys/test_manager/system-test.shard.cml",
+        "syslog/client.shard.cml",
+    ],
+    program: {
+        binary: "bin/lowpanctl_integration_test",
+    },
+    children: [
+        {
+            // WARNING: This child must be packaged with your component. The package should depend on:
+            //     //src/connectivity/lowpan/service:lowpanservice-cv2
+            // Note that you may need to route additional capabilities to this child.
+            name: "lowpan",
+            url: "#meta/lowpanservice.cm",
+        },
+    ],
+    use: [
+        {
+            protocol: [
+                "fuchsia.factory.lowpan.FactoryLookup",
+                "fuchsia.factory.lowpan.FactoryRegister",
+                "fuchsia.lowpan.device.CountersConnector",
+                "fuchsia.lowpan.device.DeviceConnector",
+                "fuchsia.lowpan.device.DeviceExtraConnector",
+                "fuchsia.lowpan.device.EnergyScanConnector",
+                "fuchsia.lowpan.DeviceWatcher",
+                "fuchsia.lowpan.driver.Register",
+                "fuchsia.lowpan.experimental.DeviceConnector",
+                "fuchsia.lowpan.experimental.DeviceExtraConnector",
+                "fuchsia.lowpan.experimental.DeviceRouterConnector",
+                "fuchsia.lowpan.experimental.DeviceRouterExtraConnector",
+                "fuchsia.lowpan.experimental.LegacyJoiningConnector",
+                "fuchsia.lowpan.test.DeviceTestConnector",
+                "fuchsia.lowpan.thread.DatasetConnector",
+                "fuchsia.lowpan.thread.MeshcopConnector",
+            ],
+            from: "#lowpan",
+        },
+        {
+            // WARNING: This protocol is not normally available to tests, you may need to add it to the
+            // system test realm or add a mock/fake implementation as a child.
+            protocol: [ "fuchsia.sys.Launcher" ],
+        },
+    ],
+}
diff --git a/tools/cmx2cml/tests/goldens/lowpanctl-integration-test.cmx b/tools/cmx2cml/tests/goldens/lowpanctl-integration-test.cmx
new file mode 100644
index 0000000..f50ef51
--- /dev/null
+++ b/tools/cmx2cml/tests/goldens/lowpanctl-integration-test.cmx
@@ -0,0 +1,51 @@
+{
+    "facets": {
+        "fuchsia.test": {
+            "injected-services": {
+                "fuchsia.factory.lowpan.FactoryLookup": "fuchsia-pkg://fuchsia.com/lowpanservice#meta/lowpanservice.cmx",
+                "fuchsia.factory.lowpan.FactoryRegister": "fuchsia-pkg://fuchsia.com/lowpanservice#meta/lowpanservice.cmx",
+                "fuchsia.lowpan.DeviceWatcher": "fuchsia-pkg://fuchsia.com/lowpanservice#meta/lowpanservice.cmx",
+                "fuchsia.lowpan.device.CountersConnector": "fuchsia-pkg://fuchsia.com/lowpanservice#meta/lowpanservice.cmx",
+                "fuchsia.lowpan.device.DeviceConnector": "fuchsia-pkg://fuchsia.com/lowpanservice#meta/lowpanservice.cmx",
+                "fuchsia.lowpan.device.DeviceExtraConnector": "fuchsia-pkg://fuchsia.com/lowpanservice#meta/lowpanservice.cmx",
+                "fuchsia.lowpan.device.EnergyScanConnector": "fuchsia-pkg://fuchsia.com/lowpanservice#meta/lowpanservice.cmx",
+                "fuchsia.lowpan.driver.Register": "fuchsia-pkg://fuchsia.com/lowpanservice#meta/lowpanservice.cmx",
+                "fuchsia.lowpan.experimental.DeviceConnector": "fuchsia-pkg://fuchsia.com/lowpanservice#meta/lowpanservice.cmx",
+                "fuchsia.lowpan.experimental.DeviceExtraConnector": "fuchsia-pkg://fuchsia.com/lowpanservice#meta/lowpanservice.cmx",
+                "fuchsia.lowpan.experimental.DeviceRouterConnector": "fuchsia-pkg://fuchsia.com/lowpanservice#meta/lowpanservice.cmx",
+                "fuchsia.lowpan.experimental.DeviceRouterExtraConnector": "fuchsia-pkg://fuchsia.com/lowpanservice#meta/lowpanservice.cmx",
+                "fuchsia.lowpan.experimental.LegacyJoiningConnector": "fuchsia-pkg://fuchsia.com/lowpanservice#meta/lowpanservice.cmx",
+                "fuchsia.lowpan.test.DeviceTestConnector": "fuchsia-pkg://fuchsia.com/lowpanservice#meta/lowpanservice.cmx",
+                "fuchsia.lowpan.thread.DatasetConnector": "fuchsia-pkg://fuchsia.com/lowpanservice#meta/lowpanservice.cmx",
+                "fuchsia.lowpan.thread.MeshcopConnector": "fuchsia-pkg://fuchsia.com/lowpanservice#meta/lowpanservice.cmx"
+            }
+        }
+    },
+    "include": [
+        "syslog/client.shard.cmx"
+    ],
+    "program": {
+        "binary": "bin/lowpanctl_integration_test"
+    },
+    "sandbox": {
+        "services": [
+            "fuchsia.factory.lowpan.FactoryLookup",
+            "fuchsia.factory.lowpan.FactoryRegister",
+            "fuchsia.lowpan.DeviceWatcher",
+            "fuchsia.lowpan.device.CountersConnector",
+            "fuchsia.lowpan.device.DeviceConnector",
+            "fuchsia.lowpan.device.DeviceExtraConnector",
+            "fuchsia.lowpan.device.EnergyScanConnector",
+            "fuchsia.lowpan.driver.Register",
+            "fuchsia.lowpan.experimental.DeviceConnector",
+            "fuchsia.lowpan.experimental.DeviceExtraConnector",
+            "fuchsia.lowpan.experimental.DeviceRouterConnector",
+            "fuchsia.lowpan.experimental.DeviceRouterExtraConnector",
+            "fuchsia.lowpan.experimental.LegacyJoiningConnector",
+            "fuchsia.lowpan.test.DeviceTestConnector",
+            "fuchsia.lowpan.thread.DatasetConnector",
+            "fuchsia.lowpan.thread.MeshcopConnector",
+            "fuchsia.sys.Launcher"
+        ]
+    }
+}
diff --git a/tools/cmx2cml/tests/goldens/pkgctl-integration-test.cml.golden b/tools/cmx2cml/tests/goldens/pkgctl-integration-test.cml.golden
new file mode 100644
index 0000000..bb2f52c
--- /dev/null
+++ b/tools/cmx2cml/tests/goldens/pkgctl-integration-test.cml.golden
@@ -0,0 +1,74 @@
+// Copyright 2022 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.
+{
+    // WARNING: These includes have been mechanically renamed from .cmx to .cml, it's possible
+    // that some of them do not yet have CML equivalents. Check with authors of the v1 shards
+    // if you get build errors using this manifest.
+    include: [
+        // NOTE: You may want to choose a test runner that understands your language's tests. See
+        // https://fuchsia.dev/fuchsia-src/development/testing/components/test_runner_framework?hl=en#inventory_of_test_runners
+        // for details.
+        "//sdk/lib/sys/testing/elf_test_runner.shard.cml",
+        "//src/lib/fuchsia-hyper/hyper.shard.cml",
+        "//src/sys/test_manager/system-test.shard.cml",
+        "syslog/client.shard.cml",
+    ],
+    program: {
+        binary: "bin/pkgctl_integration_test",
+    },
+    children: [
+        {
+            // WARNING: This child must be packaged with your component. The package should depend on:
+            //     //src/connectivity/network/dns:component
+            // Note that you may need to route additional capabilities to this child.
+            name: "dns_resolver",
+            url: "#meta/dns-resolver.cm",
+        },
+        {
+            // WARNING: This child must be packaged with your component. The package should depend on:
+            //     //src/connectivity/network/http-client:component
+            // Note that you may need to route additional capabilities to this child.
+            name: "http_client",
+            url: "#meta/http-client.cm",
+        },
+        {
+            // WARNING: This child must be packaged with your component. The package should depend on:
+            //     //src/connectivity/network/netstack:component
+            // Note that you may need to route additional capabilities to this child.
+            name: "netstack",
+            url: "#meta/netstack.cm",
+        },
+    ],
+    use: [
+        {
+            protocol: [ "fuchsia.net.name.Lookup" ],
+            from: "#dns_resolver",
+        },
+        {
+            protocol: [ "fuchsia.net.http.Loader" ],
+            from: "#http_client",
+        },
+        {
+            protocol: [
+                "fuchsia.net.routes.State",
+                "fuchsia.posix.socket.Provider",
+            ],
+            from: "#netstack",
+        },
+        {
+            protocol: [
+                "fuchsia.sys.Environment",
+
+                // WARNING: This protocol is not normally available to tests, you may need to add it to the
+                // system test realm or add a mock/fake implementation as a child.
+                "fuchsia.sys.Launcher",
+                "fuchsia.sys.Loader",
+            ],
+        },
+        {
+            storage: "tmp",
+            path: "/tmp",
+        },
+    ],
+}
diff --git a/tools/cmx2cml/tests/goldens/pkgctl-integration-test.cmx b/tools/cmx2cml/tests/goldens/pkgctl-integration-test.cmx
new file mode 100644
index 0000000..6407aeb
--- /dev/null
+++ b/tools/cmx2cml/tests/goldens/pkgctl-integration-test.cmx
@@ -0,0 +1,30 @@
+{
+    "facets": {
+        "fuchsia.test": {
+            "injected-services": {
+                "fuchsia.net.http.Loader": "fuchsia-pkg://fuchsia.com/pkgctl-integration-tests#meta/http-client.cmx",
+                "fuchsia.net.name.Lookup": "fuchsia-pkg://fuchsia.com/pkgctl-integration-tests#meta/dns-resolver.cmx",
+                "fuchsia.net.routes.State": "fuchsia-pkg://fuchsia.com/pkgctl-integration-tests#meta/netstack.cmx",
+                "fuchsia.posix.socket.Provider": "fuchsia-pkg://fuchsia.com/pkgctl-integration-tests#meta/netstack.cmx"
+            }
+        }
+    },
+    "include": [
+        "//src/lib/fuchsia-hyper/hyper.shard.cmx",
+        "syslog/client.shard.cmx"
+    ],
+    "program": {
+        "binary": "bin/pkgctl_integration_test"
+    },
+    "sandbox": {
+        "features": [
+            "isolated-temp"
+        ],
+        "services": [
+            "fuchsia.net.http.Loader",
+            "fuchsia.sys.Environment",
+            "fuchsia.sys.Launcher",
+            "fuchsia.sys.Loader"
+        ]
+    }
+}
diff --git a/tools/cmx2cml/tests/goldens/power_manager_bin_test.cml.golden b/tools/cmx2cml/tests/goldens/power_manager_bin_test.cml.golden
new file mode 100644
index 0000000..3c91912
--- /dev/null
+++ b/tools/cmx2cml/tests/goldens/power_manager_bin_test.cml.golden
@@ -0,0 +1,25 @@
+// Copyright 2022 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.
+{
+    include: [
+        "//src/sys/test_manager/system-test.shard.cml",
+        "//src/sys/test_runners/rust/default.shard.cml",
+        "syslog/client.shard.cml",
+    ],
+    program: {
+        binary: "bin/power_manager_bin_test",
+    },
+    use: [
+        {
+            protocol: [ "fuchsia.sys.Environment" ],
+        },
+        {
+            // NOTE: config-data in tests requires specifying the package:
+            // https://fuchsia.dev/fuchsia-src/development/components/v2/migration/features?hl=en#configuration_data_in_tests
+            directory: "config-data",
+            rights: [ "r*" ],
+            path: "/config/data",
+        },
+    ],
+}
diff --git a/tools/cmx2cml/tests/goldens/power_manager_bin_test.cmx b/tools/cmx2cml/tests/goldens/power_manager_bin_test.cmx
new file mode 100644
index 0000000..00a1d67
--- /dev/null
+++ b/tools/cmx2cml/tests/goldens/power_manager_bin_test.cmx
@@ -0,0 +1,16 @@
+{
+    "include": [
+        "syslog/client.shard.cmx"
+    ],
+    "program": {
+        "binary": "bin/power_manager_bin_test"
+    },
+    "sandbox": {
+        "features": [
+            "config-data"
+        ],
+        "services": [
+            "fuchsia.sys.Environment"
+        ]
+    }
+}
diff --git a/tools/cmx2cml/tests/goldens/system_recovery_fdr.cml.golden b/tools/cmx2cml/tests/goldens/system_recovery_fdr.cml.golden
new file mode 100644
index 0000000..90d02b0
--- /dev/null
+++ b/tools/cmx2cml/tests/goldens/system_recovery_fdr.cml.golden
@@ -0,0 +1,66 @@
+// Copyright 2022 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.
+{
+    // WARNING: These includes have been mechanically renamed from .cmx to .cml, it's possible
+    // that some of them do not yet have CML equivalents. Check with authors of the v1 shards
+    // if you get build errors using this manifest.
+    include: [
+        "//src/lib/fuchsia-hyper/hyper.shard.cml",
+        "syslog/elf_stdio.shard.cml",
+    ],
+    program: {
+        runner: "elf",
+        binary: "bin/system_recovery_fdr",
+        args: [
+            "--rotation",
+            "90",
+        ],
+    },
+
+    // WARNING: Components must declare capabilities they provide to parents.
+    //          Either delete or uncomment and populate these lines:
+    //
+    // capabilities: [
+    //     "fuchsia.example.Protocol",
+    // ],
+    // expose: [
+    //     {
+    //          protocol: [ "fuchsia.example.Protocol" ],
+    //          from: "self",
+    //     },
+    // ],
+    use: [
+        {
+            protocol: [
+                "fuchsia.process.Launcher",
+                "fuchsia.recovery.FactoryReset",
+                "fuchsia.recovery.policy.FactoryReset",
+                "fuchsia.sys.Launcher",
+                "fuchsia.sysmem.Allocator",
+            ],
+        },
+        {
+            // WARNING: Device directories are converted as best-effort and may need either different rights or
+            // a different directory name to function in v2.
+            directory: "dev-display-controller",
+            rights: [ "r*" ],
+            path: "/dev/class/display-controller",
+        },
+        {
+            directory: "dev-input",
+            rights: [ "r*" ],
+            path: "/dev/class/input",
+        },
+        {
+            directory: "dev-input-report",
+            rights: [ "r*" ],
+            path: "/dev/class/input-report",
+        },
+        {
+            directory: "dev-platform",
+            rights: [ "r*" ],
+            path: "/dev/sys/platform",
+        },
+    ],
+}
diff --git a/tools/cmx2cml/tests/goldens/system_recovery_fdr.cmx b/tools/cmx2cml/tests/goldens/system_recovery_fdr.cmx
new file mode 100644
index 0000000..008f833
--- /dev/null
+++ b/tools/cmx2cml/tests/goldens/system_recovery_fdr.cmx
@@ -0,0 +1,28 @@
+{
+    "include": [
+        "syslog/client.shard.cmx",
+        "//src/lib/fuchsia-hyper/hyper.shard.cmx"
+    ],
+    "program": {
+        "args": [
+            "--rotation",
+            "90"
+        ],
+        "binary": "bin/system_recovery_fdr"
+    },
+    "sandbox": {
+        "dev": [
+            "class/display-controller",
+            "class/input",
+            "class/input-report",
+            "sys/platform"
+        ],
+        "services": [
+            "fuchsia.process.Launcher",
+            "fuchsia.recovery.FactoryReset",
+            "fuchsia.recovery.policy.FactoryReset",
+            "fuchsia.sys.Launcher",
+            "fuchsia.sysmem.Allocator"
+        ]
+    }
+}
diff --git a/tools/cmx2cml/tests/goldens/tee_manager.cml.golden b/tools/cmx2cml/tests/goldens/tee_manager.cml.golden
new file mode 100644
index 0000000..b832ee8
--- /dev/null
+++ b/tools/cmx2cml/tests/goldens/tee_manager.cml.golden
@@ -0,0 +1,43 @@
+// Copyright 2022 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.
+{
+    include: [ "syslog/elf_stdio.shard.cml" ],
+    program: {
+        runner: "elf",
+        binary: "bin/tee_manager",
+    },
+
+    // WARNING: Components must declare capabilities they provide to parents.
+    //          Either delete or uncomment and populate these lines:
+    //
+    // capabilities: [
+    //     "fuchsia.example.Protocol",
+    // ],
+    // expose: [
+    //     {
+    //          protocol: [ "fuchsia.example.Protocol" ],
+    //          from: "self",
+    //     },
+    // ],
+    use: [
+        {
+            // WARNING: Device directories are converted as best-effort and may need either different rights or
+            // a different directory name to function in v2.
+            directory: "dev-tee",
+            rights: [ "r*" ],
+            path: "/dev/class/tee",
+        },
+        {
+            directory: "config-data",
+            rights: [ "r*" ],
+            path: "/config/data",
+        },
+        {
+            // NOTE: Using persistent storage requires updating the storage index. For more details:
+            // https://fuchsia.dev/fuchsia-src/development/components/v2/migration/features#update_component_storage_index
+            storage: "data",
+            path: "/data",
+        },
+    ],
+}
diff --git a/tools/cmx2cml/tests/goldens/tee_manager.cmx b/tools/cmx2cml/tests/goldens/tee_manager.cmx
new file mode 100644
index 0000000..244cad4
--- /dev/null
+++ b/tools/cmx2cml/tests/goldens/tee_manager.cmx
@@ -0,0 +1,17 @@
+{
+    "include": [
+        "syslog/client.shard.cmx"
+    ],
+    "program": {
+        "binary": "bin/tee_manager"
+    },
+    "sandbox": {
+        "dev": [
+            "class/tee"
+        ],
+        "features": [
+            "config-data",
+            "isolated-persistent-storage"
+        ]
+    }
+}
diff --git a/tools/cmx2cml/tests/goldens/test_driver.cml.golden b/tools/cmx2cml/tests/goldens/test_driver.cml.golden
new file mode 100644
index 0000000..621cfb2
--- /dev/null
+++ b/tools/cmx2cml/tests/goldens/test_driver.cml.golden
@@ -0,0 +1,25 @@
+// Copyright 2022 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.
+{
+    include: [
+        // NOTE: You may want to choose a test runner that understands your language's tests. See
+        // https://fuchsia.dev/fuchsia-src/development/testing/components/test_runner_framework?hl=en#inventory_of_test_runners
+        // for details.
+        "//sdk/lib/sys/testing/elf_test_runner.shard.cml",
+        "//src/sys/test_manager/system-test.shard.cml",
+        "syslog/client.shard.cml",
+    ],
+    program: {
+        binary: "bin/test_driver",
+    },
+    use: [
+        {
+            // WARNING: Device directories are converted as best-effort and may need either different rights or
+            // a different directory name to function in v2.
+            directory: "dev-usb-device",
+            rights: [ "r*" ],
+            path: "/dev/class/usb-device",
+        },
+    ],
+}
diff --git a/tools/cmx2cml/tests/goldens/test_driver.cmx b/tools/cmx2cml/tests/goldens/test_driver.cmx
new file mode 100644
index 0000000..20cbf10
--- /dev/null
+++ b/tools/cmx2cml/tests/goldens/test_driver.cmx
@@ -0,0 +1,13 @@
+{
+    "include": [
+        "syslog/client.shard.cmx"
+    ],
+    "program": {
+        "binary": "bin/test_driver"
+    },
+    "sandbox": {
+        "dev": [
+            "class/usb-device"
+        ]
+    }
+}
diff --git a/tools/cmx2cml/try_convert_all_cmx.sh b/tools/cmx2cml/try_convert_all_cmx.sh
new file mode 100755
index 0000000..0d0f691
--- /dev/null
+++ b/tools/cmx2cml/try_convert_all_cmx.sh
@@ -0,0 +1,223 @@
+#!/bin/sh
+# Copyright 2022 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.
+
+# list of CMX files we know we can't yet autoconvert
+exceptions=(\
+    # dart/flutter components
+    "./build/dart/tests/meta/dart-component.cmx"
+    "./build/flutter/tests/meta/flutter-component-with-flutter-driver.cmx"
+    "./build/flutter/tests/meta/flutter-component.cmx"
+    "./sdk/dart/fuchsia_inspect/meta/dart-archive-reader-test.cmx"
+    "./sdk/dart/fuchsia_inspect/test/inspect_flutter_integration_tester/meta/inspect_dart_integration_test_driver.cmx"
+    "./sdk/dart/fuchsia_inspect/test/inspect_flutter_integration_tester/meta/inspect_flutter_integration_tester.cmx"
+    "./sdk/dart/fuchsia_inspect/test/validator_puppet/meta/inspect_validator_test_dart.cmx"
+    "./sdk/dart/fuchsia_modular_testing/meta/fuchsia-modular-testing-integration-tests.cmx"
+    "./sdk/dart/fuchsia_modular/meta/fuchsia-modular-integration-tests.cmx"
+    "./sdk/dart/fuchsia_services/meta/fuchsia-services-integration-tests.cmx"
+    "./sdk/dart/fuchsia_services/test_support/meta/fuchsia-services-foo-test-server.cmx"
+    "./sdk/dart/zircon/meta/zircon_unittests.cmx"
+    "./src/experiences/benchmarks/bin/button_flutter/meta/button_flutter.cmx"
+    "./src/experiences/benchmarks/bin/clockface_flutter/meta/clockface-flutter.cmx"
+    "./src/experiences/benchmarks/bin/gamma_flutter/meta/gamma-flutter.cmx"
+    "./src/experiences/benchmarks/bin/scroll_flutter/meta/scroll-flutter.cmx"
+    "./src/experiences/bin/ermine_testserver/meta/ermine_testserver.cmx"
+    "./src/experiences/bin/settings/license/meta/license_settings.cmx"
+    "./src/experiences/bin/settings/meta/settings.cmx"
+    "./src/experiences/examples/localized_flutter/localized_flutter_app/meta/localized_flutter_app.cmx"
+    "./src/experiences/examples/spinning_cube/meta/spinning_cube.cmx"
+    "./src/flutter/tests/bin/null-safe-enabled-flutter/meta/null-safe-enabled-flutter.cmx"
+    "./src/flutter/tests/bin/pingable-flutter-component/meta/pingable-flutter-component-debug-build-cfg.cmx"
+    "./src/flutter/tests/bin/pingable-flutter-component/meta/pingable-flutter-component-profile-build-cfg.cmx"
+    "./src/flutter/tests/bin/pingable-flutter-component/meta/pingable-flutter-component-release-build-cfg.cmx"
+    "./src/lib/diagnostics/inspect/dart/bench/meta/dart-inspect-benchmarks.cmx"
+    "./src/lib/fidl/dart/gidl/meta/gidl-library-test.cmx"
+    "./src/tests/benchmarks/fidl/dart/meta/dart-fidl-benchmarks.cmx"
+    "./src/tests/fidl/compatibility/meta/dart-server.cmx"
+    "./src/tests/fidl/dart_bindings_test/meta/conformance.cmx"
+    "./src/tests/fidl/dart_bindings_test/meta/dart-bindings-test.cmx"
+    "./src/tests/fidl/dart_bindings_test/meta/server.cmx"
+    "./src/ui/tests/integration_flutter_tests/embedder/child-view/meta/child-view.cmx"
+    "./src/ui/tests/integration_flutter_tests/embedder/parent-view/meta/parent-view-disabled-hittest.cmx"
+    "./src/ui/tests/integration_flutter_tests/embedder/parent-view/meta/parent-view-show-overlay.cmx"
+    "./src/ui/tests/integration_flutter_tests/embedder/parent-view/meta/parent-view.cmx"
+
+    # these are shards, not for converting
+    "./build/dart/meta/aot_product_runtime.cmx"
+    "./build/dart/meta/aot_runtime.cmx"
+    "./build/dart/meta/jit_product_runtime.cmx"
+    "./build/dart/meta/jit_runtime.cmx"
+    "./build/flutter/meta/aot_product_runtime.cmx"
+    "./build/flutter/meta/aot_runtime.cmx"
+    "./build/flutter/meta/jit_product_runtime.cmx"
+    "./build/flutter/meta/jit_runtime.cmx"
+
+    # uses shards to have a valid minimal v1 manifest
+    "./src/ui/bin/terminal/meta/vsh-terminal.cmx"
+
+    # manually specifies a runner package
+    "./src/virtualization/packages/meta/guest_package.cmx"
+
+    # passes args to injected services
+    "./src/connectivity/lowpan/drivers/lowpan-spinel-driver/tests/test-components/meta/lowpan-driver-provision-mock.cmx"
+    "./src/connectivity/openthread/tests/test-components/meta/ot-radio-ncp-ver-query-mock.cmx"
+    "./src/connectivity/openthread/tests/test-components/meta/ot-stack-soft-reset-mock.cmx"
+    "./src/connectivity/telephony/tests/meta/tel_fake_at_driver_test.cmx"
+    "./src/connectivity/telephony/tests/meta/tel_fake_at_query.cmx"
+    "./src/connectivity/telephony/tests/meta/tel_fake_qmi_query.cmx"
+    "./src/connectivity/telephony/tests/meta/tel_snooper_multi_clients.cmx"
+    "./src/connectivity/telephony/tests/meta/telephony-qmi-usb-integration-test.cmx"
+    "./src/developer/tracing/bin/trace/tests/meta/detach_tests.cmx"
+    "./src/developer/tracing/bin/trace/tests/meta/ktrace_integration_tests.cmx"
+    "./src/developer/tracing/bin/trace/tests/meta/provider_destruction_tests.cmx"
+    "./src/developer/tracing/bin/trace/tests/meta/return_child_result_tests.cmx"
+    "./src/developer/tracing/bin/trace/tests/meta/shared_provider_integration_tests.cmx"
+    "./src/developer/tracing/bin/trace/tests/meta/trace_integration_tests.cmx"
+    "./src/factory/fake_factory_store_providers/meta/fake_factory_store_providers_test.cmx"
+    "./src/intl/intl_services/tests/meta/intl_services_test.cmx"
+    "./src/performance/bin/cpuperf_provider/meta/cpuperf_provider_integration_tests.cmx"
+    "./src/sys/sysmgr/integration_tests/meta/service_startup_test.cmx"\
+
+    # does not have a registered v2 child for an injected service replacement
+    "./src/chromium/web_runner_tests/meta/web_runner_integration_tests.cmx"
+    "./src/chromium/web_runner_tests/meta/web_runner_pixel_tests.cmx"
+    "./src/diagnostics/persistence/tests/meta/canonical-diagnostics-persistence-test.cmx"
+    "./src/diagnostics/validator/inspect/meta/validator.cmx"
+    "./src/factory/fake_factory_items/meta/fake_factory_items_test.cmx"
+    "./src/lib/fake-clock/examples/rust/meta/test.cmx"
+    "./src/lib/fake-clock/lib/meta/fake_clock_lib_test.cmx"
+    "./src/media/codec/examples/use_media_decoder/meta/use_h264_decoder_secure_input_output_test.cmx"
+    "./src/media/codec/examples/use_media_decoder/meta/use_vp9_decoder_secure_input_output_test.cmx"
+    "./src/recovery/system/meta/system_installer_bin_test.cmx"
+    "./src/recovery/system/meta/system_recovery_bin_test.cmx"
+    "./src/security/codelab/exploit/meta/security-codelab.cmx"
+    "./src/security/codelab/smart_door/meta/smart-door-functional-test.cmx"
+    "./src/security/codelab/solution/meta/security-codelab.cmx"
+    "./src/security/kms_test_client/meta/kms_test_client.cmx"
+    "./src/sys/pkg/lib/pkgfs/meta/pkgfs-lib-test.cmx"
+    "./src/sys/pkg/testing/pkgfs-ramdisk/meta/pkgfs-ramdisk-lib-test.cmx"
+    "./src/sys/test_runners/legacy_test/test_data/echo_test/meta/echo_test.cmx"
+
+    # requests boot in its sandbox
+    "./src/connectivity/openthread/ot-stack/meta/ot-stack.cmx"
+    "./src/developer/debug/debug_agent/meta/debug_agent.cmx"
+    "./src/developer/sched/meta/sched_tests.cmx"
+    "./src/devices/bin/driver_host/meta/driver_host_test.cmx"
+    "./src/devices/tests/bind-fail-test/meta/bind-fail-test.cmx"
+    "./src/devices/tests/ddk-instance-lifecycle-test/meta/ddk-instance-lifecycle-test.cmx"
+    "./src/devices/tests/ddk-metadata-test/meta/ddk-metadata-test.cmx"
+    "./src/devices/tests/driver-inspect-test/meta/driver-inspect-test.cmx"
+    "./src/lib/process/meta/process_unittests.cmx"
+    "./src/ui/bin/terminal/meta/terminal_core.cmx"
+    "./src/ui/bin/terminal/meta/terminal.cmx"
+
+    # requests pkgfs in its sandbox
+    "./src/connectivity/lowpan/drivers/lowpan-spinel-driver/meta/lowpan-spinel-driver.cmx"
+    "./src/connectivity/openthread/tests/test-components/meta/ot-radio-ncp-ver-query.cmx"
+    "./src/connectivity/openthread/tests/test-components/meta/ot-stack-ncp-ver-query-mock.cmx"
+    "./src/connectivity/openthread/tests/test-components/meta/ot-stack-ncp-ver-query.cmx"
+    "./src/connectivity/openthread/tests/test-components/meta/ot-stack-soft-reset.cmx"
+    "./src/connectivity/telephony/telephony/meta/telephony_bin_test.cmx"
+    "./src/sys/appmgr/integration_tests/lifecycle/meta/appmgr-lifecycle-tests.cmx"
+    "./src/sys/appmgr/integration_tests/meta/appmgr_full_integration_tests.cmx"
+    "./src/sys/appmgr/integration_tests/meta/failing_appmgr.cmx"
+    "./src/sys/appmgr/integration_tests/policy/meta/pkgfs_versions.cmx"
+    "./src/sys/appmgr/integration_tests/sandbox/meta/path-traversal-escape-child.cmx"
+    "./src/sys/pkg/bin/system-update-checker/meta/system-update-checker.cmx"
+    "./src/sys/sysmgr/integration_tests/meta/package_updating_loader_test.cmx"
+
+    # requests system in its sandbox
+    "./src/devices/bus/drivers/pci/test/meta/pci-driver-test.cmx"
+    "./src/sys/sysmgr/meta/sysmgr.cmx"
+
+    # uses vulkan feature
+    "./scripts/sdk/gn/test_project/tests/package/meta/part2.cmx"
+
+    # uses deprecated-ambient-replace-as-executable
+    "./scripts/sdk/gn/test_project/chromium_compat/context_provider.cmx"
+    "./sdk/dart/fuchsia_inspect/test/integration/meta/dart_inspect_vmo_test.cmx"
+    "./src/sys/appmgr/integration_tests/policy/meta/deprecated_ambient_replace_as_exec.cmx"
+    "./src/sys/appmgr/integration_tests/sandbox/features/ambient-executable-policy/meta/has_ambient_executable.cmx"
+    "./src/sys/component_manager/tests/security_policy/ambient_mark_vmo_exec/meta/cm_for_test.cmx"
+    "./src/sys/component_manager/tests/security_policy/capability_allowlist/meta/cm_for_test.cmx"
+    "./src/sys/component_manager/tests/security_policy/main_process_critical/meta/cm_for_test.cmx"
+
+    # uses deprecated-shell
+    "./src/devices/tests/devfs/meta/devfs-test.cmx"
+    "./src/sys/appmgr/integration_tests/components/meta/echo_deprecated_shell.cmx"
+    "./src/sys/appmgr/integration_tests/policy/meta/deprecated_shell.cmx"
+    "./src/sys/appmgr/integration_tests/sandbox/features/shell/meta/has_deprecated_shell.cmx"
+    "./src/sys/run_test_component/test/meta/run_test_component_test.cmx"
+
+    # uses deprecated-global-dev
+    "./src/sys/appmgr/integration_tests/sandbox/features/deprecated-global-dev/meta/has_deprecated_global_dev.cmx"
+    "./src/virtualization/bin/linux_runner/meta/linux_runner.cmx"
+
+    # uses /dev/zero
+    "./sdk/lib/fdio/tests/meta/fdio_test.cmx"
+
+    # uses /dev/null
+    "./src/diagnostics/archivist/tests/logs/go/meta/logs_integration_go_tests.cmx"
+
+    # uses device outside /dev/class/*
+    "./src/factory/factory_store_providers/meta/factory_store_providers.cmx"
+    "./src/graphics/drivers/msd-vsi-vip/tests/integration/meta/msd_vsi_vip_integration_tests.cmx"
+    "./src/performance/lib/perfmon/meta/perfmon_tests.cmx"
+    "./src/zircon/bin/hwstress/meta/hwstress.cmx"
+
+    # uses device name we can't parse properly
+    "./src/graphics/drivers/msd-intel-gen/tests/meta/msd_intel_gen_integration_tests.cmx"
+    "./src/graphics/tests/goldfish_test/meta/goldfish_test.cmx"
+    "./src/graphics/tests/vkloop/meta/vkloop.cmx"
+    "./src/media/drivers/amlogic_decoder/tests/runner/meta/amlogic_decoder_integration_tests.cmx"
+    "./src/recovery/system/meta/system_recovery_installer.cmx"
+    "./src/recovery/system/meta/system_recovery.cmx"
+    "./src/sys/pkg/bin/pkgfs/meta/pmd_pkgfs_test.cmx"
+    "./src/sys/pkg/lib/isolated-swd/meta/isolated-swd-tests.cmx"
+    "./src/sys/pkg/tests/isolated-ota/meta/isolated-ota-integration-test.cmx"
+    "./src/sys/pkg/tests/usb-system-update-checker/meta/usb-system-update-checker-integration-test.cmx"
+
+    # cmx file is intentionally invalid
+    "./scripts/sdk/gn/test_project/tests/package/meta/invalid.cmx"
+    "./src/modular/tests/meta/module_with_fake_runner.cmx"
+    "./src/sys/appmgr/integration_tests/mock_runner/fake_component/meta/fake_component.cmx"
+)
+
+# find all the CMXes we think we might be able to convert automatically
+output="$FUCHSIA_DIR/out/default/cmxes.list"
+find . -type f -name '*.cmx' \
+    -not -path "./examples/*" \
+    -not -path "./out/*" \
+    -not -path "./prebuilt/*" \
+    -not -path "./third_party/*" \
+    -not -path "./tools/cmx2cml/*" \
+    -not -path "./vendor/*" > $output
+
+# remove any files which we know we can't convert
+tempfile=$(mktemp)
+for exception in ${exceptions[@]}; do
+    grep -v "$exception" $output > $tempfile
+    mv $tempfile $output
+done
+
+sort $output > $tempfile
+mv $tempfile $output
+
+# filter out any files which would generate CML that already exist
+echo "found $(wc -l < "$output") cmx files to possibly convert..."
+echo -n > $tempfile
+for f in $(cat $output); do
+    cml="${f%.cmx}.cml"
+    if ! $(git ls-files --error-unmatch $cml &> /dev/null); then
+        echo $f >> $tempfile
+    fi
+done
+mv $tempfile $output
+
+echo "converting $(wc -l < "$output") cmx files which don't already have CML equivalents..."
+
+# run the conversion, assuming the runner since it mostly doesn't affect the output
+fx cmx2cml --runner elf-test -f $output
+
+echo "done!"
diff --git a/tools/lib/cml/src/lib.rs b/tools/lib/cml/src/lib.rs
index 1c0f8c7a..94dba35 100644
--- a/tools/lib/cml/src/lib.rs
+++ b/tools/lib/cml/src/lib.rs
@@ -16,7 +16,6 @@
 
 use {
     crate::error::Error,
-    crate::one_or_many::OneOrMany,
     cml_macro::{CheckedVec, OneOrMany, Reference},
     fidl_fuchsia_io as fio,
     json5format::{FormatOptions, PathOption},
@@ -41,7 +40,7 @@
     RelativePath, StartupMode, StorageId, Url,
 };
 
-pub use crate::translate::compile;
+pub use crate::{one_or_many::OneOrMany, translate::compile};
 
 lazy_static! {
     static ref DEFAULT_EVENT_STREAM_NAME: Name = "EventStream".parse().unwrap();