Skip to content

Commit

Permalink
Provide a cargo loader for build scripts.
Browse files Browse the repository at this point in the history
And some error hanlding improvents in the input module.
  • Loading branch information
kaj committed Aug 5, 2022
1 parent a78e941 commit 2dc3cf1
Show file tree
Hide file tree
Showing 10 changed files with 287 additions and 92 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ project adheres to
Create a context suitable for how files should be loaded, configure it
with an output format and optionally extend the global scope before
calling `Context::transform` with an input file (PR #151, PR #152).
* Also provide `CargoContext` / `CargoLoader` for convenient use in
build scripts (PR #154).
* The `@content` can have arguments when declaring and calling a mixin
(PR #146).
* Variable declartions can be scoped (like `module.$var = value`). Some
Expand Down
13 changes: 9 additions & 4 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::input::LoadError;
use crate::parser::{ParseError, SourcePos};
use crate::sass::{ArgsError, Name};
use crate::value::RangeError;
Expand All @@ -8,7 +9,7 @@ use std::{fmt, io};
/// Most functions in rsass that returns a Result uses this Error type.
pub enum Error {
/// An IO error encoundered on a specific path
Input(String, io::Error),
Input(LoadError),
/// An IO error without specifying a path.
///
/// This is (probably) an error writing output.
Expand Down Expand Up @@ -56,9 +57,7 @@ impl fmt::Debug for Error {
fn fmt(&self, out: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Error::S(ref s) => write!(out, "{}", s),
Error::Input(ref p, ref e) => {
write!(out, "Failed to read {:?}: {}", p, e)
}
Error::Input(ref load) => load.fmt(out),
Error::BadArgument(ref name, ref problem) => {
write!(out, "${}: {}", name, problem)
}
Expand Down Expand Up @@ -170,6 +169,12 @@ impl From<RangeError> for Error {
}
}

impl From<LoadError> for Error {
fn from(err: LoadError) -> Self {
Error::Input(err)
}
}

/// Something invalid.
///
/// Should be combined with a position to get an [Error].
Expand Down
102 changes: 102 additions & 0 deletions src/input/cargoloader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use super::{LoadError, Loader, SourceFile, SourceName};
use std::path::{Path, PathBuf};

/// A [`Loader`] for when calling rsass from a `build.rs` script.
///
/// This is very similar to a [`FsLoader`][super::FsLoader], but has a
/// `for_crate` constructor that uses the `CARGO_MANIFEST_DIR`
/// environment variable instead of the current working directory, and
/// it prints `cargo:rerun-if-changed` messages for each path that it
/// loads.
#[derive(Debug)]
pub struct CargoLoader {
path: Vec<PathBuf>,
}

impl CargoLoader {
/// Create a new FsFileContext.
///
/// Files will be resolved from the directory containing the
/// manifest of your package.
/// This assumes the program is called by `cargo`, so the
/// `CARGO_MANIFEST_DIR` environment variable is set.
pub fn for_crate() -> Result<Self, LoadError> {
Ok(Self {
path: vec![get_pkg_base()?],
})
}

/// Add a path to search for files.
///
/// The path can be relative to the crate manifest directory, or
/// absolute.
pub fn push_path(&mut self, path: &Path) -> Result<(), LoadError> {
self.path.push(if path.is_absolute() {
path.into()
} else {
get_pkg_base()?.join(path)
});
Ok(())
}

/// Create a Loader and a SourceFile from a given Path.
///
/// The path can be relative to the crate manifest directory, or
/// absolute.
pub fn for_path(path: &Path) -> Result<(Self, SourceFile), LoadError> {
let path = if path.is_absolute() {
path.into()
} else {
get_pkg_base()?.join(path)
};
let mut f = std::fs::File::open(&path)
.map_err(|e| LoadError::Input(path.display().to_string(), e))?;
cargo_watch(&path);
let (path, name) = if let Some(base) = path.parent() {
(vec![base.to_path_buf()], path.strip_prefix(base).unwrap())
} else {
(vec![get_pkg_base()?], path.as_ref())
};
let loader = Self { path };
let source = SourceName::root(name.display().to_string());
let source = SourceFile::read(&mut f, source)?;
Ok((loader, source))
}
}

impl Loader for CargoLoader {
type File = std::fs::File;

fn find_file(&self, url: &str) -> Result<Option<Self::File>, LoadError> {
if !url.is_empty() {
for base in &self.path {
let full = base.join(url);
if full.is_file() {
tracing::debug!(?full, "opening file");
let file = Self::File::open(&full).map_err(|e| {
LoadError::Input(full.display().to_string(), e)
})?;
cargo_watch(&full);
return Ok(Some(file));
}
tracing::trace!(?full, "Not found");
}
}
Ok(None)
}
}

/// Tell cargo to recompile if the file on `path` changes.
fn cargo_watch(path: &Path) {
println!("cargo:rerun-if-changed={}", path.display());
}

/// Get the package base dir.
///
/// This returns the `CARGO_MANIFEST_DIR` environment variable as a path.
/// If the env is not set, an error is returned.
fn get_pkg_base() -> Result<PathBuf, LoadError> {
std::env::var_os("CARGO_MANIFEST_DIR")
.map(PathBuf::from)
.ok_or(LoadError::NotCalledFromCargo)
}
71 changes: 59 additions & 12 deletions src/input/context.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
use super::{FsLoader, Loader, SourceFile, SourceKind};
use super::{
CargoLoader, FsLoader, LoadError, Loader, SourceFile, SourceKind,
};
use crate::output::{handle_parsed, CssBuf, CssHead, Format};
use crate::{Error, ScopeRef};
use std::{borrow::Cow, collections::BTreeMap, fmt, path::Path};
use tracing::instrument;

/// Utility keeping track of loading files.
///
/// The context is generic over the [`Loader`].
/// [`FsContext`] and [`CargoContext`] are type aliases for `Context`
/// where the loader is a [`FsLoader`] or [`CargoLoader`],
/// respectively.
///
/// # Examples
///
/// The Context here is a [`FsContext`].
Expand Down Expand Up @@ -49,8 +56,8 @@ use tracing::instrument;
/// );
/// # Ok(()) }
/// ```
pub struct Context<FileC> {
file_context: FileC,
pub struct Context<Loader> {
loader: Loader,
scope: Option<ScopeRef>,
loading: BTreeMap<String, SourceKind>,
// TODO: Maybe have a map to loaded SourceFiles as well? Or even Parsed?
Expand All @@ -59,7 +66,7 @@ pub struct Context<FileC> {
/// A file-system based [`Context`].
pub type FsContext = Context<FsLoader>;

impl Context<FsLoader> {
impl FsContext {
/// Create a new `Context`, loading files based on the current
/// working directory.
pub fn for_cwd() -> Self {
Expand All @@ -69,22 +76,62 @@ impl Context<FsLoader> {
/// Create a new `Context` and load a file.
///
/// The directory part of `path` is used as a base directory for the loader.
pub fn for_path(path: &Path) -> Result<(Self, SourceFile), Error> {
pub fn for_path(path: &Path) -> Result<(Self, SourceFile), LoadError> {
let (file_context, file) = FsLoader::for_path(path)?;
Ok((Context::for_loader(file_context), file))
}

/// Add a path to search for files.
pub fn push_path(&mut self, path: &Path) {
self.file_context.push_path(path);
self.loader.push_path(path);
}
}

/// A file-system based [`Context`] for use in cargo build scripts.
///
/// This is very similar to a [`FsContext`], but has a
/// `for_crate` constructor that uses the `CARGO_MANIFEST_DIR`
/// environment variable instead of the current working directory, and
/// it prints `cargo:rerun-if-changed` messages for each path that it
/// loads.
pub type CargoContext = Context<CargoLoader>;

impl CargoContext {
/// Create a new `Context`, loading files based in the manifest
/// directory of the current crate.
///
/// Relative paths will be resolved from the directory containing the
/// manifest of your package.
/// This assumes the program is called by `cargo` as a build script, so
/// the `CARGO_MANIFEST_DIR` environment variable is set.
pub fn for_crate() -> Result<Self, LoadError> {
Ok(Context::for_loader(CargoLoader::for_crate()?))
}

/// Create a new `Context` and load a file.
///
/// The directory part of `path` is used as a base directory for the loader.
/// If `path` is relative, it will be resolved from the directory
/// containing the manifest of your package.
pub fn for_path(path: &Path) -> Result<(Self, SourceFile), LoadError> {
let (file_context, file) = CargoLoader::for_path(path)?;
Ok((Context::for_loader(file_context), file))
}

/// Add a path to search for files.
///
/// If `path` is relative, it will be resolved from the directory
/// containing the manifest of your package.
pub fn push_path(&mut self, path: &Path) -> Result<(), LoadError> {
self.loader.push_path(path)
}
}

impl<AnyLoader: Loader> Context<AnyLoader> {
/// Create a new `Context` for a given file [`Loader`].
pub fn for_loader(fc: AnyLoader) -> Self {
pub fn for_loader(loader: AnyLoader) -> Self {
Context {
file_context: fc,
loader,
scope: None,
loading: Default::default(),
}
Expand Down Expand Up @@ -211,12 +258,12 @@ impl<AnyLoader: Loader> Context<AnyLoader> {
&self,
url: &str,
names: &[&dyn Fn(&str, &str) -> String],
) -> Result<Option<(String, AnyLoader::File)>, Error> {
) -> Result<Option<(String, AnyLoader::File)>, LoadError> {
if url.ends_with(".css")
|| url.ends_with(".sass")
|| url.ends_with(".scss")
{
self.file_context
self.loader
.find_file(url)
.map(|file| file.map(|file| (url.into(), file)))
} else {
Expand All @@ -226,7 +273,7 @@ impl<AnyLoader: Loader> Context<AnyLoader> {
.unwrap_or(("", url));

for name in names.iter().map(|f| f(base, name)) {
if let Some(result) = self.file_context.find_file(&name)? {
if let Some(result) = self.loader.find_file(&name)? {
return Ok(Some((name, result)));
}
}
Expand Down Expand Up @@ -277,7 +324,7 @@ fn relative<'a>(base: &SourceKind, url: &'a str) -> Cow<'a, str> {
impl<T: fmt::Debug> fmt::Debug for Context<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Context")
.field("loader", &self.file_context)
.field("loader", &self.loader)
.field(
"scope",
&if self.scope.is_some() { "loaded" } else { "no" },
Expand Down
61 changes: 61 additions & 0 deletions src/input/fsloader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use super::{LoadError, Loader, SourceFile, SourceName};
use std::path::{Path, PathBuf};

/// A [`Loader`] that loads files from the filesystem.
#[derive(Debug)]
pub struct FsLoader {
path: Vec<PathBuf>,
}

impl FsLoader {
/// Create a new FsFileContext.
///
/// Files will be resolved from the current working directory.
pub fn for_cwd() -> Self {
Self {
path: vec![PathBuf::new()],
}
}

/// Add a path to search for files.
pub fn push_path(&mut self, path: &Path) {
self.path.push(path.into());
}

/// Create a Loader and a SourceFile from a given Path.
pub fn for_path(path: &Path) -> Result<(Self, SourceFile), LoadError> {
let mut f = std::fs::File::open(&path)
.map_err(|e| LoadError::Input(path.display().to_string(), e))?;
let (path, name) = if let Some(base) = path.parent() {
(vec![base.to_path_buf()], path.strip_prefix(base).unwrap())
} else {
(vec![PathBuf::new()], path)
};
let loader = Self { path };
let source = SourceName::root(name.display().to_string());
let source = SourceFile::read(&mut f, source)?;
Ok((loader, source))
}
}

impl Loader for FsLoader {
type File = std::fs::File;

fn find_file(&self, url: &str) -> Result<Option<Self::File>, LoadError> {
if !url.is_empty() {
for base in &self.path {
let full = base.join(url);
if full.is_file() {
tracing::debug!(?full, "opening file");
return Self::File::open(&full)
.map_err(|e| {
LoadError::Input(full.display().to_string(), e)
})
.map(Some);
}
tracing::trace!(?full, "Not found");
}
}
Ok(None)
}
}
Loading

0 comments on commit 2dc3cf1

Please sign in to comment.