spacetimedb_cli/subcommands/
init.rs

1use crate::util::ModuleLanguage;
2use crate::Config;
3use crate::{detect::find_executable, util::UNSTABLE_WARNING};
4use anyhow::Context;
5use clap::{Arg, ArgMatches};
6use colored::Colorize;
7use std::path::{Path, PathBuf};
8
9pub fn cli() -> clap::Command {
10    clap::Command::new("init")
11        .about(format!("Initializes a new spacetime project. {UNSTABLE_WARNING}"))
12        .arg(
13            Arg::new("project-path")
14                .value_parser(clap::value_parser!(PathBuf))
15                .default_value(".")
16                .help("The path where we will create the spacetime project"),
17        )
18        .arg(
19            Arg::new("lang")
20                .required(true)
21                .short('l')
22                .long("lang")
23                .help("The spacetime module language.")
24                .value_parser(clap::value_parser!(ModuleLanguage)),
25        )
26}
27
28fn check_for_cargo() -> bool {
29    match std::env::consts::OS {
30        "linux" | "freebsd" | "netbsd" | "openbsd" | "solaris" | "macos" => {
31            if find_executable("cargo").is_some() {
32                return true;
33            }
34            println!("{}", "Warning: You have created a rust project, but you are missing cargo. You should install cargo with the following command:\n\n\tcurl https://sh.rustup.rs -sSf | sh\n".yellow());
35        }
36        "windows" => {
37            if find_executable("cargo.exe").is_some() {
38                return true;
39            }
40            println!("{}", "Warning: You have created a rust project, but you are missing `cargo`. Visit https://www.rust-lang.org/tools/install for installation instructions:\n\n\tYou have created a rust project, but you are missing cargo.\n".yellow());
41        }
42        unsupported_os => {
43            println!("{}", format!("This OS may be unsupported: {unsupported_os}").yellow());
44        }
45    }
46    false
47}
48
49fn check_for_dotnet() -> bool {
50    use std::fmt::Write;
51
52    let subpage = match std::env::consts::OS {
53        "windows" => {
54            if find_executable("dotnet.exe").is_some() {
55                return true;
56            }
57            Some("windows")
58        }
59        os => {
60            if find_executable("dotnet").is_some() {
61                return true;
62            }
63            match os {
64                "linux" | "macos" => Some(os),
65                // can't give any hint for those other OS
66                _ => None,
67            }
68        }
69    };
70    let mut msg = "Warning: You have created a C# project, but you are missing dotnet CLI.".to_owned();
71    if let Some(subpage) = subpage {
72        write!(
73            msg,
74            " Check out https://docs.microsoft.com/en-us/dotnet/core/install/{subpage}/ for installation instructions."
75        )
76        .unwrap();
77    }
78    println!("{}", msg.yellow());
79    false
80}
81
82fn check_for_git() -> bool {
83    match std::env::consts::OS {
84        "linux" | "freebsd" | "netbsd" | "openbsd" | "solaris" => {
85            if find_executable("git").is_some() {
86                return true;
87            }
88            println!(
89                "{}",
90                "Warning: Git is not installed. You should install git using your package manager.\n".yellow()
91            );
92        }
93        "macos" => {
94            if find_executable("git").is_some() {
95                return true;
96            }
97            println!(
98                "{}",
99                "Warning: Git is not installed. You can install git by invoking:\n\n\tgit --version\n".yellow()
100            );
101        }
102        "windows" => {
103            if find_executable("git.exe").is_some() {
104                return true;
105            }
106
107            println!("{}", "Warning: You are missing git. You should install git from here:\n\n\thttps://git-scm.com/download/win\n".yellow());
108        }
109        unsupported_os => {
110            println!("{}", format!("This OS may be unsupported: {unsupported_os}").yellow());
111        }
112    }
113    false
114}
115
116pub async fn exec(_config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
117    eprintln!("{UNSTABLE_WARNING}\n");
118
119    let project_path = args.get_one::<PathBuf>("project-path").unwrap();
120    let project_lang = *args.get_one::<ModuleLanguage>("lang").unwrap();
121
122    // Create the project path, or make sure the target project path is empty.
123    if project_path.exists() {
124        if !project_path.is_dir() {
125            return Err(anyhow::anyhow!(
126                "Path {} exists but is not a directory. A new SpacetimeDB project must be initialized in an empty directory.",
127                project_path.display()
128            ));
129        }
130
131        if std::fs::read_dir(project_path).unwrap().count() > 0 {
132            return Err(anyhow::anyhow!(
133                "Cannot create new SpacetimeDB project in non-empty directory: {}",
134                project_path.display()
135            ));
136        }
137    } else {
138        create_directory(project_path)?;
139    }
140
141    match project_lang {
142        ModuleLanguage::Rust => exec_init_rust(args).await,
143        ModuleLanguage::Csharp => exec_init_csharp(args).await,
144    }
145}
146
147pub async fn exec_init_rust(args: &ArgMatches) -> Result<(), anyhow::Error> {
148    let project_path = args.get_one::<PathBuf>("project-path").unwrap();
149
150    let export_files = vec![
151        (include_str!("project/rust/Cargo._toml"), "Cargo.toml"),
152        (include_str!("project/rust/lib._rs"), "src/lib.rs"),
153        (include_str!("project/rust/_gitignore"), ".gitignore"),
154        (include_str!("project/rust/config._toml"), ".cargo/config.toml"),
155    ];
156
157    for data_file in export_files {
158        let path = project_path.join(data_file.1);
159
160        create_directory(path.parent().unwrap())?;
161
162        std::fs::write(path, data_file.0)?;
163    }
164
165    // Check all dependencies
166    check_for_cargo();
167    check_for_git();
168
169    println!(
170        "{}",
171        format!("Project successfully created at path: {}", project_path.display()).green()
172    );
173
174    Ok(())
175}
176
177pub async fn exec_init_csharp(args: &ArgMatches) -> anyhow::Result<()> {
178    let project_path = args.get_one::<PathBuf>("project-path").unwrap();
179
180    let export_files = vec![
181        (include_str!("project/csharp/StdbModule._csproj"), "StdbModule.csproj"),
182        (include_str!("project/csharp/Lib._cs"), "Lib.cs"),
183        (include_str!("project/csharp/_gitignore"), ".gitignore"),
184        (include_str!("project/csharp/global._json"), "global.json"),
185    ];
186
187    // Check all dependencies
188    check_for_dotnet();
189    check_for_git();
190
191    for data_file in export_files {
192        let path = project_path.join(data_file.1);
193
194        create_directory(path.parent().unwrap())?;
195
196        std::fs::write(path, data_file.0)?;
197    }
198
199    println!(
200        "{}",
201        format!("Project successfully created at path: {}", project_path.display()).green()
202    );
203
204    Ok(())
205}
206
207fn create_directory(path: &Path) -> Result<(), anyhow::Error> {
208    std::fs::create_dir_all(path).context("Failed to create directory")
209}