use crate::module_writer::ModuleWriter;
use crate::{Metadata21, SDistWriter};
use anyhow::{bail, format_err, Context, Result};
use cargo_metadata::Metadata;
use fs_err as fs;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::str;
const LOCAL_DEPENDENCIES_FOLDER: &str = "local_dependencies";
fn rewrite_cargo_toml(
manifest_path: impl AsRef<Path>,
known_path_deps: &HashMap<String, String>,
root_crate: bool,
) -> Result<String> {
let text = fs::read_to_string(&manifest_path).context(format!(
"Can't read Cargo.toml at {}",
manifest_path.as_ref().display(),
))?;
let mut data = text.parse::<toml_edit::Document>().context(format!(
"Failed to parse Cargo.toml at {}",
manifest_path.as_ref().display()
))?;
for dep_category in &["dependencies", "dev-dependencies", "build-dependencies"] {
if let Some(table) = data[&dep_category].as_table_mut() {
let dep_names: Vec<_> = table.iter().map(|(key, _)| key.to_string()).collect();
for dep_name in dep_names {
if table[&dep_name]["path"].is_none() {
continue;
}
if !table[&dep_name]["path"].is_str() {
bail!(
"In {}, {} {} has a path value that is not a string",
manifest_path.as_ref().display(),
dep_category,
dep_name
)
}
table[&dep_name]["path"] = if root_crate {
toml_edit::value(format!("{}/{}", LOCAL_DEPENDENCIES_FOLDER, dep_name))
} else {
toml_edit::value(format!("../{}", dep_name))
};
if !known_path_deps.contains_key(&dep_name) {
bail!(
"cargo metadata does not know about the path for {}.{} present in {}, \
which should never happen ಠ_ಠ",
dep_category,
dep_name,
manifest_path.as_ref().display()
);
}
}
}
}
Ok(data.to_string_in_original_order())
}
fn add_crate_to_source_distribution(
writer: &mut SDistWriter,
manifest_path: impl AsRef<Path>,
prefix: impl AsRef<Path>,
known_path_deps: &HashMap<String, String>,
root_crate: bool,
) -> Result<()> {
let output = Command::new("cargo")
.args(&["package", "--list", "--allow-dirty", "--manifest-path"])
.arg(manifest_path.as_ref())
.output()
.context("Failed to run cargo")?;
if !output.status.success() {
bail!(
"Failed to query file list from cargo: {}\n--- Stdout:\n{}\n--- Stderr:\n{}",
output.status,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
let file_list: Vec<&Path> = str::from_utf8(&output.stdout)
.context("Cargo printed invalid utf-8 ಠ_ಠ")?
.lines()
.map(Path::new)
.collect();
let manifest_dir = manifest_path.as_ref().parent().unwrap();
let target_source: Vec<(PathBuf, PathBuf)> = file_list
.iter()
.map(|relative_to_manifests| {
let relative_to_cwd = manifest_dir.join(relative_to_manifests);
(relative_to_manifests.to_path_buf(), relative_to_cwd)
})
.filter(|(target, _)| {
target != Path::new("Cargo.toml.orig") && target != Path::new("Cargo.toml")
})
.collect();
if root_crate
&& !target_source
.iter()
.any(|(target, _)| target == Path::new("pyproject.toml"))
{
bail!(
"pyproject.toml was not included by `cargo package`. \
Please make sure pyproject.toml is not excluded or build with `--no-sdist`"
)
}
let rewritten_cargo_toml = rewrite_cargo_toml(&manifest_path, &known_path_deps, root_crate)?;
writer.add_directory(&prefix)?;
writer.add_bytes(
prefix
.as_ref()
.join(manifest_path.as_ref().file_name().unwrap()),
rewritten_cargo_toml.as_bytes(),
)?;
for (target, source) in target_source {
writer.add_file(prefix.as_ref().join(target), source)?;
}
Ok(())
}
pub fn source_distribution(
wheel_dir: impl AsRef<Path>,
metadata21: &Metadata21,
manifest_path: impl AsRef<Path>,
cargo_metadata: &Metadata,
sdist_include: Option<&Vec<String>>,
) -> Result<PathBuf> {
let matcher = Regex::new(r"^(.*) .* \(path\+file://(.*)\)$").unwrap();
let resolve = cargo_metadata
.resolve
.as_ref()
.context("Expected to get a dependency graph from cargo")?;
let known_path_deps: HashMap<String, String> = resolve
.nodes
.iter()
.filter(|node| &node.id != resolve.root.as_ref().unwrap())
.filter_map(|node| matcher.captures(&node.id.repr))
.map(|captures| (captures[1].to_string(), captures[2].to_string()))
.collect();
let mut writer = SDistWriter::new(wheel_dir, &metadata21)?;
let root_dir = PathBuf::from(format!(
"{}-{}",
&metadata21.get_distribution_escaped(),
&metadata21.get_version_escaped()
));
for (name, path) in known_path_deps.iter() {
add_crate_to_source_distribution(
&mut writer,
&PathBuf::from(path).join("Cargo.toml"),
&root_dir.join(LOCAL_DEPENDENCIES_FOLDER).join(name),
&known_path_deps,
false,
)
.context(format!(
"Failed to add local dependency {} at {} to the source distribution",
name, path
))?;
}
add_crate_to_source_distribution(
&mut writer,
&manifest_path,
&root_dir,
&known_path_deps,
true,
)?;
let manifest_dir = manifest_path.as_ref().parent().unwrap();
if let Some(include_targets) = sdist_include {
for pattern in include_targets {
println!("📦 Including files matching \"{}\"", pattern);
for source in glob::glob(&manifest_dir.join(pattern).to_string_lossy())
.expect("No files found for pattern")
.filter_map(Result::ok)
{
let target = root_dir.join(&source.strip_prefix(manifest_dir)?);
writer.add_file(target, source)?;
}
}
}
writer.add_bytes(
root_dir.join("PKG-INFO"),
metadata21.to_file_contents().as_bytes(),
)?;
let source_distribution_path = writer.finish()?;
println!(
"📦 Built source distribution to {}",
source_distribution_path.display()
);
Ok(source_distribution_path)
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct BuildSystem {
requires: Vec<String>,
build_backend: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct Tool {
maturin: Option<ToolMaturin>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct ToolMaturin {
sdist_include: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct PyProjectToml {
build_system: BuildSystem,
tool: Option<Tool>,
}
impl PyProjectToml {
pub fn sdist_include(&self) -> Option<&Vec<String>> {
self.tool.as_ref()?.maturin.as_ref()?.sdist_include.as_ref()
}
}
pub fn get_pyproject_toml(project_root: impl AsRef<Path>) -> Result<PyProjectToml> {
let path = project_root.as_ref().join("pyproject.toml");
let contents = fs::read_to_string(&path).context(format!(
"Couldn't find pyproject.toml at {}",
path.display()
))?;
let cargo_toml = toml::from_str(&contents)
.map_err(|err| format_err!("pyproject.toml is not PEP 517 compliant: {}", err))?;
Ok(cargo_toml)
}