use std::{
collections::{BTreeSet, VecDeque},
sync::Arc,
};
use crate::{
error::{LogoxideError, Result},
git::{Fetcher, GitRef},
library::open_locked,
lock::{LockedDownload, LockedPackage, Lockfile},
manifest::{Dependency, DetailedDependency, Manifest},
package::{Features, Package},
storage::{MemoryStorage, Storage},
};
pub trait Packages {
fn installed(&self, locked: &LockedPackage) -> Result<Option<Arc<dyn Storage>>>;
fn path_package(&self, relative: &str) -> Result<Arc<dyn Storage>>;
fn install(&self, locked: &LockedPackage, files: &dyn Storage) -> Result<()>;
fn repair(&self, locked: &LockedPackage, files: &dyn Storage) -> Result<()>;
}
pub struct ImportRequest<'a> {
pub name: &'a str,
pub import: &'a str,
pub kind: ImportKind<'a>,
}
pub enum ImportKind<'a> {
Pinned(&'a LockedPackage),
Fresh {
current: Option<(&'a LockedPackage, Arc<dyn Storage>)>,
},
}
pub struct ImportBuild {
pub files: MemoryStorage,
pub downloads: Vec<LockedDownload>,
pub upstream_version: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum Update {
#[default]
Nothing,
All,
Only(BTreeSet<String>),
}
impl Update {
fn allows(&self, id: &str) -> bool {
match self {
Update::Nothing => false,
Update::All => true,
Update::Only(ids) => ids.contains(id),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Frozen {
pub locked: bool,
pub offline: bool,
}
pub struct Resolver<'a> {
pub packages: &'a dyn Packages,
pub fetcher: &'a dyn Fetcher,
pub previous: Option<&'a Lockfile>,
pub update: Update,
pub repair: bool,
pub frozen: Frozen,
}
struct Want {
name: String,
dependency: DetailedDependency,
within: String,
from_git: bool,
from_remote: bool,
parent: Option<usize>,
parent_locked: Option<(String, Option<String>, String)>,
}
pub fn tag_version(tag: &str) -> Option<semver::Version> {
tag.strip_prefix('v').unwrap_or(tag).parse().ok()
}
pub fn folder_tag_version(tag: &str, folder: Option<&str>) -> Option<semver::Version> {
let own = match folder
.map(|f| f.trim_matches('/'))
.filter(|f| !f.is_empty())
{
Some(folder) => tag.strip_prefix(folder)?.strip_prefix('/')?,
None => tag,
};
if own.contains('/') {
return None;
}
tag_version(own)
}
pub fn not_semver_tags<'t>(
tags: impl IntoIterator<Item = &'t str>,
folder: Option<&str>,
) -> Vec<&'t str> {
let folder = folder
.map(|f| f.trim_matches('/'))
.filter(|f| !f.is_empty());
(tags.into_iter())
.filter(|tag| {
let own = match folder {
Some(folder) => tag.strip_prefix(folder).and_then(|t| t.strip_prefix('/')),
None => Some(*tag),
};
own.filter(|own| !own.contains('/'))
.map(|own| own.strip_prefix('v').unwrap_or(own))
.is_some_and(|own| own.starts_with(|c: char| c.is_ascii_digit()))
&& folder_tag_version(tag, folder).is_none()
})
.collect()
}
pub fn not_semver_note<'t>(
tags: impl IntoIterator<Item = &'t str>,
folder: Option<&str>,
) -> String {
let odd = not_semver_tags(tags, folder);
match odd.len() {
0 => String::new(),
n => format!(
" ({} {} no version: releases are tagged with three numbers, v1.0.0)",
odd.join(", "),
if n == 1 { "names" } else { "name" }
),
}
}
fn is_remote(locked: &LockedPackage) -> bool {
let remote = |url: &Option<String>| url.as_deref().is_some_and(|u| !crate::git::is_local(u));
remote(&locked.git) || remote(&locked.registry) || remote(&locked.archive)
}
fn relative_join(within: &str, path: &str) -> String {
if path.starts_with('/') {
return path.to_string();
}
let mut parts: Vec<&str> = within.split('/').filter(|p| !p.is_empty()).collect();
for part in path.split('/').filter(|p| !p.is_empty() && *p != ".") {
if part == ".." && parts.last().is_some_and(|p| *p != "..") {
parts.pop();
} else {
parts.push(part);
}
}
parts.join("/")
}
impl Resolver<'_> {
pub fn resolve(&self, manifest: &Manifest) -> Result<Lockfile> {
self.check_update(manifest)?;
if self.frozen.locked && self.previous.is_none() {
return Err(LogoxideError::Resolve(String::from(
"there's no logoxide.lock to keep (install without --locked to write one)",
)));
}
let lock = self.resolve_all(manifest)?;
if self.frozen.locked && self.previous != Some(&lock) {
return Err(LogoxideError::Resolve(String::from(
"logoxide.lock would change (the manifest asks for something it doesn't have): \
install without --locked to update it",
)));
}
Ok(lock)
}
fn may_fetch(&self, name: &str, what: &str) -> Result<()> {
if self.frozen.offline {
return Err(LogoxideError::Resolve(format!(
"`{name}`: {what} isn't installed, and offline nothing is fetched"
)));
}
Ok(())
}
fn may_choose(&self, name: &str) -> Result<()> {
let why = match (self.frozen.locked, self.frozen.offline) {
(true, _) => "logoxide.lock must stay as it is (--locked)",
(_, true) => "offline nothing is fetched to choose a version from",
_ => return Ok(()),
};
Err(LogoxideError::Resolve(format!(
"`{name}` isn't locked as the manifest asks for it, and {why}"
)))
}
fn resolve_all(&self, manifest: &Manifest) -> Result<Lockfile> {
let mut lock = Lockfile::default();
let mut todo: VecDeque<Want> = VecDeque::new();
for (name, dependency) in &manifest.dependencies {
let dependency = match dependency {
Dependency::Detailed(d) => d.clone(),
Dependency::Version(req) => {
return Err(LogoxideError::Resolve(format!(
"{name} = \"{req}\": there's no default registry; give `git`, `registry`, `path`, or `import`"
)));
}
};
todo.push_back(Want {
name: name.clone(),
dependency,
within: String::new(),
from_git: false,
from_remote: false,
parent: None,
parent_locked: None,
});
}
while let Some(want) = todo.pop_front() {
let mut locked = self.resolve_one(&want)?;
if want.parent.is_none() && want.name != locked.id {
locked.rename = Some(want.name.clone());
}
let name = locked.name();
let (index, changed) = match lock.packages.iter().position(|p| p.name() == name) {
Some(index) => {
let existing = &mut lock.packages[index];
let before = (existing.features.clone(), existing.default_features);
let mut features: BTreeSet<String> =
existing.features.iter().cloned().collect();
features.extend(locked.features.iter().cloned());
existing.features = features.into_iter().collect();
existing.default_features |= locked.default_features;
(
index,
before != (existing.features.clone(), existing.default_features),
)
}
None => {
lock.packages.push(locked);
(lock.packages.len() - 1, true)
}
};
let named_by = match want.parent {
Some(parent) => &mut lock.packages[parent].dependencies,
None => &mut lock.dependencies,
};
if !named_by.contains(&name) {
named_by.push(name);
}
if !changed {
continue;
}
let locked = lock.packages[index].clone();
let package = self.open(&locked)?;
let enabled = package.enabled().clone();
let within = match &locked.path {
Some(path) => path.clone(),
None => String::new(),
};
for (dep_name, dependency) in &package.manifest().dependencies {
let Dependency::Detailed(dependency) = dependency else {
return Err(LogoxideError::Resolve(format!(
"{}: dependency `{dep_name}` has no `git`, `registry`, `path`, or `import`",
locked.name()
)));
};
if dependency.optional && !enabled.dependencies.contains(dep_name) {
continue;
}
let mut dependency = dependency.clone();
if let Some(features) = enabled.dependency_features.get(dep_name) {
dependency.features.extend(features.iter().cloned());
}
todo.push_back(Want {
name: dep_name.clone(),
dependency,
within: within.clone(),
from_git: locked.git.is_some() || locked.registry.is_some(),
from_remote: want.from_remote || is_remote(&locked),
parent: Some(index),
parent_locked: Some((locked.id.clone(), locked.rename.clone(), locked.name())),
});
}
}
lock.packages
.sort_by(|a, b| (&a.id, &a.version, &a.rename).cmp(&(&b.id, &b.version, &b.rename)));
Ok(lock)
}
fn check_update(&self, manifest: &Manifest) -> Result<()> {
let Update::Only(names) = &self.update else {
return Ok(());
};
let locked = |name: &String| {
(self.previous.into_iter().flat_map(|l| &l.packages))
.any(|p| p.id == *name || p.dependency_name() == name)
};
let unknown: Vec<&str> = (names.iter())
.filter(|name| !manifest.dependencies.contains_key(*name) && !locked(name))
.map(String::as_str)
.collect();
if unknown.is_empty() {
return Ok(());
}
let known: Vec<&str> = manifest.dependencies.keys().map(String::as_str).collect();
Err(LogoxideError::Resolve(format!(
"nothing here is named {} (the manifest has {})",
unknown.join(", "),
match known.is_empty() {
true => String::from("no dependencies"),
false => known.join(", "),
}
)))
}
fn open(&self, locked: &LockedPackage) -> Result<Package> {
let storage = self
.packages
.installed(locked)?
.ok_or_else(|| LogoxideError::NotFound(format!("{} (not installed)", locked.name())))?;
open_locked(storage, locked)
}
fn resolve_one(&self, want: &Want) -> Result<LockedPackage> {
let d = &want.dependency;
crate::manifest::check_id("dependency", &want.name)?;
if let Some(package) = &d.package {
crate::manifest::check_id(&format!("dependency `{}`: package", want.name), package)?;
}
if want.from_remote {
let local = match &d.path {
Some(path) => Some(path),
None => [&d.git, &d.registry]
.into_iter()
.flatten()
.find(|url| crate::git::is_local(&crate::git::expand_url(url))),
};
if let Some(local) = local {
return Err(LogoxideError::Resolve(format!(
"`{}`: a package from another machine names `{local}`, on this one",
want.name
)));
}
if let Some(url) =
(d.git.as_deref()).filter(|u| u.trim().to_ascii_lowercase().starts_with("git://"))
{
return Err(LogoxideError::Resolve(format!(
"`{}`: a package from another machine names `{url}`, over git's unencrypted protocol",
want.name
)));
}
}
let mut features: Vec<String> = d.features.clone();
features.sort();
features.dedup();
if let Some(import) = &d.import {
let mut locked = self.import_release(want, import)?;
locked.features = features;
locked.default_features = d.default_features;
return Ok(locked);
}
if let Some(path) = &d.path {
if want.from_git {
return Err(LogoxideError::Resolve(format!(
"`{}`: a git or registry package can't have path dependencies",
want.name
)));
}
let relative = relative_join(&want.within, path);
let storage = self.packages.path_package(&relative)?;
let names: Vec<&str> = features.iter().map(String::as_str).collect();
let package = Package::open(
storage,
Features {
features: &names,
default: d.default_features,
},
)
.map_err(|e| LogoxideError::Resolve(format!("{} ({relative}): {e}", want.name)))?;
let info = package.info();
self.check_id(&want.name, d, &info.id)?;
return Ok(LockedPackage {
path: Some(relative),
features,
default_features: d.default_features,
..LockedPackage::new(&info.id, info.version.clone())
});
}
if let Some(registry) = &d.registry {
if d.git.is_some() {
return Err(LogoxideError::Resolve(format!(
"`{}`: give `git` or `registry`, not both",
want.name
)));
}
let mut locked = self.registry_release(want, registry)?;
locked.features = features;
locked.default_features = d.default_features;
return Ok(locked);
}
let url = d.git.as_deref().ok_or_else(|| {
LogoxideError::Resolve(format!(
"`{}`: give `git`, `registry`, `path`, or `import`",
want.name
))
})?;
let url = &crate::git::expand_url(url);
let kept = self.kept(want, |p| {
let same = match (&d.tag, &d.branch, &d.rev) {
(Some(tag), _, _) => p.tag.as_ref() == Some(tag),
(_, Some(branch), _) => p.branch.as_ref() == Some(branch),
(_, _, Some(rev)) => p.rev.as_ref() == Some(rev),
_ => {
p.branch.is_none()
&& p.rev.is_none()
&& d.version.as_ref().is_none_or(|req| req.matches(&p.version))
}
};
p.id == d.package_id(&want.name)
&& p.git.as_deref() == Some(url.as_str())
&& p.folder.as_deref().map(|f| f.trim_matches('/'))
== d.folder.as_deref().map(|f| f.trim_matches('/'))
&& p.commit.is_some()
&& same
});
let (reference, tag) = match kept {
Some(kept) => (
GitRef::Rev(kept.commit.clone().unwrap_or_default()),
kept.tag.clone(),
),
None => {
self.may_choose(&want.name)?;
self.reference(&want.name, url, d)?
}
};
let mut locked = self.install(&want.name, d, url, &reference, kept)?;
locked.tag = tag;
locked.branch = d.branch.clone();
locked.rev = d.rev.clone();
locked.features = features;
locked.default_features = d.default_features;
Ok(locked)
}
fn kept(&self, want: &Want, fits: impl Fn(&LockedPackage) -> bool) -> Option<&LockedPackage> {
match self.update.allows(&want.name) {
true => None,
false => self.locked_before(want, fits),
}
}
fn locked_before(
&self,
want: &Want,
fits: impl Fn(&LockedPackage) -> bool,
) -> Option<&LockedPackage> {
let previous = self.previous?;
let asker: Option<&Vec<String>> = match &want.parent_locked {
None => Some(&previous.dependencies),
Some((id, rename, name)) => (previous.named(name))
.or_else(|| (previous.packages.iter()).find(|p| p.id == *id && p.rename == *rename))
.map(|p| &p.dependencies),
};
let theirs = (asker.into_iter().flatten())
.filter_map(|name| previous.named(name))
.find(|p| fits(p));
theirs.or_else(|| previous.packages.iter().find(|p| fits(p)))
}
fn reference(
&self,
name: &str,
url: &str,
d: &DetailedDependency,
) -> Result<(GitRef, Option<String>)> {
if let Some(tag) = &d.tag {
return Ok((GitRef::Tag(tag.clone()), Some(tag.clone())));
}
if let Some(branch) = &d.branch {
return Ok((GitRef::Branch(branch.clone()), None));
}
if let Some(rev) = &d.rev {
return Ok((GitRef::Rev(rev.clone()), None));
}
let tags = self.fetcher.tags(url)?;
let newest = tags
.iter()
.filter_map(|(tag, _)| Some((folder_tag_version(tag, d.folder.as_deref())?, tag)))
.filter(|(version, _)| match &d.version {
Some(req) => req.matches(version),
None => version.pre.is_empty(),
})
.max_by(|a, b| a.0.cmp(&b.0));
match (newest, &d.version) {
(Some((_, tag)), _) => Ok((GitRef::Tag(tag.clone()), Some(tag.clone()))),
(None, Some(req)) => {
let mut message = match &d.folder {
Some(folder) => {
format!("`{name}`: no tag of {url} for {folder}/ matches {req}")
}
None => format!("`{name}`: no tag of {url} matches {req}"),
};
message.push_str(¬_semver_note(
tags.iter().map(|(t, _)| t.as_str()),
d.folder.as_deref(),
));
Err(LogoxideError::Resolve(message))
}
(None, None) => Ok((GitRef::Head, None)),
}
}
fn install(
&self,
name: &str,
d: &DetailedDependency,
url: &str,
reference: &GitRef,
kept: Option<&LockedPackage>,
) -> Result<LockedPackage> {
if let Some(kept) = kept
&& let Some(installed) = self.packages.installed(kept)?
{
let checksum = crate::package::checksum(installed.as_ref())?;
let changed = kept.checksum.as_ref().is_some_and(|c| *c != checksum);
if changed && !self.repair {
return Err(LogoxideError::Changed {
id: kept.label(),
dir: crate::library::library_dir(kept),
expected: kept.checksum.clone().unwrap_or_default(),
actual: checksum,
});
}
if !changed {
return Ok(kept.clone());
}
}
let what = kept.map_or_else(|| url.to_string(), LockedPackage::label);
self.may_fetch(name, &what)?;
let fetched = MemoryStorage::new();
let commit = (self.fetcher.fetch(url, reference, &fetched)).map_err(|e| match kept {
Some(kept) if e.is_fetch() => e.with_fetch_note(&format!(
"(the locked commit {} of {} may no longer be on the host: if its tag was moved \
or its branch rewritten, `lx update {name}` locks what's there now)",
kept.commit.as_deref().unwrap_or("?"),
kept.label()
)),
_ => e,
})?;
let fetched = match d.folder.as_deref().map(|f| f.trim_matches('/')) {
Some(folder) if !folder.is_empty() => {
crate::release::within(&fetched, &format!("{folder}/"))?
}
_ => fetched,
};
let storage = crate::package::only_declared(&fetched)?;
let manifest =
crate::manifest::Manifest::parse(&storage.read_string(crate::manifest::MANIFEST)?)?;
let info = manifest.package.clone().ok_or_else(|| {
LogoxideError::Resolve(format!(
"`{name}`: {url} has a logoxide.toml without [package]"
))
})?;
self.check_id(name, d, &info.id)?;
if let GitRef::Tag(tag) = reference
&& let Some(version) = folder_tag_version(tag, d.folder.as_deref())
&& version != info.version
{
return Err(LogoxideError::Resolve(format!(
"`{name}`: tag {tag} has version {} in its logoxide.toml",
info.version
)));
}
let checksum = crate::package::checksum(&storage)?;
if let Some(kept) = kept
&& kept.checksum.as_ref().is_some_and(|c| *c != checksum)
{
return Err(LogoxideError::Checksum {
id: kept.name(),
expected: kept.checksum.clone().unwrap_or_default(),
actual: checksum,
});
}
let locked = LockedPackage {
git: Some(url.to_string()),
tag: match reference {
GitRef::Tag(tag) => Some(tag.clone()),
_ => None,
},
branch: match reference {
GitRef::Branch(branch) => Some(branch.clone()),
_ => None,
},
rev: None,
commit: Some(commit),
folder: d.folder.as_ref().map(|f| f.trim_matches('/').to_string()),
checksum: Some(checksum),
..LockedPackage::new(&info.id, info.version.clone())
};
let mut locked = locked;
if let Some(kept) = kept {
locked.tag = kept.tag.clone();
locked.branch = kept.branch.clone();
locked.rev = kept.rev.clone();
}
match self.packages.installed(&locked)? {
Some(installed) => {
let installed = crate::package::checksum(installed.as_ref())?;
if Some(&installed) != locked.checksum.as_ref() {
if !self.repair {
return Err(LogoxideError::Checksum {
id: locked.name(),
expected: installed,
actual: locked.checksum.clone().unwrap_or_default(),
});
}
self.packages.repair(&locked, &storage)?;
}
}
None => self.packages.install(&locked, &storage)?,
}
Ok(locked)
}
fn registry_release(&self, want: &Want, registry: &str) -> Result<LockedPackage> {
let d = &want.dependency;
let id = d.package_id(&want.name);
crate::manifest::check_id("package", id)?;
let kept = self.kept(want, |p| {
p.id == id
&& p.registry.as_deref() == Some(registry)
&& p.archive.is_some()
&& p.sha256.is_some()
&& p.checksum.is_some()
&& d.version.as_ref().is_none_or(|req| req.matches(&p.version))
});
let mut locked = match kept {
Some(kept) => kept.clone(),
None => {
self.may_choose(&want.name)?;
let text = self.fetcher.download(registry)?;
let catalog =
crate::catalog::Catalog::parse(registry, &String::from_utf8_lossy(&text))?;
let entry = catalog.find(id)?.ok_or_else(|| {
LogoxideError::Resolve(format!("`{}`: {registry} has no `{id}`", want.name))
})?;
let local = (entry.releases.iter())
.flat_map(|r| std::iter::once(&r.url).chain(&r.mirrors))
.find(|url| crate::git::is_local(url));
if !crate::git::is_local(registry)
&& let Some(local) = local
{
return Err(LogoxideError::Resolve(format!(
"`{}`: {registry} lists `{local}`, on this machine",
want.name
)));
}
let release = entry.release(d.version.as_ref()).ok_or_else(|| {
let wanted = (d.version.as_ref()).map_or(String::from("a release"), |r| {
format!("a release matching {r}")
});
LogoxideError::Resolve(format!(
"`{}`: {registry} has no {wanted} of `{id}`",
want.name
))
})?;
let checksum = release.checksum.clone().ok_or_else(|| {
LogoxideError::Resolve(format!(
"`{}`: {registry} lists {id} {} without a `checksum` (republish it with \
a newer lx)",
want.name, release.version
))
})?;
let version = release.version.parse().map_err(|e| {
LogoxideError::Resolve(format!("{id} {}: {e}", release.version))
})?;
LockedPackage {
registry: Some(registry.to_string()),
archive: Some(release.url.clone()),
mirrors: release.mirrors.clone(),
sha256: Some(release.sha256.clone()),
checksum: Some(checksum),
..LockedPackage::new(id, version)
}
}
};
locked.rename = None;
locked.dependencies = vec![];
let installed = self.packages.installed(&locked)?;
if let Some(installed) = &installed {
let checksum = crate::package::checksum(installed.as_ref())?;
let changed = locked.checksum.as_ref().is_some_and(|c| *c != checksum);
if changed && !self.repair {
return Err(LogoxideError::Changed {
id: locked.label(),
dir: crate::library::library_dir(&locked),
expected: locked.checksum.clone().unwrap_or_default(),
actual: checksum,
});
}
if !changed {
locked.checksum = Some(checksum);
return Ok(locked);
}
}
self.may_fetch(&want.name, &locked.label())?;
let urls: Vec<String> = (locked.archive.iter().chain(&locked.mirrors))
.cloned()
.collect();
let (url, bytes) = self.fetcher.download_any(&urls, &|url, bytes| {
let sha256 = crate::publish::sha256(bytes);
match Some(&sha256) == locked.sha256.as_ref() {
true => Ok(()),
false => Err(LogoxideError::Checksum {
id: format!("{} ({url})", locked.label()),
expected: locked.sha256.clone().unwrap_or_default(),
actual: sha256,
}),
}
})?;
let unpacked = MemoryStorage::new();
crate::net::archive::unpack(&bytes, &unpacked)?;
let storage = crate::package::only_declared(&unpacked)?;
let manifest =
crate::manifest::Manifest::parse(&storage.read_string(crate::manifest::MANIFEST)?)?;
let info = manifest.package.ok_or_else(|| {
LogoxideError::Resolve(format!("`{}`: {url} has no [package]", want.name))
})?;
self.check_id(&want.name, d, &info.id)?;
if info.version != locked.version {
return Err(LogoxideError::Resolve(format!(
"`{}`: {url} is version {}, listed as {}",
want.name, info.version, locked.version
)));
}
let checksum = crate::package::checksum(&storage)?;
if locked.checksum.as_ref().is_some_and(|c| *c != checksum) {
return Err(LogoxideError::Checksum {
id: locked.label(),
expected: locked.checksum.clone().unwrap_or_default(),
actual: checksum,
});
}
locked.checksum = Some(checksum);
match installed {
Some(_) => self.packages.repair(&locked, &storage)?,
None => self.packages.install(&locked, &storage)?,
}
Ok(locked)
}
fn import_release(&self, want: &Want, import: &str) -> Result<LockedPackage> {
let d = &want.dependency;
let name = want.name.as_str();
let id = d.package_id(name);
crate::manifest::check_id("package", id)?;
let fits = |p: &LockedPackage| {
p.id == id && p.import.as_deref() == Some(import) && p.checksum.is_some()
};
let kept = self.kept(want, |p| {
fits(p) && d.version.as_ref().is_none_or(|req| req.matches(&p.version))
});
if let Some(kept) = kept {
let mut locked = kept.clone();
locked.rename = None;
locked.dependencies = vec![];
let installed = self.packages.installed(&locked)?;
if let Some(installed) = &installed {
let checksum = crate::package::checksum(installed.as_ref())?;
let changed = locked.checksum.as_ref().is_some_and(|c| *c != checksum);
if changed && !self.repair {
return Err(LogoxideError::Changed {
id: locked.label(),
dir: crate::library::library_dir(&locked),
expected: locked.checksum.clone().unwrap_or_default(),
actual: checksum,
});
}
if !changed {
return Ok(locked);
}
}
self.may_fetch(name, &locked.label())?;
let build = self.fetcher.import(&ImportRequest {
name,
import,
kind: ImportKind::Pinned(kept),
})?;
let storage = self.built(name, d, &build)?;
let info = Manifest::parse(&storage.read_string(crate::manifest::MANIFEST)?)?;
let version = info.package.map(|p| p.version);
let checksum = crate::package::checksum(&storage)?;
if version.as_ref() != Some(&locked.version)
|| Some(&checksum) != locked.checksum.as_ref()
{
return Err(LogoxideError::Resolve(format!(
"`{name}`: {import} made again isn't the locked {} (its files' checksum is \
{checksum}, locked {}): the import has changed since; `lx update {name}` \
locks what it makes now",
locked.label(),
locked.checksum.as_deref().unwrap_or("none"),
)));
}
match installed {
Some(_) => self.packages.repair(&locked, &storage)?,
None => self.packages.install(&locked, &storage)?,
}
return Ok(locked);
}
self.may_choose(name)?;
let before = self.locked_before(want, fits).cloned();
let current = match &before {
Some(before) => (self.packages.installed(before)?).map(|files| (before, files)),
None => None,
};
let build = self.fetcher.import(&ImportRequest {
name,
import,
kind: ImportKind::Fresh { current },
})?;
let storage = self.built(name, d, &build)?;
let info = Manifest::parse(&storage.read_string(crate::manifest::MANIFEST)?)?
.package
.ok_or_else(|| {
LogoxideError::Resolve(format!("`{name}`: {import} made no [package]"))
})?;
if let Some(req) = &d.version
&& !req.matches(&info.version)
{
return Err(LogoxideError::Resolve(format!(
"`{name}`: {import} is version {} now, which `version = \"{req}\"` doesn't allow",
info.version
)));
}
let locked = LockedPackage {
import: Some(import.to_string()),
upstream_version: build.upstream_version.clone(),
downloads: build.downloads.clone(),
checksum: Some(crate::package::checksum(&storage)?),
..LockedPackage::new(&info.id, info.version)
};
match self.packages.installed(&locked)? {
Some(installed) => {
let checksum = crate::package::checksum(installed.as_ref())?;
if Some(&checksum) != locked.checksum.as_ref() {
if !self.repair {
return Err(LogoxideError::Changed {
id: locked.label(),
dir: crate::library::library_dir(&locked),
expected: locked.checksum.clone().unwrap_or_default(),
actual: checksum,
});
}
self.packages.repair(&locked, &storage)?;
}
}
None => self.packages.install(&locked, &storage)?,
}
Ok(locked)
}
fn built(
&self,
name: &str,
d: &DetailedDependency,
build: &ImportBuild,
) -> Result<MemoryStorage> {
let storage = crate::package::only_declared(&build.files)?;
let manifest = Manifest::parse(&storage.read_string(crate::manifest::MANIFEST)?)?;
let info = manifest.package.ok_or_else(|| {
LogoxideError::Resolve(format!("`{name}`: the import made no [package]"))
})?;
self.check_id(name, d, &info.id)?;
Ok(storage)
}
fn check_id(&self, name: &str, d: &DetailedDependency, id: &str) -> Result<()> {
match &d.package {
None if name != id => Err(LogoxideError::Resolve(format!(
"dependency `{name}` is the package `{id}`: name it `{id}`, or add `package = \"{id}\"`"
))),
Some(package) if package != id => Err(LogoxideError::Resolve(format!(
"dependency `{name}` asks for the package `{package}`, but it's `{id}`"
))),
_ => Ok(()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tags_and_paths() {
assert_eq!(tag_version("v1.2.0"), Some("1.2.0".parse().unwrap()));
assert_eq!(tag_version("1.2.0-beta.1").unwrap().pre.as_str(), "beta.1");
assert_eq!(tag_version("release"), None);
assert_eq!(relative_join("", "Bibles/Mine"), "Bibles/Mine");
assert_eq!(relative_join("Bibles/Mine", "../Lexicon"), "Bibles/Lexicon");
assert_eq!(relative_join("Bibles/Mine", "/srv/Lexicon"), "/srv/Lexicon");
}
#[test]
fn tags_that_look_like_releases() {
let tags = [
"v1.0",
"2.1",
"v1.0.0",
"release",
"v2",
"books/v1.0",
"books/v1.0.0",
];
assert_eq!(not_semver_tags(tags, None), ["v1.0", "2.1", "v2"]);
assert_eq!(not_semver_tags(tags, Some("books")), ["books/v1.0"]);
}
}