Skip to content

Commit 7734dcd

Browse files
committed
Provide a cargo loader for build scripts.
And some error hanlding improvents in the input module.
1 parent a78e941 commit 7734dcd

File tree

9 files changed

+284
-91
lines changed

9 files changed

+284
-91
lines changed

Diff for: src/error.rs

+9-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::input::LoadError;
12
use crate::parser::{ParseError, SourcePos};
23
use crate::sass::{ArgsError, Name};
34
use crate::value::RangeError;
@@ -8,7 +9,7 @@ use std::{fmt, io};
89
/// Most functions in rsass that returns a Result uses this Error type.
910
pub enum Error {
1011
/// An IO error encoundered on a specific path
11-
Input(String, io::Error),
12+
Input(LoadError),
1213
/// An IO error without specifying a path.
1314
///
1415
/// This is (probably) an error writing output.
@@ -56,9 +57,7 @@ impl fmt::Debug for Error {
5657
fn fmt(&self, out: &mut fmt::Formatter<'_>) -> fmt::Result {
5758
match *self {
5859
Error::S(ref s) => write!(out, "{}", s),
59-
Error::Input(ref p, ref e) => {
60-
write!(out, "Failed to read {:?}: {}", p, e)
61-
}
60+
Error::Input(ref load) => load.fmt(out),
6261
Error::BadArgument(ref name, ref problem) => {
6362
write!(out, "${}: {}", name, problem)
6463
}
@@ -170,6 +169,12 @@ impl From<RangeError> for Error {
170169
}
171170
}
172171

172+
impl From<LoadError> for Error {
173+
fn from(err: LoadError) -> Self {
174+
Error::Input(err)
175+
}
176+
}
177+
173178
/// Something invalid.
174179
///
175180
/// Should be combined with a position to get an [Error].

Diff for: src/input/cargoloader.rs

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
use super::{LoadError, Loader, SourceFile, SourceName};
2+
use std::path::{Path, PathBuf};
3+
4+
/// A [`Loader`] for when calling rsass from a `build.rs` script.
5+
///
6+
/// This is very similar to a [`FsLoader`][super::FsLoader], but has a
7+
/// `for_crate` constructor that uses the `CARGO_MANIFEST_DIR`
8+
/// environment variable instead of the current working directory, and
9+
/// it prints `cargo:rerun-if-changed` messages for each path that it
10+
/// loads.
11+
#[derive(Debug)]
12+
pub struct CargoLoader {
13+
path: Vec<PathBuf>,
14+
}
15+
16+
impl CargoLoader {
17+
/// Create a new FsFileContext.
18+
///
19+
/// Files will be resolved from the directory containing the
20+
/// manifest of your package.
21+
/// This assumes the program is called by `cargo`, so the
22+
/// `CARGO_MANIFEST_DIR` environment variable is set.
23+
pub fn for_crate() -> Result<Self, LoadError> {
24+
Ok(Self {
25+
path: vec![get_pkg_base()?],
26+
})
27+
}
28+
29+
/// Add a path to search for files.
30+
///
31+
/// The path can be relative to the crate manifest directory, or
32+
/// absolute.
33+
pub fn push_path(&mut self, path: &Path) -> Result<(), LoadError> {
34+
self.path.push(if path.is_absolute() {
35+
path.into()
36+
} else {
37+
get_pkg_base()?.join(path)
38+
});
39+
Ok(())
40+
}
41+
42+
/// Create a Loader and a SourceFile from a given Path.
43+
///
44+
/// The path can be relative to the crate manifest directory, or
45+
/// absolute.
46+
pub fn for_path(path: &Path) -> Result<(Self, SourceFile), LoadError> {
47+
let path = if path.is_absolute() {
48+
path.into()
49+
} else {
50+
get_pkg_base()?.join(path)
51+
};
52+
let mut f = std::fs::File::open(&path)
53+
.map_err(|e| LoadError::Input(path.display().to_string(), e))?;
54+
cargo_watch(&path);
55+
let (path, name) = if let Some(base) = path.parent() {
56+
(vec![base.to_path_buf()], path.strip_prefix(base).unwrap())
57+
} else {
58+
(vec![get_pkg_base()?], path.as_ref())
59+
};
60+
let loader = Self { path };
61+
let source = SourceName::root(name.display().to_string());
62+
let source = SourceFile::read(&mut f, source)?;
63+
Ok((loader, source))
64+
}
65+
}
66+
67+
impl Loader for CargoLoader {
68+
type File = std::fs::File;
69+
70+
fn find_file(&self, url: &str) -> Result<Option<Self::File>, LoadError> {
71+
if !url.is_empty() {
72+
for base in &self.path {
73+
let full = base.join(url);
74+
if full.is_file() {
75+
tracing::debug!(?full, "opening file");
76+
let file = Self::File::open(&full).map_err(|e| {
77+
LoadError::Input(full.display().to_string(), e)
78+
})?;
79+
cargo_watch(&full);
80+
return Ok(Some(file));
81+
}
82+
tracing::trace!(?full, "Not found");
83+
}
84+
}
85+
Ok(None)
86+
}
87+
}
88+
89+
/// Tell cargo to recompile if the file on `path` changes.
90+
fn cargo_watch(path: &Path) {
91+
println!("cargo:rerun-if-changed={}", path.display());
92+
}
93+
94+
/// Get the package base dir.
95+
///
96+
/// This returns the `CARGO_MANIFEST_DIR` environment variable as a path.
97+
/// If the env is not set, an error is returned.
98+
fn get_pkg_base() -> Result<PathBuf, LoadError> {
99+
std::env::var_os("CARGO_MANIFEST_DIR")
100+
.map(PathBuf::from)
101+
.ok_or(LoadError::NotCalledFromCargo)
102+
}

Diff for: src/input/context.rs

+58-11
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
1-
use super::{FsLoader, Loader, SourceFile, SourceKind};
1+
use super::{
2+
CargoLoader, FsLoader, LoadError, Loader, SourceFile, SourceKind,
3+
};
24
use crate::output::{handle_parsed, CssBuf, CssHead, Format};
35
use crate::{Error, ScopeRef};
46
use std::{borrow::Cow, collections::BTreeMap, fmt, path::Path};
57
use tracing::instrument;
68

79
/// Utility keeping track of loading files.
810
///
11+
/// The context is generic over the [`Loader`].
12+
/// [`FsContext`] and [`CargoContext`] are type aliases for `Context`
13+
/// where the loader is a [`FsLoader`] or [`CargoLoader`],
14+
/// respectively.
15+
///
916
/// # Examples
1017
///
1118
/// The Context here is a [`FsContext`].
@@ -49,8 +56,8 @@ use tracing::instrument;
4956
/// );
5057
/// # Ok(()) }
5158
/// ```
52-
pub struct Context<FileC> {
53-
file_context: FileC,
59+
pub struct Context<Loader> {
60+
loader: Loader,
5461
scope: Option<ScopeRef>,
5562
loading: BTreeMap<String, SourceKind>,
5663
// TODO: Maybe have a map to loaded SourceFiles as well? Or even Parsed?
@@ -59,7 +66,7 @@ pub struct Context<FileC> {
5966
/// A file-system based [`Context`].
6067
pub type FsContext = Context<FsLoader>;
6168

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

7784
/// Add a path to search for files.
7885
pub fn push_path(&mut self, path: &Path) {
79-
self.file_context.push_path(path);
86+
self.loader.push_path(path);
87+
}
88+
}
89+
90+
/// A file-system based [`Context`] for use in cargo build scripts.
91+
///
92+
/// This is very similar to a [`FsContext`], but has a
93+
/// `for_crate` constructor that uses the `CARGO_MANIFEST_DIR`
94+
/// environment variable instead of the current working directory, and
95+
/// it prints `cargo:rerun-if-changed` messages for each path that it
96+
/// loads.
97+
pub type CargoContext = Context<CargoLoader>;
98+
99+
impl CargoContext {
100+
/// Create a new `Context`, loading files based in the manifest
101+
/// directory of the current crate.
102+
///
103+
/// Relative paths will be resolved from the directory containing the
104+
/// manifest of your package.
105+
/// This assumes the program is called by `cargo` as a build script, so
106+
/// the `CARGO_MANIFEST_DIR` environment variable is set.
107+
pub fn for_crate() -> Result<Self, LoadError> {
108+
Ok(Context::for_loader(CargoLoader::for_crate()?))
109+
}
110+
111+
/// Create a new `Context` and load a file.
112+
///
113+
/// The directory part of `path` is used as a base directory for the loader.
114+
/// If `path` is relative, it will be resolved from the directory
115+
/// containing the manifest of your package.
116+
pub fn for_path(path: &Path) -> Result<(Self, SourceFile), LoadError> {
117+
let (file_context, file) = CargoLoader::for_path(path)?;
118+
Ok((Context::for_loader(file_context), file))
119+
}
120+
121+
/// Add a path to search for files.
122+
///
123+
/// If `path` is relative, it will be resolved from the directory
124+
/// containing the manifest of your package.
125+
pub fn push_path(&mut self, path: &Path) -> Result<(), LoadError> {
126+
self.loader.push_path(path)
80127
}
81128
}
82129

83130
impl<AnyLoader: Loader> Context<AnyLoader> {
84131
/// Create a new `Context` for a given file [`Loader`].
85132
pub fn for_loader(fc: AnyLoader) -> Self {
86133
Context {
87-
file_context: fc,
134+
loader: fc,
88135
scope: None,
89136
loading: Default::default(),
90137
}
@@ -211,12 +258,12 @@ impl<AnyLoader: Loader> Context<AnyLoader> {
211258
&self,
212259
url: &str,
213260
names: &[&dyn Fn(&str, &str) -> String],
214-
) -> Result<Option<(String, AnyLoader::File)>, Error> {
261+
) -> Result<Option<(String, AnyLoader::File)>, LoadError> {
215262
if url.ends_with(".css")
216263
|| url.ends_with(".sass")
217264
|| url.ends_with(".scss")
218265
{
219-
self.file_context
266+
self.loader
220267
.find_file(url)
221268
.map(|file| file.map(|file| (url.into(), file)))
222269
} else {
@@ -226,7 +273,7 @@ impl<AnyLoader: Loader> Context<AnyLoader> {
226273
.unwrap_or(("", url));
227274

228275
for name in names.iter().map(|f| f(base, name)) {
229-
if let Some(result) = self.file_context.find_file(&name)? {
276+
if let Some(result) = self.loader.find_file(&name)? {
230277
return Ok(Some((name, result)));
231278
}
232279
}
@@ -277,7 +324,7 @@ fn relative<'a>(base: &SourceKind, url: &'a str) -> Cow<'a, str> {
277324
impl<T: fmt::Debug> fmt::Debug for Context<T> {
278325
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279326
f.debug_struct("Context")
280-
.field("loader", &self.file_context)
327+
.field("loader", &self.loader)
281328
.field(
282329
"scope",
283330
&if self.scope.is_some() { "loaded" } else { "no" },

Diff for: src/input/fsloader.rs

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
use super::{LoadError, Loader, SourceFile, SourceName};
2+
use std::path::{Path, PathBuf};
3+
4+
/// A [`Loader`] that loads files from the filesystem.
5+
#[derive(Debug)]
6+
pub struct FsLoader {
7+
path: Vec<PathBuf>,
8+
}
9+
10+
impl FsLoader {
11+
/// Create a new FsFileContext.
12+
///
13+
/// Files will be resolved from the current working directory.
14+
pub fn for_cwd() -> Self {
15+
Self {
16+
path: vec![PathBuf::new()],
17+
}
18+
}
19+
20+
/// Add a path to search for files.
21+
pub fn push_path(&mut self, path: &Path) {
22+
self.path.push(path.into());
23+
}
24+
25+
/// Create a Loader and a SourceFile from a given Path.
26+
pub fn for_path(path: &Path) -> Result<(Self, SourceFile), LoadError> {
27+
let mut f = std::fs::File::open(&path)
28+
.map_err(|e| LoadError::Input(path.display().to_string(), e))?;
29+
let (path, name) = if let Some(base) = path.parent() {
30+
(vec![base.to_path_buf()], path.strip_prefix(base).unwrap())
31+
} else {
32+
(vec![PathBuf::new()], path)
33+
};
34+
let loader = Self { path };
35+
let source = SourceName::root(name.display().to_string());
36+
let source = SourceFile::read(&mut f, source)?;
37+
Ok((loader, source))
38+
}
39+
}
40+
41+
impl Loader for FsLoader {
42+
type File = std::fs::File;
43+
44+
fn find_file(&self, url: &str) -> Result<Option<Self::File>, LoadError> {
45+
if !url.is_empty() {
46+
for base in &self.path {
47+
let full = base.join(url);
48+
if full.is_file() {
49+
tracing::debug!(?full, "opening file");
50+
return Self::File::open(&full)
51+
.map_err(|e| {
52+
LoadError::Input(full.display().to_string(), e)
53+
})
54+
.map(Some);
55+
}
56+
tracing::trace!(?full, "Not found");
57+
}
58+
}
59+
Ok(None)
60+
}
61+
}

0 commit comments

Comments
 (0)