[fx][create] Add a tool that creates scaffolding for projects

fx create is a tool that generates project scaffolding so that
developers can get up and running quickly without remembering
all the boiler-plate necessary to stand up a project on Fuchsia.

This first commit supports v2 components, but v1 component support
will be added in a follow up CL to prove that no changes need to
happen in the Rust code to add new project types.

Usage: fx create component-v2 my-project --lang rust

Change-Id: Ifbeca5eb68532aec51db4d71f34960f9bf34556d
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/375833
Reviewed-by: Adam Barth <abarth@google.com>
Reviewed-by: Adam Lesinski <adamlesinski@google.com>
Reviewed-by: Gary Bressler <geb@google.com>
Reviewed-by: Adam Perry <adamperry@google.com>
Commit-Queue: Adam Lesinski <adamlesinski@google.com>
Testability-Review: Adam Perry <adamperry@google.com>
diff --git a/tools/BUILD.gn b/tools/BUILD.gn
index 95d76e42..78e2776 100644
--- a/tools/BUILD.gn
+++ b/tools/BUILD.gn
@@ -10,6 +10,7 @@
     "//tools/bindc:host",
     "//tools/blackout:all",
     "//tools/build",
+    "//tools/create",
     "//tools/debroot($host_toolchain)",
     "//tools/fidl/fidldoc($host_toolchain)",
     "//tools/fidlcat:fidlcat_host",
@@ -59,6 +60,7 @@
     "//tools/bootserver:tests($host_toolchain)",
     "//tools/botanist:tests($host_toolchain)",
     "//tools/build:tests($host_toolchain)",
+    "//tools/create:tests($host_toolchain)",
     "//tools/debug:tests($host_toolchain)",
     "//tools/fidlcat:fidlcat_host_tests($host_toolchain)",
     "//tools/fidlcat/tests",
