1use {
8 crate::environment::{PyOxidizerSource, BUILD_GIT_COMMIT, PYOXIDIZER_VERSION},
9 anyhow::{anyhow, Context, Result},
10 handlebars::Handlebars,
11 once_cell::sync::Lazy,
12 serde::Serialize,
13 std::{
14 collections::BTreeMap,
15 io::Write,
16 path::{Path, PathBuf},
17 str::FromStr,
18 },
19};
20
21static HANDLEBARS: Lazy<Handlebars<'static>> = Lazy::new(|| {
22 let mut handlebars = Handlebars::new();
23
24 handlebars
25 .register_template_string(
26 "application-manifest.rc",
27 include_str!("templates/application-manifest.rc.hbs"),
28 )
29 .unwrap();
30 handlebars
31 .register_template_string(
32 "cargo-extra.toml",
33 include_str!("templates/cargo-extra.toml.hbs"),
34 )
35 .unwrap();
36 handlebars
37 .register_template_string("exe.manifest", include_str!("templates/exe.manifest.hbs"))
38 .unwrap();
39 handlebars
40 .register_template_string("new-build.rs", include_str!("templates/new-build.rs.hbs"))
41 .unwrap();
42 handlebars
43 .register_template_string(
44 "new-cargo-config",
45 include_str!("templates/new-cargo-config.hbs"),
46 )
47 .unwrap();
48 handlebars
49 .register_template_string("new-main.rs", include_str!("templates/new-main.rs.hbs"))
50 .unwrap();
51 handlebars
52 .register_template_string(
53 "new-pyoxidizer.bzl",
54 include_str!("templates/new-pyoxidizer.bzl.hbs"),
55 )
56 .unwrap();
57
58 handlebars
59});
60
61const NEW_PROJECT_CARGO_LOCK: &str = include_str!("new-project-cargo.lock");
63
64const NEW_PROJECT_DEPENDENCIES: &[&str] = &[
66 "embed-resource",
67 "jemallocator",
68 "mimalloc",
69 "pyembed",
70 "snmalloc-rs",
71];
72
73#[derive(Serialize)]
74struct PythonDistribution {
75 build_target: String,
76 url: String,
77 sha256: String,
78}
79
80#[derive(Serialize)]
81struct TemplateData {
82 pyoxidizer_version: Option<String>,
83 pyoxidizer_commit: Option<String>,
84 pyoxidizer_local_repo_path: Option<String>,
85 pyoxidizer_git_url: Option<String>,
86 pyoxidizer_git_commit: Option<String>,
87 pyoxidizer_git_tag: Option<String>,
88
89 python_distributions: Vec<PythonDistribution>,
90 program_name: Option<String>,
91 code: Option<String>,
92 pip_install_simple: Vec<String>,
93}
94
95impl TemplateData {
96 fn new() -> TemplateData {
97 TemplateData {
98 pyoxidizer_version: None,
99 pyoxidizer_commit: None,
100 pyoxidizer_local_repo_path: None,
101 pyoxidizer_git_url: None,
102 pyoxidizer_git_commit: None,
103 pyoxidizer_git_tag: None,
104 python_distributions: Vec::new(),
105 program_name: None,
106 code: None,
107 pip_install_simple: Vec::new(),
108 }
109 }
110}
111
112fn populate_template_data(source: &PyOxidizerSource, data: &mut TemplateData) {
113 data.pyoxidizer_version = Some(PYOXIDIZER_VERSION.to_string());
114 data.pyoxidizer_commit = Some(
115 BUILD_GIT_COMMIT
116 .clone()
117 .unwrap_or_else(|| "UNKNOWN".to_string()),
118 );
119
120 match source {
121 PyOxidizerSource::LocalPath { path } => {
122 data.pyoxidizer_local_repo_path = Some(path.display().to_string());
123 }
124 PyOxidizerSource::GitUrl { url, commit, tag } => {
125 data.pyoxidizer_git_url = Some(url.clone());
126
127 if let Some(commit) = commit {
128 data.pyoxidizer_git_commit = Some(commit.clone());
129 }
130 if let Some(tag) = tag {
131 data.pyoxidizer_git_tag = Some(tag.clone());
132 }
133 }
134 }
135}
136
137pub fn write_new_cargo_config(project_path: &Path) -> Result<()> {
139 let cargo_path = project_path.join(".cargo");
140
141 if !cargo_path.is_dir() {
142 std::fs::create_dir(&cargo_path)?;
143 }
144
145 let data: BTreeMap<String, String> = BTreeMap::new();
146 let t = HANDLEBARS.render("new-cargo-config", &data)?;
147
148 let config_path = cargo_path.join("config");
149 println!("writing {}", config_path.display());
150 std::fs::write(&config_path, t)?;
151
152 Ok(())
153}
154
155pub fn write_new_cargo_lock(
164 project_path: &Path,
165 project_name: &str,
166 pyembed_location: &PyembedLocation,
167) -> Result<()> {
168 let mut lock_file = cargo_lock::Lockfile::from_str(NEW_PROJECT_CARGO_LOCK)?;
171
172 let dependencies = NEW_PROJECT_DEPENDENCIES
173 .iter()
174 .map(|dep| cargo_lock::Dependency {
175 name: cargo_lock::Name::from_str(dep)
176 .expect("could not convert dependency name to Name"),
177 version: lock_file
178 .packages
179 .iter()
180 .filter_map(|package| {
181 if package.name.as_str() == *dep {
182 Some(package.version.clone())
183 } else {
184 None
185 }
186 })
187 .next()
188 .expect("unable to find dependency in frozen Cargo lock; something is out of sync"),
189 source: None,
190 })
191 .collect::<Vec<_>>();
192
193 lock_file.packages.push(cargo_lock::Package {
194 name: cargo_lock::Name::from_str(project_name)?,
195 version: semver::Version::new(0, 1, 0),
196 source: None,
197 checksum: None,
198 dependencies,
199 replace: None,
200 });
201
202 if let PyembedLocation::Git(url, commit) = pyembed_location {
227 for package in lock_file.packages.iter_mut() {
228 if !package.version.pre.is_empty() && package.source.is_none() {
229 package.source = Some(
230 cargo_lock::SourceId::for_git(
231 &url::Url::from_str(url).context("parsing Git url")?,
232 cargo_lock::package::GitReference::Rev(commit.clone()),
233 )
234 .context("constructing Cargo.lock Git source")?,
235 );
236 }
237 }
238 }
239
240 lock_file
242 .packages
243 .sort_by(|a, b| a.name.as_str().cmp(b.name.as_str()));
244
245 let lock_path = project_path.join("Cargo.lock");
246 println!("writing {}", lock_path.display());
247 std::fs::write(&lock_path, &lock_file.to_string())?;
248
249 Ok(())
250}
251
252pub fn write_new_build_rs(path: &Path, program_name: &str) -> Result<()> {
253 let mut data = TemplateData::new();
254 data.program_name = Some(program_name.to_string());
255 let t = HANDLEBARS.render("new-build.rs", &data)?;
256
257 println!("writing {}", path.display());
258 std::fs::write(path, t)?;
259
260 Ok(())
261}
262
263pub fn write_new_main_rs(path: &Path, windows_subsystem: &str) -> Result<()> {
267 let mut data: BTreeMap<String, String> = BTreeMap::new();
268 data.insert(
269 "windows_subsystem".to_string(),
270 windows_subsystem.to_string(),
271 );
272 let t = HANDLEBARS.render("new-main.rs", &data)?;
273
274 println!("writing {}", path.to_str().unwrap());
275 let mut fh = std::fs::File::create(path)?;
276 fh.write_all(t.as_bytes())?;
277
278 Ok(())
279}
280
281pub fn write_new_pyoxidizer_config_file(
283 source: &PyOxidizerSource,
284 project_dir: &Path,
285 name: &str,
286 code: Option<&str>,
287 pip_install: &[&str],
288) -> Result<()> {
289 let path = project_dir.join("pyoxidizer.bzl");
290
291 let mut data = TemplateData::new();
292 populate_template_data(source, &mut data);
293 data.program_name = Some(name.to_string());
294
295 if let Some(code) = code {
296 data.code = Some(code.replace('\"', "\\\""));
299 }
300
301 data.pip_install_simple = pip_install.iter().map(|v| (*v).to_string()).collect();
302
303 let t = HANDLEBARS.render("new-pyoxidizer.bzl", &data)?;
304
305 println!("writing {}", path.to_str().unwrap());
306 let mut fh = std::fs::File::create(path)?;
307 fh.write_all(t.as_bytes())?;
308
309 Ok(())
310}
311
312pub fn write_application_manifest(project_dir: &Path, program_name: &str) -> Result<()> {
321 let mut data = TemplateData::new();
322 data.program_name = Some(program_name.to_string());
323
324 let manifest_path = project_dir.join(format!("{}.exe.manifest", program_name));
325 let manifest_data = HANDLEBARS.render("exe.manifest", &data)?;
326 println!("writing {}", manifest_path.display());
327 let mut fh = std::fs::File::create(&manifest_path)?;
328 fh.write_all(manifest_data.as_bytes())?;
329
330 let rc_path = project_dir.join(format!("{}-manifest.rc", program_name));
331 let rc_data = HANDLEBARS.render("application-manifest.rc", &data)?;
332 println!("writing {}", rc_path.display());
333 let mut fh = std::fs::File::create(&rc_path)?;
334 fh.write_all(rc_data.as_bytes())?;
335
336 Ok(())
337}
338
339pub enum PyembedLocation {
341 Version(String),
345
346 Path(PathBuf),
348
349 Git(String, String),
351}
352
353impl PyembedLocation {
354 pub fn cargo_manifest_fields(&self) -> String {
356 match self {
357 Self::Version(version) => format!("version = \"{}\"", version),
358 Self::Path(path) => format!("path = \"{}\"", path.display()),
359 Self::Git(url, commit) => format!("git = \"{}\", rev = \"{}\"", url, commit),
360 }
361 }
362}
363
364pub fn update_new_cargo_toml(path: &Path, pyembed_location: &PyembedLocation) -> Result<()> {
366 let content = std::fs::read_to_string(path)?;
367
368 let version_start = match content.find("version =") {
371 Some(off) => off,
372 None => return Err(anyhow!("could not find version line in Cargo.toml")),
373 };
374
375 let nl_off = match &content[version_start..content.len()].find('\n') {
376 Some(off) => version_start + off + 1,
377 None => return Err(anyhow!("could not find newline after version line")),
378 };
379
380 let (before, after) = content.split_at(nl_off);
381
382 let mut content = before.to_string();
383
384 content.push_str("# The license for the project boilerplate is public domain.\n");
386 content.push_str("# Feel free to change the license if you make modifications.\n");
387 content.push_str("license = \"CC-PDDC\"\n");
388
389 content.push_str("build = \"build.rs\"\n");
390 content.push_str(after);
391
392 content.push_str(&format!(
393 "pyembed = {{ {}, default-features = false }}\n",
394 pyembed_location.cargo_manifest_fields()
395 ));
396 content.push('\n');
397
398 let data = TemplateData::new();
399 content.push_str(
400 &HANDLEBARS
401 .render("cargo-extra.toml", &data)
402 .context("rendering cargo-extra.toml template")?,
403 );
404
405 std::fs::write(path, content)?;
406
407 Ok(())
408}
409
410pub fn initialize_project(
418 source: &PyOxidizerSource,
419 project_path: &Path,
420 cargo_exe: &Path,
421 code: Option<&str>,
422 pip_install: &[&str],
423 windows_subsystem: &str,
424) -> Result<()> {
425 let status = std::process::Command::new(cargo_exe)
426 .arg("init")
427 .arg("--bin")
428 .arg(project_path)
429 .status()
430 .context("invoking cargo init")?;
431
432 if !status.success() {
433 return Err(anyhow!("cargo init failed"));
434 }
435
436 let path = PathBuf::from(project_path);
437 let name = path.iter().last().unwrap().to_str().unwrap();
438 update_new_cargo_toml(&path.join("Cargo.toml"), &source.as_pyembed_location())
439 .context("updating Cargo.toml")?;
440 write_new_cargo_config(&path).context("writing cargo config")?;
441 write_new_cargo_lock(&path, name, &source.as_pyembed_location())
442 .context("writing Cargo.lock")?;
443 write_new_build_rs(&path.join("build.rs"), name).context("writing build.rs")?;
444 write_new_main_rs(&path.join("src").join("main.rs"), windows_subsystem)
445 .context("writing main.rs")?;
446 write_new_pyoxidizer_config_file(source, &path, name, code, pip_install)
447 .context("writing PyOxidizer config file")?;
448 write_application_manifest(&path, name).context("writing application manifest")?;
449
450 Ok(())
451}