use {
crate::environment::{PyOxidizerSource, BUILD_GIT_COMMIT, PYOXIDIZER_VERSION},
anyhow::{anyhow, Context, Result},
handlebars::Handlebars,
once_cell::sync::Lazy,
serde::Serialize,
std::{
collections::BTreeMap,
io::Write,
path::{Path, PathBuf},
str::FromStr,
},
};
static HANDLEBARS: Lazy<Handlebars<'static>> = Lazy::new(|| {
let mut handlebars = Handlebars::new();
handlebars
.register_template_string(
"application-manifest.rc",
include_str!("templates/application-manifest.rc.hbs"),
)
.unwrap();
handlebars
.register_template_string(
"cargo-extra.toml",
include_str!("templates/cargo-extra.toml.hbs"),
)
.unwrap();
handlebars
.register_template_string("exe.manifest", include_str!("templates/exe.manifest.hbs"))
.unwrap();
handlebars
.register_template_string("new-build.rs", include_str!("templates/new-build.rs.hbs"))
.unwrap();
handlebars
.register_template_string(
"new-cargo-config",
include_str!("templates/new-cargo-config.hbs"),
)
.unwrap();
handlebars
.register_template_string("new-main.rs", include_str!("templates/new-main.rs.hbs"))
.unwrap();
handlebars
.register_template_string(
"new-pyoxidizer.bzl",
include_str!("templates/new-pyoxidizer.bzl.hbs"),
)
.unwrap();
handlebars
});
const NEW_PROJECT_CARGO_LOCK: &str = include_str!("new-project-cargo.lock");
const NEW_PROJECT_DEPENDENCIES: &[&str] = &[
"embed-resource",
"jemallocator",
"mimalloc",
"pyembed",
"snmalloc-rs",
];
#[derive(Serialize)]
struct PythonDistribution {
build_target: String,
url: String,
sha256: String,
}
#[derive(Serialize)]
struct TemplateData {
pyoxidizer_version: Option<String>,
pyoxidizer_commit: Option<String>,
pyoxidizer_local_repo_path: Option<String>,
pyoxidizer_git_url: Option<String>,
pyoxidizer_git_commit: Option<String>,
pyoxidizer_git_tag: Option<String>,
python_distributions: Vec<PythonDistribution>,
program_name: Option<String>,
code: Option<String>,
pip_install_simple: Vec<String>,
}
impl TemplateData {
fn new() -> TemplateData {
TemplateData {
pyoxidizer_version: None,
pyoxidizer_commit: None,
pyoxidizer_local_repo_path: None,
pyoxidizer_git_url: None,
pyoxidizer_git_commit: None,
pyoxidizer_git_tag: None,
python_distributions: Vec::new(),
program_name: None,
code: None,
pip_install_simple: Vec::new(),
}
}
}
fn populate_template_data(source: &PyOxidizerSource, data: &mut TemplateData) {
data.pyoxidizer_version = Some(PYOXIDIZER_VERSION.to_string());
data.pyoxidizer_commit = Some(
BUILD_GIT_COMMIT
.clone()
.unwrap_or_else(|| "UNKNOWN".to_string()),
);
match source {
PyOxidizerSource::LocalPath { path } => {
data.pyoxidizer_local_repo_path = Some(path.display().to_string());
}
PyOxidizerSource::GitUrl { url, commit, tag } => {
data.pyoxidizer_git_url = Some(url.clone());
if let Some(commit) = commit {
data.pyoxidizer_git_commit = Some(commit.clone());
}
if let Some(tag) = tag {
data.pyoxidizer_git_tag = Some(tag.clone());
}
}
}
}
pub fn write_new_cargo_config(project_path: &Path) -> Result<()> {
let cargo_path = project_path.join(".cargo");
if !cargo_path.is_dir() {
std::fs::create_dir(&cargo_path)?;
}
let data: BTreeMap<String, String> = BTreeMap::new();
let t = HANDLEBARS.render("new-cargo-config", &data)?;
let config_path = cargo_path.join("config");
println!("writing {}", config_path.display());
std::fs::write(&config_path, t)?;
Ok(())
}
pub fn write_new_cargo_lock(
project_path: &Path,
project_name: &str,
pyembed_location: &PyembedLocation,
) -> Result<()> {
let mut lock_file = cargo_lock::Lockfile::from_str(NEW_PROJECT_CARGO_LOCK)?;
let dependencies = NEW_PROJECT_DEPENDENCIES
.iter()
.map(|dep| cargo_lock::Dependency {
name: cargo_lock::Name::from_str(dep)
.expect("could not convert dependency name to Name"),
version: lock_file
.packages
.iter()
.filter_map(|package| {
if package.name.as_str() == *dep {
Some(package.version.clone())
} else {
None
}
})
.next()
.expect("unable to find dependency in frozen Cargo lock; something is out of sync"),
source: None,
})
.collect::<Vec<_>>();
lock_file.packages.push(cargo_lock::Package {
name: cargo_lock::Name::from_str(project_name)?,
version: semver::Version::new(0, 1, 0),
source: None,
checksum: None,
dependencies,
replace: None,
});
if let PyembedLocation::Git(url, commit) = pyembed_location {
for package in lock_file.packages.iter_mut() {
if !package.version.pre.is_empty() && package.source.is_none() {
package.source = Some(
cargo_lock::SourceId::for_git(
&url::Url::from_str(url).context("parsing Git url")?,
cargo_lock::package::GitReference::Rev(commit.clone()),
)
.context("constructing Cargo.lock Git source")?,
);
}
}
}
lock_file
.packages
.sort_by(|a, b| a.name.as_str().cmp(b.name.as_str()));
let lock_path = project_path.join("Cargo.lock");
println!("writing {}", lock_path.display());
std::fs::write(&lock_path, &lock_file.to_string())?;
Ok(())
}
pub fn write_new_build_rs(path: &Path, program_name: &str) -> Result<()> {
let mut data = TemplateData::new();
data.program_name = Some(program_name.to_string());
let t = HANDLEBARS.render("new-build.rs", &data)?;
println!("writing {}", path.display());
std::fs::write(path, t)?;
Ok(())
}
pub fn write_new_main_rs(path: &Path, windows_subsystem: &str) -> Result<()> {
let mut data: BTreeMap<String, String> = BTreeMap::new();
data.insert(
"windows_subsystem".to_string(),
windows_subsystem.to_string(),
);
let t = HANDLEBARS.render("new-main.rs", &data)?;
println!("writing {}", path.to_str().unwrap());
let mut fh = std::fs::File::create(path)?;
fh.write_all(t.as_bytes())?;
Ok(())
}
pub fn write_new_pyoxidizer_config_file(
source: &PyOxidizerSource,
project_dir: &Path,
name: &str,
code: Option<&str>,
pip_install: &[&str],
) -> Result<()> {
let path = project_dir.join("pyoxidizer.bzl");
let mut data = TemplateData::new();
populate_template_data(source, &mut data);
data.program_name = Some(name.to_string());
if let Some(code) = code {
data.code = Some(code.replace('\"', "\\\""));
}
data.pip_install_simple = pip_install.iter().map(|v| (*v).to_string()).collect();
let t = HANDLEBARS.render("new-pyoxidizer.bzl", &data)?;
println!("writing {}", path.to_str().unwrap());
let mut fh = std::fs::File::create(path)?;
fh.write_all(t.as_bytes())?;
Ok(())
}
pub fn write_application_manifest(project_dir: &Path, program_name: &str) -> Result<()> {
let mut data = TemplateData::new();
data.program_name = Some(program_name.to_string());
let manifest_path = project_dir.join(format!("{}.exe.manifest", program_name));
let manifest_data = HANDLEBARS.render("exe.manifest", &data)?;
println!("writing {}", manifest_path.display());
let mut fh = std::fs::File::create(&manifest_path)?;
fh.write_all(manifest_data.as_bytes())?;
let rc_path = project_dir.join(format!("{}-manifest.rc", program_name));
let rc_data = HANDLEBARS.render("application-manifest.rc", &data)?;
println!("writing {}", rc_path.display());
let mut fh = std::fs::File::create(&rc_path)?;
fh.write_all(rc_data.as_bytes())?;
Ok(())
}
pub enum PyembedLocation {
Version(String),
Path(PathBuf),
Git(String, String),
}
impl PyembedLocation {
pub fn cargo_manifest_fields(&self) -> String {
match self {
Self::Version(version) => format!("version = \"{}\"", version),
Self::Path(path) => format!("path = \"{}\"", path.display()),
Self::Git(url, commit) => format!("git = \"{}\", rev = \"{}\"", url, commit),
}
}
}
pub fn update_new_cargo_toml(path: &Path, pyembed_location: &PyembedLocation) -> Result<()> {
let content = std::fs::read_to_string(path)?;
let version_start = match content.find("version =") {
Some(off) => off,
None => return Err(anyhow!("could not find version line in Cargo.toml")),
};
let nl_off = match &content[version_start..content.len()].find('\n') {
Some(off) => version_start + off + 1,
None => return Err(anyhow!("could not find newline after version line")),
};
let (before, after) = content.split_at(nl_off);
let mut content = before.to_string();
content.push_str("# The license for the project boilerplate is public domain.\n");
content.push_str("# Feel free to change the license if you make modifications.\n");
content.push_str("license = \"CC-PDDC\"\n");
content.push_str("build = \"build.rs\"\n");
content.push_str(after);
content.push_str(&format!(
"pyembed = {{ {}, default-features = false }}\n",
pyembed_location.cargo_manifest_fields()
));
content.push('\n');
let data = TemplateData::new();
content.push_str(
&HANDLEBARS
.render("cargo-extra.toml", &data)
.context("rendering cargo-extra.toml template")?,
);
std::fs::write(path, content)?;
Ok(())
}
pub fn initialize_project(
source: &PyOxidizerSource,
project_path: &Path,
cargo_exe: &Path,
code: Option<&str>,
pip_install: &[&str],
windows_subsystem: &str,
) -> Result<()> {
let status = std::process::Command::new(cargo_exe)
.arg("init")
.arg("--bin")
.arg(project_path)
.status()
.context("invoking cargo init")?;
if !status.success() {
return Err(anyhow!("cargo init failed"));
}
let path = PathBuf::from(project_path);
let name = path.iter().last().unwrap().to_str().unwrap();
update_new_cargo_toml(&path.join("Cargo.toml"), &source.as_pyembed_location())
.context("updating Cargo.toml")?;
write_new_cargo_config(&path).context("writing cargo config")?;
write_new_cargo_lock(&path, name, &source.as_pyembed_location())
.context("writing Cargo.lock")?;
write_new_build_rs(&path.join("build.rs"), name).context("writing build.rs")?;
write_new_main_rs(&path.join("src").join("main.rs"), windows_subsystem)
.context("writing main.rs")?;
write_new_pyoxidizer_config_file(source, &path, name, code, pip_install)
.context("writing PyOxidizer config file")?;
write_application_manifest(&path, name).context("writing application manifest")?;
Ok(())
}