diff --git a/tools/create/BUILD.gn b/tools/create/BUILD.gn
new file mode 100644
index 0000000..47ed5f3
--- /dev/null
+++ b/tools/create/BUILD.gn
@@ -0,0 +1,57 @@
+# Copyright 2020 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/host.gni")
+import("//build/rust/rustc_binary.gni")
+
+if (host_toolchain == current_toolchain) {
+  rustc_binary("create_bin") {
+    name = "create"
+    deps = [
+      "//third_party/rust_crates:anyhow",
+      "//third_party/rust_crates:chrono",
+      "//third_party/rust_crates:handlebars",
+      "//third_party/rust_crates:heck",
+      "//third_party/rust_crates:serde",
+      "//third_party/rust_crates:serde_derive",
+      "//third_party/rust_crates:serde_json",
+      "//third_party/rust_crates:structopt",
+      "//third_party/rust_crates:tempfile",
+      "//third_party/rust_crates:termion",
+    ]
+    with_unit_tests = true
+  }
+
+  copy("copy_templates") {
+    sources = [
+      "templates/component-v2/BUILD.gn.tmpl-cpp",
+      "templates/component-v2/BUILD.gn.tmpl-rust",
+      "templates/component-v2/\$.cc.tmpl-cpp",
+      "templates/component-v2/\$.h.tmpl-cpp",
+      "templates/component-v2/\$_test.cc.tmpl-cpp",
+      "templates/component-v2/main.cc.tmpl-cpp",
+      "templates/component-v2/meta/\$.cml.tmpl",
+      "templates/component-v2/meta/\$_test.cml.tmpl-rust",
+      "templates/component-v2/meta/\$_unittests.cml.tmpl-cpp",
+      "templates/component-v2/src/main.rs.tmpl-rust",
+    ]
+    outputs =
+        [ "${host_tools_dir}/create_templates/{{source_target_relative}}" ]
+  }
+}
+
+install_host_tools("create") {
+  deps = [
+    ":copy_templates($host_toolchain)",
+    ":create_bin($host_toolchain)",
+  ]
+
+  outputs = [ "create" ]
+}
+
+group("tests") {
+  testonly = true
+
+  deps = [ ":create_bin_test($host_toolchain)" ]
+}
diff --git a/tools/create/OWNERS b/tools/create/OWNERS
new file mode 100644
index 0000000..b81ae3a
--- /dev/null
+++ b/tools/create/OWNERS
@@ -0,0 +1,2 @@
+adamlesinski@google.com
+adamperry@google.com
diff --git a/tools/create/README.md b/tools/create/README.md
new file mode 100644
index 0000000..7259480
--- /dev/null
+++ b/tools/create/README.md
@@ -0,0 +1,73 @@
+# fx create
+
+The `fx create` command generates scaffolding for new projects. See `fx create --help` for
+usage details.
+
+## Adding a new project type
+
+1. Add a new directory with the project type name under `//tools/create/templates/`.
+2. Populate the directory with the intended project structure
+    * Name all template files with the `.tmpl` extension or `.tmpl-<lang>` for
+      language-specific template files.
+    * Use `$` in a file/directory name to substitute the user's `PROJECT_NAME`.
+3. Edit the `copy_templates` target in `//tools/create/BUILD.gn` to include all your new
+   template files.
+4. Add the project type to the help doc-string in `CreateArgs` in `//tools/create/src/main.rs`.
+
+## Templates
+
+### Layout
+
+Each top-level directory in `//tools/create/templates` corresponds to a project type
+of the same name.
+
+Files with the `.tmpl` and `.tmpl-*` extensions in these directories are [handlebars] templates.
+Template expansion is performed on the templates and the directory structure is replicated in
+the new project directory.
+
+Templates with the `.tmpl` extension are language agnostic.
+
+Templates with the `.tmpl-<lang>` extension are expanded only when the user supplies the
+`--lang=<lang>` flag.
+
+Multiple languages can be supported in the same template directory. For instance, the
+`component-v2` command supports `cpp` and `rust` languages by having both a `BUILD.gn.tmpl-cpp`
+and `BUILD.gn.tmpl-rust` file.
+
+### Variables
+
+The template expansion uses [handlebars] syntax. Expand a variable with the syntax `{{VAR_NAME}}`.
+
+The available variable names are:
+
+* `COPYRIGHT_YEAR`: Today's year, eg. 2020, for use in copyright headers,
+* `PROJECT_NAME`: The user-specified project name,
+* `PROJECT_PATH`: The path from the fuchsia root directory to the new project,
+* `PROJECT_TYPE`: The project-type as specified on the command line, e.g: 'component-v2'.
+
+Path names are treated differently. If a `$` character is encountered in a template path, it is
+substituted with the `PROJECT_NAME` variable expansion.
+
+Eg. `component/meta/$.cml.tmpl` with `PROJECT_NAME="foo"` expands to `meta/foo.cml`.
+
+#### Adding a new variable
+
+To make a new variable accessible to templates, add it to `TemplateArgs` in
+`//tools/create/src/main.rs`.
+
+### Helpers
+
+Helper functions can be invoked with the syntax `{{helper VAR_NAME}}`.
+
+The available helper functions are:
+
+* `pascal_case`: Converts a string argument into *P*ascal*C*ase,
+* `snake_case`: Converts a string argument into snake*_*case,
+* `screaming_snake_case`: Converts a string argument into SCREAMING_SNAKE_CASE,
+
+#### Adding a new helper
+
+To make a new helper function accessible to templates, follow instructions in
+`//tools/create/src/tmpl_helpers.rs`.
+
+[handlebars]: https://docs.rs/handlebars
diff --git a/tools/create/src/main.rs b/tools/create/src/main.rs
new file mode 100644
index 0000000..3c813a9
--- /dev/null
+++ b/tools/create/src/main.rs
@@ -0,0 +1,365 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+mod tmpl_helpers;
+mod util;
+
+use anyhow::{anyhow, bail};
+use chrono::{Datelike, Utc};
+use handlebars::Handlebars;
+use serde_derive::Serialize;
+use std::collections::HashMap;
+use std::path::Path;
+use std::str::FromStr;
+use std::{env, fmt, fs, io};
+use structopt::StructOpt;
+use tempfile::tempdir;
+use termion::{color, style};
+
+fn main() -> Result<(), anyhow::Error> {
+    let args = CreateArgs::from_args();
+    if args.project_name.contains("_") {
+        bail!("project-name cannot contain underscores");
+    }
+    let templates_dir_path = util::get_templates_dir_path()?;
+    if !util::dir_contains(&templates_dir_path, &args.project_type)? {
+        bail!("unrecognized project type \"{}\"", &args.project_type);
+    }
+    let project_template_path = templates_dir_path.join(&args.project_type);
+
+    // Collect the template files for this project type and language.
+    let templates = TemplateTree::from_dir(&project_template_path, &args.lang)?;
+
+    // Create the set of variables accessible to template files.
+    let tmpl_args = TemplateArgs::from_create_args(&args)?;
+
+    // Register the template engine and execute the templates.
+    let mut handlebars = Handlebars::new();
+    handlebars.set_strict_mode(true);
+    tmpl_helpers::register_helpers(&mut handlebars);
+    let project = templates.render(&mut handlebars, &tmpl_args)?;
+
+    // Write the rendered files to a temp directory.
+    let dir = tempdir()?;
+    let tmp_out_path = dir.path().join(&args.project_name);
+    project.write(&tmp_out_path)?;
+
+    // Rename the temp directory project to the final location.
+    let dest_project_path = env::current_dir()?.join(&args.project_name);
+    fs::rename(&tmp_out_path, &dest_project_path)?;
+
+    println!("Project created at {}.", dest_project_path.to_string_lossy());
+
+    // Find the parent BUILD.gn file and suggest adding the test target.
+    let parent_build =
+        dest_project_path.parent().map(|p| p.join("BUILD.gn")).filter(|b| b.exists());
+    if let Some(parent_build) = parent_build {
+        println!(
+            "{}note:{} Don't forget to include the {}{}:tests{} GN target in the parent {}tests{} target ({}).",
+            color::Fg(color::Yellow), color::Fg(color::Reset),
+            style::Bold, &args.project_name, style::Reset,
+            style::Bold, style::Reset,
+            parent_build.to_string_lossy()
+        );
+    }
+
+    Ok(())
+}
+
+#[derive(Debug, StructOpt)]
+#[structopt(name = "fx-create", about = "Creates scaffolding for new projects.")]
+struct CreateArgs {
+    /// The type of project to create.
+    ///
+    /// This can be one of:
+    ///
+    /// - component-v2: A V2 component launched with Component Manager,
+    #[structopt(name = "project-type")]
+    project_type: String,
+
+    /// The name of the new project.
+    ///
+    /// This will be the name of the GN target and directory for the project.
+    /// The name should not contain any underscores.
+    #[structopt(name = "project-name")]
+    project_name: String,
+
+    /// The programming language.
+    #[structopt(short, long)]
+    lang: Language,
+}
+
+/// Supported languages for project creation.
+#[derive(Debug)]
+enum Language {
+    Rust,
+    Cpp,
+}
+
+impl Language {
+    /// Returns the language's template suffix. Template
+    /// files that match this suffix belong to this language.
+    fn template_suffix(&self) -> &'static str {
+        match self {
+            Self::Rust => ".tmpl-rust",
+            Self::Cpp => ".tmpl-cpp",
+        }
+    }
+}
+
+impl FromStr for Language {
+    type Err = String;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        Ok(match s {
+            "rust" => Self::Rust,
+            "cpp" => Self::Cpp,
+            _ => return Err(format!("unrecognized language \"{}\"", s)),
+        })
+    }
+}
+
+impl fmt::Display for Language {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.write_str(match self {
+            Self::Rust => "rust",
+            Self::Cpp => "cpp",
+        })
+    }
+}
+
+/// The arguments passed in during template execution.
+/// The fields defined here represent what variables can be present in template files.
+/// Add a field here and populate it to make it accessible in a template.
+///
+/// NOTE: The fields are serialized to JSON when passed to templates. The serialization
+/// process renames all fields to UPPERCASE.
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "UPPERCASE")]
+struct TemplateArgs {
+    /// The current year, for use in copyright headers.
+    /// Reference from a template with `{{COPYRIGHT_YEAR}}`.
+    copyright_year: String,
+
+    /// The project name, as given on the command line.
+    /// Reference from a template with `{{PROJECT_NAME}}`.
+    project_name: String,
+
+    /// The path to the new project, relative to the FUCHSIA_DIR environment variable.
+    /// Reference from a template with `{{PROJECT_PATH}}`.
+    project_path: String,
+
+    /// The project-type, as specified on the command line. E.g. 'component-v2'.
+    project_type: String,
+}
+
+impl TemplateArgs {
+    /// Build TemplateArgs from the program args and environment.
+    fn from_create_args(create_args: &CreateArgs) -> Result<Self, anyhow::Error> {
+        Ok(TemplateArgs {
+            copyright_year: Utc::now().year().to_string(),
+            project_name: create_args.project_name.clone(),
+            project_path: {
+                let absolute_project_path = env::current_dir()?.join(&create_args.project_name);
+                let fuchsia_root = util::get_fuchsia_root()?;
+                absolute_project_path
+                    .strip_prefix(&fuchsia_root)
+                    .map_err(|_| {
+                        anyhow!(
+                            "current working directory must be a descendant of FUCHSIA_DIR ({:?})",
+                            &fuchsia_root
+                        )
+                    })?
+                    .to_str()
+                    .ok_or_else(|| anyhow!("invalid path {:?}", &absolute_project_path))?
+                    .to_string()
+            },
+            project_type: create_args.project_type.clone(),
+        })
+    }
+}
+
+/// The in-memory filtered template file tree.
+#[derive(Debug, PartialEq)]
+enum TemplateTree {
+    /// A file and its template contents.
+    File(String),
+
+    /// A directory and its entries.
+    Dir(HashMap<String, Box<TemplateTree>>),
+}
+
+impl TemplateTree {
+    /// Populate a TemplateTree recursively from a path, filtering by `lang`.
+    /// See [`Language::template_suffix`].
+    fn from_dir(path: &Path, lang: &Language) -> io::Result<Self> {
+        Ok(if path.is_dir() {
+            let mut templates = HashMap::new();
+            for entry in fs::read_dir(path)? {
+                let entry = entry?;
+                let filename = match Self::filter_entry(&entry, lang)? {
+                    Some(filename) => filename,
+                    None => continue,
+                };
+
+                // Recursively create a TemplateTree from this subpath.
+                let sub_templates = TemplateTree::from_dir(&entry.path(), lang)?;
+                if !sub_templates.is_empty() {
+                    templates.insert(filename, Box::new(sub_templates));
+                }
+            }
+            TemplateTree::Dir(templates)
+        } else {
+            TemplateTree::File(fs::read_to_string(path)?)
+        })
+    }
+
+    /// Filters a template directory entry based on language, returning the
+    /// template filename without the `.tmpl` or `.tmpl-<lang>` extension.
+    fn filter_entry(entry: &fs::DirEntry, lang: &Language) -> io::Result<Option<String>> {
+        let filename = util::filename_to_string(entry.file_name())?;
+        if entry.file_type()?.is_dir() {
+            // Directories don't get filtered by language.
+            Ok(Some(filename))
+        } else {
+            // Check if the file's extension matches the language-specific template
+            // extension or the general template extension.
+            let filter = util::strip_suffix(&filename, lang.template_suffix());
+            let filter = filter.or_else(|| util::strip_suffix(&filename, ".tmpl"));
+            Ok(filter.map(str::to_string))
+        }
+    }
+
+    /// Checks if the file or directory is empty.
+    fn is_empty(&self) -> bool {
+        match self {
+            Self::File(contents) => contents.is_empty(),
+            Self::Dir(m) => m.is_empty(),
+        }
+    }
+
+    /// Recursively renders this TemplateTree into a mirror type [`RenderedTree`],
+    /// using `handlebars` as the template engine and `args` as the exported variables
+    // accessible to the templates.
+    fn render(
+        &self,
+        handlebars: &mut Handlebars,
+        args: &TemplateArgs,
+    ) -> Result<RenderedTree, handlebars::TemplateRenderError> {
+        Ok(match self {
+            Self::File(template_str) => {
+                RenderedTree::File(handlebars.render_template(&template_str, args)?)
+            }
+            Self::Dir(nested_templates) => {
+                let mut rendered_subtree = HashMap::new();
+                for (filename, template) in nested_templates {
+                    rendered_subtree.insert(
+                        filename.replace("$", &args.project_name),
+                        Box::new(template.render(handlebars, args)?),
+                    );
+                }
+                RenderedTree::Dir(rendered_subtree)
+            }
+        })
+    }
+}
+
+/// An in-memory representation of a file tree, where the paths and contents have
+/// all been executed and rendered into their final form.
+/// This is the mirror of [`TemplateTree`].
+#[derive(Debug, PartialEq)]
+enum RenderedTree {
+    /// A file and its contents.
+    File(String),
+
+    /// A directory and its entries.
+    Dir(HashMap<String, Box<RenderedTree>>),
+}
+
+impl RenderedTree {
+    /// Write the RenderedTree to the `dest` path.
+    fn write(&self, dest: &Path) -> io::Result<()> {
+        match self {
+            Self::File(contents) => {
+                fs::write(dest, &contents)?;
+            }
+            Self::Dir(tree) => {
+                fs::create_dir(dest)?;
+                for (filename, subtree) in tree {
+                    let dest = dest.join(filename);
+                    subtree.write(&dest)?;
+                }
+            }
+        }
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn render_templates() {
+        let templates = TemplateTree::Dir({
+            let mut entries = HashMap::new();
+            entries.insert(
+                "file.h".to_string(),
+                Box::new(TemplateTree::File("class {{PROJECT_NAME}};".to_string())),
+            );
+            entries.insert(
+                "$_nested".to_string(),
+                Box::new(TemplateTree::Dir({
+                    let mut entries = HashMap::new();
+                    entries.insert(
+                        "file.h".to_string(),
+                        Box::new(TemplateTree::File(
+                            "#include \"{{PROJECT_PATH}}/file.h\"\n// `fx create {{PROJECT_TYPE}}`"
+                                .to_string(),
+                        )),
+                    );
+                    entries
+                })),
+            );
+            entries
+        });
+
+        let mut handlebars = Handlebars::new();
+        handlebars.set_strict_mode(true);
+        let args = TemplateArgs {
+            copyright_year: "2020".to_string(),
+            project_name: "foo".to_string(),
+            project_path: "bar/foo".to_string(),
+            project_type: "component-v2".to_string(),
+        };
+
+        let rendered =
+            templates.render(&mut handlebars, &args).expect("failed to render templates");
+        assert_eq!(
+            rendered,
+            RenderedTree::Dir({
+                let mut entries = HashMap::new();
+                entries.insert(
+                    "file.h".to_string(),
+                    Box::new(RenderedTree::File("class foo;".to_string())),
+                );
+                entries.insert(
+                    "foo_nested".to_string(),
+                    Box::new(RenderedTree::Dir({
+                        let mut entries = HashMap::new();
+                        entries.insert(
+                            "file.h".to_string(),
+                            Box::new(RenderedTree::File(
+                                "#include \"bar/foo/file.h\"\n// `fx create component-v2`"
+                                    .to_string(),
+                            )),
+                        );
+                        entries
+                    })),
+                );
+                entries
+            })
+        );
+    }
+}
diff --git a/tools/create/src/tmpl_helpers.rs b/tools/create/src/tmpl_helpers.rs
new file mode 100644
index 0000000..b706fdb
--- /dev/null
+++ b/tools/create/src/tmpl_helpers.rs
@@ -0,0 +1,59 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use handlebars::{handlebars_helper, Handlebars};
+use heck::{CamelCase, ShoutySnakeCase, SnekCase};
+
+// heck::CamelCase is actually PascalCase.
+handlebars_helper!(pascal_case: |arg: str| arg.to_camel_case());
+
+handlebars_helper!(snake_case: |arg: str| arg.to_snek_case());
+
+handlebars_helper!(screaming_snake_case: |arg: str| arg.to_shouty_snake_case());
+
+/// Register all applicable helper template methods.
+pub fn register_helpers(handlebars: &mut Handlebars) {
+    // `{{pascal_case arg}}` converts `arg` to PascalCase.
+    handlebars.register_helper("pascal_case", Box::new(pascal_case));
+
+    // `{{snake_case arg}}` converts `arg` to snake_case.
+    handlebars.register_helper("snake_case", Box::new(snake_case));
+
+    // `{{screaming_snake_case arg}}` converts `arg` to SCREAMING_SNAKE_CASE.
+    handlebars.register_helper("screaming_snake_case", Box::new(screaming_snake_case));
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use serde::Serialize;
+
+    #[derive(Serialize)]
+    struct Args {
+        var: String,
+    }
+
+    #[test]
+    fn helpers_work() {
+        let mut handlebars = Handlebars::new();
+        handlebars.set_strict_mode(true);
+        register_helpers(&mut handlebars);
+
+        let args = Args { var: "foo_bar".to_string() };
+        let render =
+            handlebars.render_template("{{pascal_case var}}", &args).expect("failed to render");
+        assert_eq!(render, "FooBar");
+
+        let args = Args { var: "FooBar".to_string() };
+        let render =
+            handlebars.render_template("{{snake_case var}}", &args).expect("failed to render");
+        assert_eq!(render, "foo_bar");
+
+        let args = Args { var: "FooBar".to_string() };
+        let render = handlebars
+            .render_template("{{screaming_snake_case var}}", &args)
+            .expect("failed to render");
+        assert_eq!(render, "FOO_BAR");
+    }
+}
diff --git a/tools/create/src/util.rs b/tools/create/src/util.rs
new file mode 100644
index 0000000..2617b91
--- /dev/null
+++ b/tools/create/src/util.rs
@@ -0,0 +1,54 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use anyhow::anyhow;
+use std::ffi::OsString;
+use std::path::{Path, PathBuf};
+use std::{env, fs, io};
+
+/// Return the fuchsia root directory.
+pub fn get_fuchsia_root() -> Result<PathBuf, env::VarError> {
+    Ok(PathBuf::from(env::var("FUCHSIA_DIR")?))
+}
+
+/// Returns the path to the template files.
+pub fn get_templates_dir_path() -> io::Result<PathBuf> {
+    let exe_path = env::current_exe()?;
+    let exe_dir_path = exe_path.parent().ok_or_else(|| {
+        io::Error::new(io::ErrorKind::InvalidData, anyhow!("exe directory is root"))
+    })?;
+    Ok(exe_dir_path.join("create_templates/templates"))
+}
+
+/// Returns whether the directory `dir` contains an entry named `filename`.
+pub fn dir_contains(dir: &Path, filename: &str) -> io::Result<bool> {
+    if dir.is_dir() {
+        for entry in fs::read_dir(dir)? {
+            let entry = entry?;
+            let dir_filename = filename_to_string(entry.file_name())?;
+            if dir_filename == filename {
+                return Ok(true);
+            }
+        }
+    }
+    return Ok(false);
+}
+
+/// Converts an OS-specific filename to a String, returning an io::Error
+/// if a failure occurs. The io::Error contains the invalid filename,
+/// which gives the user a better indication of what went wrong.
+pub fn filename_to_string(filename: OsString) -> io::Result<String> {
+    filename.into_string().map_err(|s| {
+        io::Error::new(io::ErrorKind::InvalidData, anyhow!("invalid filename {:?}", s))
+    })
+}
+
+/// Strips `suffix` from the end of `s` if `s` ends with `suffix`.
+pub fn strip_suffix<'a>(s: &'a str, suffix: &str) -> Option<&'a str> {
+    if s.ends_with(suffix) {
+        Some(&s[0..s.len() - suffix.len()])
+    } else {
+        None
+    }
+}
diff --git a/tools/create/templates/component-v2/$.cc.tmpl-cpp b/tools/create/templates/component-v2/$.cc.tmpl-cpp
new file mode 100644
index 0000000..fbb796f
--- /dev/null
+++ b/tools/create/templates/component-v2/$.cc.tmpl-cpp
@@ -0,0 +1,18 @@
+// Copyright {{COPYRIGHT_YEAR}} 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 "{{PROJECT_PATH}}/{{PROJECT_NAME}}.h"
+
+#include <lib/async/cpp/task.h>
+#include <lib/async/dispatcher.h>
+
+#include <iostream>
+
+namespace {{snake_case PROJECT_NAME}} {
+
+App::App(async_dispatcher_t* dispatcher) : dispatcher_(dispatcher) {
+  async::PostTask(dispatcher_, []() { std::cout << "Hello, Fuchsia!" << std::endl; });
+}
+
+}  // namespace {{snake_case PROJECT_NAME}}
diff --git a/tools/create/templates/component-v2/$.h.tmpl-cpp b/tools/create/templates/component-v2/$.h.tmpl-cpp
new file mode 100644
index 0000000..10902f4
--- /dev/null
+++ b/tools/create/templates/component-v2/$.h.tmpl-cpp
@@ -0,0 +1,23 @@
+// Copyright {{COPYRIGHT_YEAR}} 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.
+
+#ifndef {{screaming_snake_case PROJECT_PATH}}_{{screaming_snake_case PROJECT_NAME}}_H_
+#define {{screaming_snake_case PROJECT_PATH}}_{{screaming_snake_case PROJECT_NAME}}_H_
+
+#include <lib/async/dispatcher.h>
+
+namespace {{snake_case PROJECT_NAME}} {
+
+// This is the component's main class. It holds all of the component's state.
+class App {
+ public:
+  explicit App(async_dispatcher_t* dispatcher);
+
+ private:
+  async_dispatcher_t* dispatcher_;
+};
+
+}  // namespace {{snake_case PROJECT_NAME}}
+
+#endif  // {{screaming_snake_case PROJECT_PATH}}_{{screaming_snake_case PROJECT_NAME}}_H_
diff --git a/tools/create/templates/component-v2/$_test.cc.tmpl-cpp b/tools/create/templates/component-v2/$_test.cc.tmpl-cpp
new file mode 100644
index 0000000..fdf7d7c
--- /dev/null
+++ b/tools/create/templates/component-v2/$_test.cc.tmpl-cpp
@@ -0,0 +1,16 @@
+// Copyright {{COPYRIGHT_YEAR}} 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 "{{PROJECT_PATH}}/{{PROJECT_NAME}}.h"
+
+#include <lib/async-loop/cpp/loop.h>
+#include <lib/async-loop/default.h>
+
+#include <gtest/gtest.h>
+
+TEST({{pascal_case PROJECT_NAME}}Test, Smoke) {
+  async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread);
+  {{snake_case PROJECT_NAME}}::App app(loop.dispatcher());
+  loop.RunUntilIdle();
+}
diff --git a/tools/create/templates/component-v2/BUILD.gn.tmpl-cpp b/tools/create/templates/component-v2/BUILD.gn.tmpl-cpp
new file mode 100644
index 0000000..08b622c
--- /dev/null
+++ b/tools/create/templates/component-v2/BUILD.gn.tmpl-cpp
@@ -0,0 +1,96 @@
+# Copyright {{COPYRIGHT_YEAR}} 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.
+
+# This file was generated by the `fx create component-v2` command. The template
+# is located at `//tools/create/templates/component-v2/BUILD.gn.tmpl-cpp`.
+# If something is broken, consider authoring a fix.
+
+import("//build/package.gni")
+import("//build/test/test_package.gni")
+import("//build/testing/environments.gni")
+
+group("{{PROJECT_NAME}}") {
+  testonly = true
+  public_deps = [
+    ":pkg",
+    ":tests",
+  ]
+}
+
+group("tests") {
+  testonly = true
+  public_deps = [ ":{{PROJECT_NAME}}-tests" ]
+}
+
+source_set("lib") {
+  sources = [
+    "{{PROJECT_NAME}}.cc",
+    "{{PROJECT_NAME}}.h",
+  ]
+
+  public_deps = [ "//zircon/public/lib/async-cpp" ]
+}
+
+executable("bin") {
+  output_name = "{{PROJECT_NAME}}"
+
+  sources = [ "main.cc" ]
+
+  deps = [
+    ":lib",
+    "//zircon/public/lib/async-default",
+    "//zircon/public/lib/async-loop-cpp",
+    "//zircon/public/lib/async-loop-default",
+  ]
+}
+
+executable("unittests") {
+  output_name = "{{PROJECT_NAME}}_unittests"
+  testonly = true
+
+  sources = [ "{{PROJECT_NAME}}_test.cc" ]
+
+  deps = [
+    ":lib",
+    "//third_party/googletest:gtest",
+    "//third_party/googletest:gtest_main",
+    "//zircon/public/lib/async-default",
+    "//zircon/public/lib/async-loop-cpp",
+    "//zircon/public/lib/async-loop-default",
+  ]
+}
+
+package("pkg") {
+  package_name = "{{PROJECT_NAME}}"
+
+  deps = [ ":bin" ]
+
+  binaries = [
+    {
+      name = "{{PROJECT_NAME}}"
+    },
+  ]
+
+  meta = [
+    # Compile the package's CML manifest.
+    # The resulting component manifest can be referenced through the URL
+    # fuchsia-pkg://fuchsia.com/{{PROJECT_NAME}}#meta/{{PROJECT_NAME}}.cm.
+    {
+      path = rebase_path("meta/{{PROJECT_NAME}}.cml")
+      dest = "{{PROJECT_NAME}}.cm"
+    },
+  ]
+}
+
+# Run with `fx test {{PROJECT_NAME}}-tests`.
+test_package("{{PROJECT_NAME}}-tests") {
+  deps = [ ":unittests" ]
+
+  v2_tests = [
+    {
+      name = "{{PROJECT_NAME}}_unittests"
+      environments = basic_envs
+    },
+  ]
+}
diff --git a/tools/create/templates/component-v2/BUILD.gn.tmpl-rust b/tools/create/templates/component-v2/BUILD.gn.tmpl-rust
new file mode 100644
index 0000000..784010e
--- /dev/null
+++ b/tools/create/templates/component-v2/BUILD.gn.tmpl-rust
@@ -0,0 +1,83 @@
+# Copyright {{COPYRIGHT_YEAR}} 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.
+
+# This file was generated by the `fx create component-v2` command. The template
+# is located at `//tools/create/templates/component-v2/BUILD.gn.tmpl-rust`.
+# If something is broken, consider authoring a fix.
+
+import("//build/package.gni")
+import("//build/rust/rustc_binary.gni")
+import("//build/test/test_package.gni")
+import("//build/testing/environments.gni")
+
+group("{{PROJECT_NAME}}") {
+  testonly = true
+  public_deps = [
+    ":pkg",
+    ":tests",
+  ]
+}
+
+group("tests") {
+  testonly = true
+  public_deps = [ ":{{PROJECT_NAME}}-tests" ]
+}
+
+rustc_binary("bin") {
+  name = "{{PROJECT_NAME}}"
+
+  # Generates a GN target for unit-tests with the label `bin_test`, and
+  # a binary named `{{snake_case PROJECT_NAME}}_bin_test`.
+  with_unit_tests = true
+
+  deps = [
+    "//src/lib/fuchsia-async",
+    "//src/lib/fuchsia-component",
+  ]
+}
+
+package("pkg") {
+  package_name = "{{PROJECT_NAME}}"
+
+  deps = [ ":bin" ]
+
+  binaries = [
+    {
+      name = "{{snake_case PROJECT_NAME}}"
+      dest = "{{PROJECT_NAME}}"
+    },
+  ]
+
+  meta = [
+    # Compile the package's CML manifest.
+    # The resulting component manifest can be referenced through the URL
+    # fuchsia-pkg://fuchsia.com/{{PROJECT_NAME}}#meta/{{PROJECT_NAME}}.cm.
+    {
+      path = rebase_path("meta/{{PROJECT_NAME}}.cml")
+      dest = "{{PROJECT_NAME}}.cm"
+    },
+  ]
+}
+
+# Run with `fx test {{PROJECT_NAME}}-tests`.
+test_package("{{PROJECT_NAME}}-tests") {
+  deps = [
+    ":bin_test",
+    "//src/sys/test_adapters/rust",
+  ]
+
+  binaries = [
+    {
+      name = "rust_test_adapter"
+    },
+  ]
+
+  v2_tests = [
+    {
+      name = "{{snake_case PROJECT_NAME}}_bin_test"
+      dest = "{{PROJECT_NAME}}_test"
+      environments = basic_envs
+    },
+  ]
+}
diff --git a/tools/create/templates/component-v2/main.cc.tmpl-cpp b/tools/create/templates/component-v2/main.cc.tmpl-cpp
new file mode 100644
index 0000000..360bebe
--- /dev/null
+++ b/tools/create/templates/component-v2/main.cc.tmpl-cpp
@@ -0,0 +1,20 @@
+// Copyright {{COPYRIGHT_YEAR}} 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 <lib/async-loop/cpp/loop.h>
+#include <lib/async-loop/default.h>
+
+#include "{{PROJECT_PATH}}/{{PROJECT_NAME}}.h"
+
+int main(int argc, const char** argv) {
+  // Create the main async event loop.
+  async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread);
+
+  // Create an instance of the application state.
+  {{snake_case PROJECT_NAME}}::App app(loop.dispatcher());
+
+  // Run the loop until it is shutdown.
+  loop.Run();
+  return 0;
+}
diff --git a/tools/create/templates/component-v2/meta/$.cml.tmpl b/tools/create/templates/component-v2/meta/$.cml.tmpl
new file mode 100644
index 0000000..b187340
--- /dev/null
+++ b/tools/create/templates/component-v2/meta/$.cml.tmpl
@@ -0,0 +1,27 @@
+// Copyright {{COPYRIGHT_YEAR}} 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.
+
+// {{PROJECT_NAME}} component manifest.
+// For information on manifest format and features,
+// see https://fuchsia.dev/fuchsia-src/concepts/components/component_manifests.
+
+// This file was generated by the `fx create component-v2` command. The template
+// is located at `//tools/create/templates/component-v2/meta/$.cml.tmpl`.
+// If something is broken, consider authoring a fix.
+
+{
+    // The binary to run for this component.
+    program: {
+        binary: "bin/{{PROJECT_NAME}}",
+    },
+
+    // Capabilities used by this component.
+    use: [
+        // Use the built-in ELF runner to run native binaries.
+        { runner: "elf" },
+
+        // List your component's dependencies here, ex:
+        // { protocol: "/svc/fuchsia.logger.Log" }
+    ],
+}
diff --git a/tools/create/templates/component-v2/meta/$_test.cml.tmpl-rust b/tools/create/templates/component-v2/meta/$_test.cml.tmpl-rust
new file mode 100644
index 0000000..8d3afdd
--- /dev/null
+++ b/tools/create/templates/component-v2/meta/$_test.cml.tmpl-rust
@@ -0,0 +1,35 @@
+// Copyright {{COPYRIGHT_YEAR}} 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.
+
+// {{PROJECT_NAME}} test component manifest.
+// For information on manifest format and features,
+// see https://fuchsia.dev/fuchsia-src/concepts/components/component_manifests.
+//
+// This file was generated by the `fx create component-v2` command. The template
+// is located at `//tools/create/templates/component-v2/meta/$_test.cml.tmpl-rust`.
+// If something is broken, consider authoring a fix.
+
+{
+    program: {
+        // Run the Rust test adapter, which will configure the environment
+        // and execute the test binary.
+        binary: "bin/rust_test_adapter",
+        args: [ "/pkg/test/{{PROJECT_NAME}}_test" ],
+    },
+    use: [
+        // Use the built-in ELF runner to run Rust binaries.
+        { runner: "elf" },
+
+        // Needed for the Rust test framework to run tests in sub-processes.
+        { protocol: "/svc/fuchsia.process.Launcher" },
+    ],
+    expose: [
+        // The Fuchsia Test Framework expects this service to be exposed from the
+        // test component. The Rust test adapter provides this service.
+        {
+            protocol: "/svc/fuchsia.test.Suite",
+            from: "self",
+        },
+    ],
+}
diff --git a/tools/create/templates/component-v2/meta/$_unittests.cml.tmpl-cpp b/tools/create/templates/component-v2/meta/$_unittests.cml.tmpl-cpp
new file mode 100644
index 0000000..e6a40f2
--- /dev/null
+++ b/tools/create/templates/component-v2/meta/$_unittests.cml.tmpl-cpp
@@ -0,0 +1,28 @@
+// Copyright {{COPYRIGHT_YEAR}} 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.
+
+// {{PROJECT_NAME}} test component manifest.
+// For information on manifest format and features,
+// see https://fuchsia.dev/fuchsia-src/concepts/components/component_manifests.
+//
+// This file was generated by the `fx create component-v2` command. The template
+// is located at `//tools/create/templates/component-v2/meta/$_test.cml.tmpl-cpp`.
+// If something is broken, consider authoring a fix.
+
+{
+    program: {
+        binary: "test/{{PROJECT_NAME}}_unittests",
+    },
+    use: [
+        { runner: "gtest_runner" },
+    ],
+    expose: [
+        // The Fuchsia Test Framework expects this service to be exposed from the
+        // test component. The gtest_runner provides this service.
+        {
+            protocol: "/svc/fuchsia.test.Suite",
+            from: "self",
+        },
+    ],
+}
diff --git a/tools/create/templates/component-v2/src/main.rs.tmpl-rust b/tools/create/templates/component-v2/src/main.rs.tmpl-rust
new file mode 100644
index 0000000..5ffb5f1
--- /dev/null
+++ b/tools/create/templates/component-v2/src/main.rs.tmpl-rust
@@ -0,0 +1,18 @@
+// Copyright {{COPYRIGHT_YEAR}} 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 fuchsia_async as fasync;
+
+#[fasync::run_singlethreaded]
+async fn main() {
+    println!("Hello, Fuchsia!");
+}
+
+#[cfg(test)]
+mod tests {
+    #[test]
+    fn it_works() {
+        assert!(true);
+    }
+}