pub mod errors;
#[cfg(test)]
mod tests;
use std::{
collections::HashSet,
path::{Path, PathBuf},
};
use color_eyre::eyre::{Context, ContextCompat};
use crate::{
error::{OptionUserError, Report, UserError, WrapUserError},
scell::{
Link, SCell, SCellContainer,
compile::errors::{
CircularTargets, CopySrcNotFound, DirNotFoundFromStmt, DockerfileNotFound,
FileLoadFromStmt, MissingEntrypoint, MissingHangStmt, MissingShellStmt, MissingTarget,
MountHostDirNotFound,
},
image::SCellImage,
link::RootNode,
service::Service,
types::{
SCellFile,
extra_arguments::SCellExtraArguments,
name::TargetName,
target::{
TargetStmt,
config::ConfigStmt,
copy::CopyStmt,
from::{FromStmt, target_ref::TargetRef},
hang::HangStmt,
services::{ServiceName, ServicesStmt},
shell::ShellStmt,
},
},
},
scell_home_dir,
};
const SCELL_DEFAULT_ENTRY_POINT: &str = "main";
type CompiledTarget = (
Vec<Link>,
Option<ShellStmt>,
Option<HangStmt>,
Option<ConfigStmt>,
Vec<(ServiceName, Service)>,
);
impl SCell {
pub fn compile<P: AsRef<Path>>(
path: P,
entry: Option<TargetName>,
) -> color_eyre::Result<Self> {
let scell_extra_args = SCellExtraArguments::from_path(&path)?;
let mut scell_f = SCellFile::from_path(path, &scell_extra_args)?;
let entry_point_target = entry.map_or_else(
|| {
SCELL_DEFAULT_ENTRY_POINT.parse().context(format!(
"'{SCELL_DEFAULT_ENTRY_POINT}' must be a valid Shell-Cell name"
))
},
Ok,
)?;
let entry_point =
scell_f
.targets
.remove(&entry_point_target)
.user_err(MissingEntrypoint(
scell_f.location.clone(),
entry_point_target.clone(),
))?;
let (links, shell, hang, config, services) =
compile_target(scell_f, entry_point, entry_point_target)?;
let mut report = Report::new();
if shell.is_none() {
report.add_error(UserError::wrap(MissingShellStmt));
}
if hang.is_none() {
report.add_error(UserError::wrap(MissingHangStmt));
}
report.check()?;
color_eyre::eyre::ensure!(
links.len() >= 2,
"It must be at least two links in the target chain"
);
let image = SCellImage::new(links, hang.context("'hang' cannot be 'None'")?)?;
let container = SCellContainer::new(config);
Ok(Self {
image,
container,
shell: shell.context("'shell' cannot be 'None'")?,
services,
})
}
}
fn compile_target(
mut walk_f: SCellFile,
mut walk_target: TargetStmt,
mut walk_target_name: TargetName,
) -> color_eyre::Result<CompiledTarget> {
let mut visited_targets = HashSet::new();
let mut links = Vec::new();
let mut shell = None;
let mut hang = None;
let mut config = None;
let services = resolve_services(walk_target.services, &walk_f)?;
loop {
if shell.is_none() {
shell = walk_target.shell;
}
if hang.is_none() {
hang = walk_target.hang;
}
if config.is_none() {
config = resolve_config(&walk_f.location, &walk_target_name, walk_target.config)?;
}
let copy = resolve_copy(
&walk_f.location,
&walk_target_name,
walk_target.copy.clone(),
)?;
links.push(Link::Node {
name: walk_target_name.clone(),
location: walk_f.location.clone(),
workspace: walk_target.workspace.clone(),
copy,
build: walk_target.build.clone(),
env: walk_target.env.clone(),
});
match walk_target.from {
FromStmt::Image(docker_image_def) => {
links.push(Link::Root(RootNode::Image(docker_image_def)));
break;
},
FromStmt::Docker(docker_file_path) => {
let docker_path = resolve_path(&walk_f.location, &docker_file_path).user_err(
DockerfileNotFound(
docker_file_path,
walk_target_name.clone(),
walk_f.location.clone(),
),
)?;
links.push(Link::Root(RootNode::Dockerfile(docker_path)));
break;
},
FromStmt::Target(TargetRef { location, name }) => {
if let Some(location) = location {
let location = resolve_path(&walk_f.location, &location).user_err(
DirNotFoundFromStmt(location, name.clone(), walk_f.location.clone()),
)?;
walk_f = SCellFile::from_path(&location, &SCellExtraArguments::new_emtpy())
.wrap_user_err(FileLoadFromStmt(
location.clone(),
name.clone(),
walk_f.location.clone(),
))?;
}
if visited_targets.contains(&(name.clone(), walk_f.location.clone())) {
return UserError::bail(CircularTargets(name.clone(), walk_f.location))?;
}
walk_target = walk_f
.targets
.get(&name)
.user_err(MissingTarget(name.clone(), walk_f.location.clone()))?
.clone();
walk_target_name = name;
visited_targets.insert((walk_target_name.clone(), walk_f.location.clone()));
},
}
}
Ok((links, shell, hang, config, services))
}
fn resolve_config(
location: &Path,
target_name: &TargetName,
config: Option<ConfigStmt>,
) -> color_eyre::Result<Option<ConfigStmt>> {
config
.map(|mut c| {
c.mounts.0 = c
.mounts
.0
.into_iter()
.map(|mut m| {
m.host = resolve_path(location, &m.host).user_err(MountHostDirNotFound(
m.host,
target_name.clone(),
location.to_path_buf(),
))?;
color_eyre::eyre::Ok(m)
})
.collect::<Result<_, _>>()?;
color_eyre::eyre::Ok(c)
})
.transpose()
}
fn resolve_services(
services: ServicesStmt,
f: &SCellFile,
) -> color_eyre::Result<Vec<(ServiceName, Service)>> {
let mut res = Vec::new();
for (s_name, s) in services.0 {
let (links, _shell, hang, config, services) = compile_target(f.clone(), s, s_name.clone())?;
color_eyre::eyre::ensure!(
links.len() >= 2,
"It must be at least two links in the target chain"
);
let image = SCellImage::new(links, hang.ok_or(MissingHangStmt).mark_as_user_err()?)?;
let container = SCellContainer::new(config);
res.push((s_name, Service { image, container }));
res.extend(services);
}
Ok(res)
}
fn resolve_copy(
location: &Path,
target_name: &TargetName,
mut copy: CopyStmt,
) -> color_eyre::Result<CopyStmt> {
let mut report = Report::new();
for e in &mut copy.0 {
for src_item in &mut e.src {
if let Ok(new_src) = resolve_path(location, src_item) {
*src_item = new_src;
} else {
report.add_error(UserError::wrap(CopySrcNotFound(
src_item.clone(),
target_name.clone(),
location.to_path_buf(),
)));
}
}
}
report.check()?;
Ok(copy)
}
fn resolve_path(
ctx: &Path,
path: &Path,
) -> color_eyre::Result<PathBuf> {
Ok(std::fs::canonicalize(ctx.join(path))?)
}
#[allow(dead_code)]
fn global() -> color_eyre::Result<Option<SCellFile>> {
const SCELL_GLOBAL: &str = "global.yml";
let scell_home = scell_home_dir()?;
SCellFile::from_path(
scell_home.join(SCELL_GLOBAL),
&SCellExtraArguments::new_emtpy(),
)
.map(Some)
.or_else(|e| {
let io_e = e.downcast::<std::io::Error>()?;
if io_e.kind() == std::io::ErrorKind::NotFound {
Ok(None)
} else {
Err(io_e.into())
}
})
}