mod dependencies;
mod environment;
pub mod errors;
pub mod grouped_environment;
pub mod manifest;
mod solve_group;
pub mod virtual_packages;
use async_once_cell::OnceCell as AsyncCell;
use indexmap::{Equivalent, IndexMap, IndexSet};
use miette::{IntoDiagnostic, NamedSource, WrapErr};
use once_cell::sync::OnceCell;
use rattler_conda_types::{Channel, GenericVirtualPackage, Platform, Version};
use rattler_networking::AuthenticationMiddleware;
use reqwest_middleware::ClientWithMiddleware;
use rip::index::PackageSources;
use rip::{index::PackageDb, normalize_index_url};
use std::hash::Hash;
use std::{
collections::{HashMap, HashSet},
env,
ffi::OsStr,
fmt::{Debug, Formatter},
fs,
path::{Path, PathBuf},
sync::Arc,
};
use crate::activation::{get_environment_variables, run_activation};
use crate::project::grouped_environment::GroupedEnvironment;
use crate::task::TaskName;
use crate::{
config,
consts::{self, PROJECT_MANIFEST},
task::Task,
};
pub use dependencies::Dependencies;
pub use environment::Environment;
use manifest::{EnvironmentName, Manifest, PyPiRequirement, SystemRequirements};
pub use solve_group::SolveGroup;
use url::Url;
use self::manifest::Environments;
#[derive(Debug, Copy, Clone)]
pub enum DependencyType {
CondaDependency(SpecType),
PypiDependency,
}
impl DependencyType {
pub fn name(&self) -> &'static str {
match self {
DependencyType::CondaDependency(dep) => dep.name(),
DependencyType::PypiDependency => consts::PYPI_DEPENDENCIES,
}
}
}
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
pub enum SpecType {
Host,
Build,
Run,
}
impl SpecType {
pub fn name(&self) -> &'static str {
match self {
SpecType::Host => "host-dependencies",
SpecType::Build => "build-dependencies",
SpecType::Run => "dependencies",
}
}
pub fn all() -> impl Iterator<Item = SpecType> {
[SpecType::Run, SpecType::Host, SpecType::Build].into_iter()
}
}
#[derive(Clone)]
pub struct Project {
root: PathBuf,
package_db: OnceCell<Arc<PackageDb>>,
client: reqwest::Client,
authenticated_client: ClientWithMiddleware,
pub(crate) manifest: Manifest,
env_vars: HashMap<EnvironmentName, Arc<AsyncCell<HashMap<String, String>>>>,
}
impl Debug for Project {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Project")
.field("root", &self.root)
.field("manifest", &self.manifest)
.finish()
}
}
impl Project {
pub fn from_manifest(manifest: Manifest) -> Self {
let client = reqwest::Client::new();
let authenticated_client = reqwest_middleware::ClientBuilder::new(reqwest::Client::new())
.with_arc(Arc::new(AuthenticationMiddleware::default()))
.build();
let env_vars = Project::init_env_vars(&manifest.parsed.environments);
Self {
root: manifest.path.parent().unwrap_or(Path::new("")).to_owned(),
package_db: Default::default(),
client,
authenticated_client,
manifest,
env_vars,
}
}
fn init_env_vars(
environments: &Environments,
) -> HashMap<EnvironmentName, Arc<AsyncCell<HashMap<String, String>>>> {
environments
.iter()
.map(|environment| (environment.name.clone(), Arc::new(AsyncCell::new())))
.collect()
}
pub fn from_str(root: &Path, content: &str) -> miette::Result<Self> {
let manifest = Manifest::from_str(root, content)?;
Ok(Self::from_manifest(manifest))
}
pub fn discover() -> miette::Result<Self> {
let project_toml = match find_project_root() {
Some(root) => root.join(PROJECT_MANIFEST),
None => miette::bail!("could not find {}", PROJECT_MANIFEST),
};
Self::load(&project_toml)
}
pub fn manifest_named_source(&self) -> NamedSource<String> {
NamedSource::new(PROJECT_MANIFEST, self.manifest.contents.clone())
}
fn load(manifest_path: &Path) -> miette::Result<Self> {
let full_path = dunce::canonicalize(manifest_path).into_diagnostic()?;
if full_path.file_name().and_then(OsStr::to_str) != Some(PROJECT_MANIFEST) {
miette::bail!("the manifest-path must point to a {PROJECT_MANIFEST} file");
}
let root = full_path
.parent()
.ok_or_else(|| miette::miette!("can not find parent of {}", manifest_path.display()))?;
let manifest = fs::read_to_string(manifest_path)
.into_diagnostic()
.and_then(|content| Manifest::from_str(root, content))
.wrap_err_with(|| {
format!(
"failed to parse {} from {}",
consts::PROJECT_MANIFEST,
root.display()
)
})?;
let env_vars = Project::init_env_vars(&manifest.parsed.environments);
Ok(Self {
root: root.to_owned(),
package_db: Default::default(),
client: Default::default(),
authenticated_client: reqwest_middleware::ClientBuilder::new(reqwest::Client::new())
.with_arc(Arc::new(AuthenticationMiddleware::default()))
.build(),
manifest,
env_vars,
})
}
pub fn load_or_else_discover(manifest_path: Option<&Path>) -> miette::Result<Self> {
let project = match manifest_path {
Some(path) => Project::load(path)?,
None => Project::discover()?,
};
Ok(project)
}
pub fn name(&self) -> &str {
&self.manifest.parsed.project.name
}
pub fn version(&self) -> &Option<Version> {
&self.manifest.parsed.project.version
}
pub fn description(&self) -> &Option<String> {
&self.manifest.parsed.project.description
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn pixi_dir(&self) -> PathBuf {
self.root.join(consts::PIXI_DIR)
}
pub fn environments_dir(&self) -> PathBuf {
self.pixi_dir().join(consts::ENVIRONMENTS_DIR)
}
pub fn solve_group_environments_dir(&self) -> PathBuf {
self.pixi_dir().join(consts::SOLVE_GROUP_ENVIRONMENTS_DIR)
}
pub fn manifest_path(&self) -> PathBuf {
self.manifest.path.clone()
}
pub fn lock_file_path(&self) -> PathBuf {
self.root.join(consts::PROJECT_LOCK_FILE)
}
pub fn save(&mut self) -> miette::Result<()> {
self.manifest.save()
}
pub fn default_environment(&self) -> Environment<'_> {
Environment::new(self, self.manifest.default_environment())
}
pub fn environment<Q: ?Sized>(&self, name: &Q) -> Option<Environment<'_>>
where
Q: Hash + Equivalent<EnvironmentName>,
{
Some(Environment::new(self, self.manifest.environment(name)?))
}
pub fn environments(&self) -> Vec<Environment> {
self.manifest
.parsed
.environments
.iter()
.map(|env| Environment::new(self, env))
.collect()
}
pub fn solve_groups(&self) -> Vec<SolveGroup> {
self.manifest
.parsed
.solve_groups
.iter()
.map(|group| SolveGroup {
project: self,
solve_group: group,
})
.collect()
}
pub fn solve_group(&self, name: &str) -> Option<SolveGroup> {
self.manifest
.parsed
.solve_groups
.find(name)
.map(|group| SolveGroup {
project: self,
solve_group: group,
})
}
pub fn grouped_environments(&self) -> Vec<GroupedEnvironment> {
let mut environments = HashSet::new();
environments.extend(
self.environments()
.into_iter()
.filter(|env| env.solve_group().is_none())
.map(GroupedEnvironment::from),
);
environments.extend(
self.solve_groups()
.into_iter()
.map(GroupedEnvironment::from),
);
environments.into_iter().collect()
}
pub fn channels(&self) -> IndexSet<&Channel> {
self.default_environment().channels()
}
pub fn platforms(&self) -> HashSet<Platform> {
self.default_environment().platforms()
}
pub fn tasks(&self, platform: Option<Platform>) -> HashMap<&TaskName, &Task> {
self.default_environment()
.tasks(platform, true)
.unwrap_or_default()
}
pub fn task_opt(&self, name: &TaskName, platform: Option<Platform>) -> Option<&Task> {
self.default_environment().task(name, platform).ok()
}
pub fn virtual_packages(&self, platform: Platform) -> Vec<GenericVirtualPackage> {
self.default_environment().virtual_packages(platform)
}
pub fn system_requirements(&self) -> SystemRequirements {
self.default_environment().system_requirements()
}
pub fn dependencies(&self, kind: Option<SpecType>, platform: Option<Platform>) -> Dependencies {
self.default_environment().dependencies(kind, platform)
}
pub fn pypi_dependencies(
&self,
platform: Option<Platform>,
) -> IndexMap<rip::types::PackageName, Vec<PyPiRequirement>> {
self.default_environment().pypi_dependencies(platform)
}
pub fn activation_scripts(&self, platform: Option<Platform>) -> Vec<String> {
self.default_environment().activation_scripts(platform)
}
pub fn has_pypi_dependencies(&self) -> bool {
self.manifest.has_pypi_dependencies()
}
pub fn pypi_index_url(&self) -> Url {
normalize_index_url(Url::parse("https://pypi.org/simple/").unwrap())
}
pub fn pypi_package_db(&self) -> miette::Result<Arc<PackageDb>> {
Ok(self
.package_db
.get_or_try_init(|| {
PackageDb::new(
PackageSources::from(self.pypi_index_url()),
self.authenticated_client().clone(),
&config::get_cache_dir()?.join("pypi/"),
)
.map(Arc::new)
})?
.clone())
}
pub fn client(&self) -> &reqwest::Client {
&self.client
}
pub fn authenticated_client(&self) -> &ClientWithMiddleware {
&self.authenticated_client
}
pub async fn get_env_variables(
&self,
environment: &Environment<'_>,
) -> miette::Result<&HashMap<String, String>> {
let cell = self.env_vars.get(environment.name()).ok_or_else(|| {
miette::miette!(
"{} environment should be already created during project creation",
environment.name()
)
})?;
cell.get_or_try_init::<miette::Report>(async {
let activation_env = run_activation(environment).await?;
let environment_variables = get_environment_variables(environment);
let all_variables: HashMap<String, String> = activation_env
.into_iter()
.chain(environment_variables.into_iter())
.collect();
Ok(all_variables)
})
.await
}
}
pub fn find_project_root() -> Option<PathBuf> {
let current_dir = env::current_dir().ok()?;
std::iter::successors(Some(current_dir.as_path()), |prev| prev.parent())
.find(|dir| dir.join(consts::PROJECT_MANIFEST).is_file())
.map(Path::to_path_buf)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::project::manifest::FeatureName;
use insta::{assert_debug_snapshot, assert_display_snapshot};
use itertools::Itertools;
use rattler_virtual_packages::{LibC, VirtualPackage};
use std::str::FromStr;
const PROJECT_BOILERPLATE: &str = r#"
[project]
name = "foo"
version = "0.1.0"
channels = []
platforms = ["linux-64", "win-64"]
"#;
#[test]
fn test_system_requirements_edge_cases() {
let file_contents = [
r#"
[system-requirements]
libc = { version = "2.12" }
"#,
r#"
[system-requirements]
libc = "2.12"
"#,
r#"
[system-requirements.libc]
version = "2.12"
"#,
r#"
[system-requirements.libc]
version = "2.12"
family = "glibc"
"#,
];
for file_content in file_contents {
let file_content = format!("{PROJECT_BOILERPLATE}\n{file_content}");
let manifest = Manifest::from_str(Path::new(""), &file_content).unwrap();
let project = Project::from_manifest(manifest);
let expected_result = vec![VirtualPackage::LibC(LibC {
family: "glibc".to_string(),
version: Version::from_str("2.12").unwrap(),
})];
let virtual_packages = project.system_requirements().virtual_packages();
assert_eq!(virtual_packages, expected_result);
}
}
fn format_dependencies(deps: Dependencies) -> String {
deps.iter_specs()
.map(|(name, spec)| format!("{} = \"{}\"", name.as_source(), spec))
.join("\n")
}
#[test]
fn test_dependency_sets() {
let file_contents = r#"
[dependencies]
foo = "1.0"
[host-dependencies]
libc = "2.12"
[build-dependencies]
bar = "1.0"
"#;
let manifest = Manifest::from_str(
Path::new(""),
format!("{PROJECT_BOILERPLATE}\n{file_contents}").as_str(),
)
.unwrap();
let project = Project::from_manifest(manifest);
assert_display_snapshot!(format_dependencies(
project.dependencies(None, Some(Platform::Linux64))
));
}
#[test]
fn test_dependency_target_sets() {
let file_contents = r#"
[dependencies]
foo = "1.0"
[host-dependencies]
libc = "2.12"
[build-dependencies]
bar = "1.0"
[target.linux-64.build-dependencies]
baz = "1.0"
[target.linux-64.host-dependencies]
banksy = "1.0"
[target.linux-64.dependencies]
wolflib = "1.0"
"#;
let manifest = Manifest::from_str(
Path::new(""),
format!("{PROJECT_BOILERPLATE}\n{file_contents}").as_str(),
)
.unwrap();
let project = Project::from_manifest(manifest);
assert_display_snapshot!(format_dependencies(
project.dependencies(None, Some(Platform::Linux64))
));
}
#[test]
fn test_activation_scripts() {
fn fmt_activation_scripts(scripts: Vec<String>) -> String {
scripts.iter().join("\n")
}
let file_contents = r#"
[target.linux-64.activation]
scripts = ["Cargo.toml"]
[target.win-64.activation]
scripts = ["Cargo.lock"]
[activation]
scripts = ["pixi.toml", "pixi.lock"]
"#;
let manifest = Manifest::from_str(
Path::new(""),
format!("{PROJECT_BOILERPLATE}\n{file_contents}").as_str(),
)
.unwrap();
let project = Project::from_manifest(manifest);
assert_display_snapshot!(format!(
"= Linux64\n{}\n\n= Win64\n{}\n\n= OsxArm64\n{}",
fmt_activation_scripts(project.activation_scripts(Some(Platform::Linux64))),
fmt_activation_scripts(project.activation_scripts(Some(Platform::Win64))),
fmt_activation_scripts(project.activation_scripts(Some(Platform::OsxArm64)))
));
}
#[test]
fn test_target_specific_tasks() {
let file_contents = r#"
[tasks]
test = "test multi"
[target.win-64.tasks]
test = "test win"
[target.linux-64.tasks]
test = "test linux"
"#;
let manifest = Manifest::from_str(
Path::new(""),
format!("{PROJECT_BOILERPLATE}\n{file_contents}").as_str(),
)
.unwrap();
let project = Project::from_manifest(manifest);
assert_debug_snapshot!(project
.manifest
.tasks(Some(Platform::Osx64), &FeatureName::Default)
.unwrap());
assert_debug_snapshot!(project
.manifest
.tasks(Some(Platform::Win64), &FeatureName::Default)
.unwrap());
assert_debug_snapshot!(project
.manifest
.tasks(Some(Platform::Linux64), &FeatureName::Default)
.unwrap());
}
}