use crate::error::Error;
use crate::project::manifest::{
Dependency, Environment, Manifest, parse_version_req, split_package_name,
};
use crate::project::workspace::{self, Workspace};
use crate::registry::index::{DownloadSource, Index};
use std::collections::{BTreeMap, HashMap, VecDeque};
use std::path::Path;
pub struct ResolvedInstall {
pub name: String,
pub version: semver::Version,
pub environment: Option<Environment>,
pub source: DownloadSource,
pub index_url: String,
pub link: String,
}
enum Request {
Registry {
req_text: String,
index_url: String,
},
Workspace,
}
pub fn resolve(
manifest: &Manifest,
project_dir: &Path,
refresh: bool,
) -> Result<Vec<ResolvedInstall>, Error> {
let prefer_environment = manifest.target.as_ref().map(|target| target.environment);
let mut indices: HashMap<String, Index> = HashMap::new();
let mut workspace_memo: Option<Option<Workspace>> = None;
let mut queue: VecDeque<(String, Request, Option<String>)> = VecDeque::new();
for (alias, dependency) in &manifest.dependencies {
queue.push_back((
dependency_name(dependency).to_lowercase(),
request_for(dependency, manifest)?,
Some(alias.clone()),
));
}
let mut resolved: BTreeMap<String, (ResolvedInstall, String)> = BTreeMap::new();
while let Some((name, request, link)) = queue.pop_front() {
let req_text = match &request {
Request::Registry { req_text, .. } => req_text.clone(),
Request::Workspace => "*".to_string(),
};
let req = parse_version_req(&req_text)?;
if let Some((existing, first_req)) = resolved.get(&name) {
if req.matches(&existing.version) {
continue;
}
return Err(Error::DependencyConflict {
name,
first: first_req.clone(),
second: req_text,
});
}
let install = match request {
Request::Registry { index_url, .. } => {
let index = open_index(&mut indices, &index_url, refresh)?;
let package = index.resolve(&name, &req, prefer_environment)?;
for dependency in &package.dependencies {
queue.push_back((
dependency.name.clone(),
Request::Registry {
req_text: dependency.version_req.clone(),
index_url: dependency
.index_url
.clone()
.unwrap_or_else(|| index_url.clone()),
},
None,
));
}
ResolvedInstall {
name: name.clone(),
version: package.version,
environment: package.environment,
source: package.source,
index_url,
link: String::new(),
}
}
Request::Workspace => {
let workspace = workspace_context(&mut workspace_memo, manifest, project_dir)?
.ok_or_else(|| Error::NotInWorkspace(name.clone()))?;
let member = workspace
.member(&name)
.ok_or_else(|| Error::NoWorkspaceMember(name.clone()))?;
let version = semver::Version::parse(&member.manifest.package.version)?;
let environment = member
.manifest
.target
.as_ref()
.map(|target| target.environment)
.ok_or_else(|| Error::UnknownPackageEnvironment(name.clone()))?;
for dependency in member.manifest.dependencies.values() {
queue.push_back((
dependency_name(dependency).to_lowercase(),
request_for(dependency, &member.manifest)?,
None,
));
}
let path =
workspace::relative_path(&std::path::absolute(project_dir)?, &member.dir);
ResolvedInstall {
name: name.clone(),
version,
environment: Some(environment),
source: DownloadSource::Workspace { path },
index_url: "workspace".to_string(),
link: String::new(),
}
}
};
let link = link.unwrap_or_else(|| {
split_package_name(&name)
.map(|(_, short)| short.to_string())
.unwrap_or_else(|_| name.replace('/', "_"))
});
resolved.insert(name, (ResolvedInstall { link, ..install }, req_text));
}
Ok(resolved.into_values().map(|(install, _)| install).collect())
}
fn dependency_name(dependency: &Dependency) -> &str {
match dependency {
Dependency::Registry { name, .. } => name,
Dependency::Workspace { workspace, .. } => workspace,
}
}
fn request_for(dependency: &Dependency, owner: &Manifest) -> Result<Request, Error> {
Ok(match dependency {
Dependency::Registry { version, index, .. } => Request::Registry {
req_text: version.clone(),
index_url: owner.index_url(index.as_deref())?.to_string(),
},
Dependency::Workspace { .. } => Request::Workspace,
})
}
fn workspace_context<'memo>(
memo: &'memo mut Option<Option<Workspace>>,
manifest: &Manifest,
project_dir: &Path,
) -> Result<Option<&'memo Workspace>, Error> {
if memo.is_none() {
let workspace = if manifest.workspace_members().is_empty() {
workspace::containing(project_dir)?
} else {
Some(Workspace::open(project_dir)?)
};
*memo = Some(workspace);
}
Ok(memo.as_ref().expect("just memoized").as_ref())
}
fn open_index<'a>(
indices: &'a mut HashMap<String, Index>,
url: &str,
refresh: bool,
) -> Result<&'a Index, Error> {
if !indices.contains_key(url) {
indices.insert(url.to_string(), Index::open(url, refresh)?);
}
Ok(&indices[url])
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn write(dir: &Path, file: &str, contents: &str) {
let path = dir.join(file);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, contents).unwrap();
}
#[test]
fn workspace_dependencies_resolve_to_local_members() {
let base = std::env::temp_dir().join("lpm-test-resolver-workspace");
let _ = fs::remove_dir_all(&base);
write(
&base,
"lpm.toml",
"[package]\nname = \"acme/root\"\nversion = \"0.0.0\"\nprivate = true\n\n\
[target]\nenvironment = \"shared\"\nworkspace = [\"packages/*\"]\n",
);
write(
&base.join("packages/core"),
"lpm.toml",
"[package]\nname = \"acme/core\"\nversion = \"1.2.0\"\n\n\
[target]\nenvironment = \"shared\"\n",
);
write(
&base.join("packages/extra"),
"lpm.toml",
"[package]\nname = \"acme/extra\"\nversion = \"0.3.0\"\n\n\
[target]\nenvironment = \"shared\"\n\n\
[dependencies]\ncore = { workspace = \"acme/core\", version = \"^\" }\n",
);
let member_dir = base.join("packages/extra");
let manifest = Manifest::load_from(&member_dir.join("lpm.toml")).unwrap();
let installs = resolve(&manifest, &member_dir, false).unwrap();
assert_eq!(installs.len(), 1);
let core = &installs[0];
assert_eq!(core.name, "acme/core");
assert_eq!(core.version, semver::Version::new(1, 2, 0));
assert_eq!(core.environment, Some(Environment::Shared));
assert_eq!(core.link, "core");
assert!(matches!(
&core.source,
DownloadSource::Workspace { path } if path == "../core"
));
let root_manifest_text = "[package]\nname = \"acme/root\"\nversion = \"0.0.0\"\nprivate = true\n\n\
[target]\nenvironment = \"shared\"\nworkspace = [\"packages/*\"]\n\n\
[dependencies]\nextra = { workspace = \"acme/extra\" }\n";
write(&base, "lpm.toml", root_manifest_text);
let manifest = Manifest::load_from(&base.join("lpm.toml")).unwrap();
let installs = resolve(&manifest, &base, false).unwrap();
let names: Vec<_> = installs
.iter()
.map(|install| install.name.as_str())
.collect();
assert_eq!(names, ["acme/core", "acme/extra"]);
assert!(matches!(
&installs[1].source,
DownloadSource::Workspace { path } if path == "packages/extra"
));
let _ = fs::remove_dir_all(&base);
}
#[test]
fn workspace_dependency_outside_a_workspace_fails() {
let base = std::env::temp_dir().join("lpm-test-resolver-no-workspace");
let _ = fs::remove_dir_all(&base);
write(
&base,
"lpm.toml",
"[package]\nname = \"acme/lonely\"\nversion = \"0.1.0\"\n\n\
[dependencies]\ncore = { workspace = \"acme/core\" }\n",
);
let manifest = Manifest::load_from(&base.join("lpm.toml")).unwrap();
assert!(matches!(
resolve(&manifest, &base, false),
Err(Error::NotInWorkspace(_))
));
let _ = fs::remove_dir_all(&base);
}
}