[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);
+ }
+}