mod cache_only_store;
mod fetching_store;
mod library_store;
mod user_git_store;
use std::path::{Path, PathBuf};
use super::checkout_bounds::{CheckoutBounds, RepositoryAddress};
use super::exit_status::ExitStatus;
use super::program_file::ProgramFile;
use super::terminal::Terminal;
use crate::build::StoreError;
use crate::diagnostic::{Diagnostic, Diagnostics};
use crate::language::Language;
use crate::modules::{LibraryRoot, LibraryTable};
use crate::project::{
CrateDependency, CrateSet, LibraryDependency, LibrarySource, Project, ProjectProblems,
};
use crate::source::Span;
pub use cache_only_store::CacheOnlyStore;
pub use fetching_store::FetchingStore;
pub use library_store::LibraryStore;
pub struct LibraryWalk<'t, S: LibraryStore> {
terminal: &'t mut Terminal,
seen: Vec<SeenLibrary>,
roots: Vec<LibraryRoot>,
store: S,
crates: CrateSet,
crate_library: Option<String>,
}
pub struct WalkedLibraries {
pub project: Project,
pub table: LibraryTable,
pub crates: Vec<CrateDependency>,
pub dev_crates: Vec<CrateDependency>,
pub crate_library: Option<String>,
}
#[derive(Clone, Copy)]
struct Listing<'l> {
entry: Option<&'l str>,
checkout: Option<&'l CheckoutBounds>,
}
impl Listing<'_> {
const PROGRAM: Self = Self {
entry: None,
checkout: None,
};
}
struct VisitedLibrary<'v> {
root: &'v Path,
library: &'v LibraryDependency,
project: &'v Project,
checkout: Option<&'v CheckoutBounds>,
}
struct SeenLibrary {
name: String,
root: PathBuf,
span: Span,
}
impl<'t, S: LibraryStore> LibraryWalk<'t, S> {
const LIB_KEY: &'static str = ":lib";
const PATH_KEY: &'static str = ":path";
const GIT_KEY: &'static str = ":git";
pub fn walk(
terminal: &'t mut Terminal,
root: &Path,
store: S,
) -> Result<WalkedLibraries, ExitStatus> {
let project = terminal.load_project(&root.join(Language::project_file_name()))?;
let mut walk = Self {
terminal,
seen: Vec::new(),
roots: Vec::new(),
store,
crates: CrateSet::default(),
crate_library: None,
};
walk.add_crates(root, &project)?;
walk.visit_all(root, &project, Listing::PROGRAM)?;
let dev_crates = walk.dev_crates(root, &project)?;
Ok(WalkedLibraries {
table: LibraryTable::new(Self::names(&project), walk.roots),
project,
dev_crates,
crates: walk.crates.into_crates(),
crate_library: walk.crate_library,
})
}
fn add_crates(&mut self, root: &Path, project: &Project) -> Result<(), ExitStatus> {
for dependency in &project.dependencies {
let resolved = ProgramFile::resolved_crate(dependency, root);
if let Err(problem) = self.crates.add(resolved) {
return Err(self.report(problem));
}
}
Ok(())
}
fn dev_crates(
&self,
root: &Path,
project: &Project,
) -> Result<Vec<CrateDependency>, ExitStatus> {
let mut every_crate = self.crates.clone();
project
.dev_dependencies
.iter()
.map(|dependency| {
let resolved = ProgramFile::resolved_crate(dependency, root);
every_crate
.add(resolved.clone())
.map_err(|problem| self.report(problem))?;
Ok(resolved)
})
.collect()
}
fn visit_all(
&mut self,
declaring_root: &Path,
project: &Project,
listing: Listing<'_>,
) -> Result<(), ExitStatus> {
project.libraries.iter().try_for_each(|library| {
let library_root = self.library_directory(declaring_root, library)?;
let crates_before = self.crates.count();
self.visit(&library_root, library, listing)?;
self.note_crate_library(library, listing, crates_before);
Ok(())
})
}
fn note_crate_library(
&mut self,
library: &LibraryDependency,
listing: Listing<'_>,
crates_before: usize,
) {
let listed_by_program = listing.entry.is_none();
let added_crates = self.crates.count() > crates_before;
if listed_by_program && added_crates && self.crate_library.is_none() {
self.crate_library = Some(library.name.clone());
}
}
fn library_directory(
&mut self,
declaring_root: &Path,
library: &LibraryDependency,
) -> Result<PathBuf, ExitStatus> {
match &library.source {
LibrarySource::Path(path) => Ok(declaring_root.join(path)),
LibrarySource::Git { url, commit } => {
let repository = Self::repository_location(declaring_root, url);
self.checkout(library, &repository, commit)
}
}
}
fn repository_location(declaring_root: &Path, url: &str) -> String {
match RepositoryAddress::of(url) {
RepositoryAddress::Remote(remote) => remote.to_string(),
RepositoryAddress::Local(path) => declaring_root.join(path).display().to_string(),
}
}
fn checkout(
&mut self,
library: &LibraryDependency,
url: &str,
commit: &str,
) -> Result<PathBuf, ExitStatus> {
self.store
.checkout(url, commit)
.map_err(|error| self.report_store(library, error))
}
fn report_store(&self, library: &LibraryDependency, error: StoreError) -> ExitStatus {
match error {
StoreError::Fetch(reason) => self.report(ProjectProblems::unavailable_git_library(
&library.name,
library.name_span,
&reason,
)),
StoreError::NotCached => self.report_environment(&format!(
"git library `{}` is not in the cache\nhelp: run `{} check` to fetch it",
library.name,
Language::NAME
)),
StoreError::NoCacheDirectory => self.report_environment(
"no cache directory for git libraries: set `XDG_CACHE_HOME` or `HOME`",
),
StoreError::GitMissing(error) => self
.report_environment(&format!("cannot run `git` to fetch git libraries: {error}")),
StoreError::Unwritable(path, error) => {
self.report_environment(&format!("cannot write `{}`: {error}", path.display()))
}
}
}
fn report_environment(&self, message: &str) -> ExitStatus {
self.terminal.diagnostic_output().write_error(message);
ExitStatus::Environment
}
fn visit(
&mut self,
library_root: &Path,
library: &LibraryDependency,
listing: Listing<'_>,
) -> Result<(), ExitStatus> {
let project_file = self.project_file_of(library_root, library)?;
if !self.is_first_visit(library_root, library)? {
return Ok(());
}
let project = self.terminal.load_project(&project_file)?;
let own_checkout = Self::own_checkout(library_root, library);
let visited = VisitedLibrary {
root: library_root,
library,
project: &project,
checkout: own_checkout.as_ref().or(listing.checkout),
};
self.record(&visited, listing.entry)?;
let nested = Listing {
entry: Some(&library.name),
checkout: visited.checkout,
};
self.visit_all(library_root, &project, nested)
}
fn project_file_of(
&self,
library_root: &Path,
library: &LibraryDependency,
) -> Result<PathBuf, ExitStatus> {
let project_file = library_root.join(Language::project_file_name());
if project_file.is_file() {
return Ok(project_file);
}
Err(self.report(ProjectProblems::library_without_project_file(
&library.name,
library.name_span,
&project_file.display().to_string(),
)))
}
fn is_first_visit(
&mut self,
library_root: &Path,
library: &LibraryDependency,
) -> Result<bool, ExitStatus> {
let identity = ProgramFile::canonical(library_root).unwrap_or(library_root.to_path_buf());
if self.is_seen(library, &identity)? {
return Ok(false);
}
self.seen.push(SeenLibrary {
name: library.name.clone(),
root: identity,
span: library.name_span,
});
Ok(true)
}
fn own_checkout(library_root: &Path, library: &LibraryDependency) -> Option<CheckoutBounds> {
let fetched = matches!(library.source, LibrarySource::Git { .. });
fetched.then(|| CheckoutBounds::of(library_root))
}
fn record(
&mut self,
visited: &VisitedLibrary<'_>,
lister: Option<&str>,
) -> Result<(), ExitStatus> {
if let Some(bounds) = visited.checkout {
self.keep_inside_checkout(bounds, visited.root, visited.library, visited.project)?;
}
self.add_crates(visited.root, visited.project)?;
let root = self.library_root(visited.library, visited.root, visited.project)?;
let root = match visited.checkout {
Some(bounds) => root.within_checkout(bounds.root().to_path_buf()),
None => root,
};
self.roots.push(root.listed_by(lister.map(str::to_string)));
Ok(())
}
fn keep_inside_checkout(
&self,
bounds: &CheckoutBounds,
library_root: &Path,
library: &LibraryDependency,
project: &Project,
) -> Result<(), ExitStatus> {
if !bounds.contains(&library_root.join(&project.library_root)) {
return Err(self.report(ProjectProblems::path_outside_checkout(
&library.name,
Self::LIB_KEY,
library.name_span,
)));
}
let outside = project
.libraries
.iter()
.find_map(|nested| Self::reaching_outside(bounds, library_root, nested));
if let Some((nested, key)) = outside {
return Err(self.report(ProjectProblems::path_outside_checkout(
&nested.name,
key,
nested.name_span,
)));
}
match Self::crate_outside(bounds, library_root, project) {
Some(dependency) => Err(self.report(ProjectProblems::crate_path_outside_checkout(
&dependency.name,
dependency.name_span,
))),
None => Ok(()),
}
}
fn crate_outside<'p>(
bounds: &CheckoutBounds,
library_root: &Path,
project: &'p Project,
) -> Option<&'p CrateDependency> {
project.dependencies.iter().find(|dependency| {
dependency
.path
.as_ref()
.is_some_and(|path| !bounds.contains(&library_root.join(path)))
})
}
fn reaching_outside<'d>(
bounds: &CheckoutBounds,
library_root: &Path,
library: &'d LibraryDependency,
) -> Option<(&'d LibraryDependency, &'static str)> {
let (path, key) = match &library.source {
LibrarySource::Path(path) => (path.clone(), Self::PATH_KEY),
LibrarySource::Git { url, .. } => match RepositoryAddress::of(url) {
RepositoryAddress::Remote(_) => return None,
RepositoryAddress::Local(path) => (path, Self::GIT_KEY),
},
};
(!bounds.contains(&library_root.join(path))).then_some((library, key))
}
fn is_seen(&self, library: &LibraryDependency, identity: &Path) -> Result<bool, ExitStatus> {
let Some(seen) = self.seen.iter().find(|seen| seen.name == library.name) else {
return Ok(false);
};
if seen.root == identity {
return Ok(true);
}
Err(self.report(ProjectProblems::library_at_two_sources(
&library.name,
library.name_span,
seen.span,
)))
}
fn library_root(
&self,
library: &LibraryDependency,
library_root: &Path,
project: &Project,
) -> Result<LibraryRoot, ExitStatus> {
let root_file = library_root.join(&project.library_root);
if !root_file.is_file() {
let project_file = library_root.join(Language::project_file_name());
return Err(self.report(ProjectProblems::library_without_root_module(
&library.name,
library.name_span,
&root_file.display().to_string(),
&project_file.display().to_string(),
)));
}
let source_root = ProgramFile::directory_of(&root_file);
Ok(LibraryRoot::new(
library.name.clone(),
source_root.clone(),
source_root,
ProgramFile::file_stem(&root_file),
Self::names(project),
))
}
fn report(&self, problem: Diagnostic) -> ExitStatus {
let mut problems = Diagnostics::default();
problems.push(problem);
self.terminal.report(&problems)
}
fn names(project: &Project) -> Vec<String> {
project
.libraries
.iter()
.map(|library| library.name.clone())
.collect()
}
}