use anyhow::Context;
use std::ffi::OsStr;
use std::fs::{self, File};
use std::io::{self, BufWriter, Write};
use std::path::{Component, Path, PathBuf};
pub fn is_retina<P: AsRef<Path>>(path: P) -> bool {
path.as_ref()
.file_stem()
.and_then(OsStr::to_str)
.map(|stem| stem.ends_with("@2x"))
.unwrap_or(false)
}
pub fn create_file(path: &Path) -> crate::Result<BufWriter<File>> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create directory {parent:?}"))?;
}
let file = File::create(path).with_context(|| format!("Failed to create file {path:?}"))?;
Ok(BufWriter::new(file))
}
#[cfg(unix)]
fn symlink_dir(src: &Path, dst: &Path) -> io::Result<()> {
std::os::unix::fs::symlink(src, dst)
}
#[cfg(windows)]
fn symlink_dir(src: &Path, dst: &Path) -> io::Result<()> {
std::os::windows::fs::symlink_dir(src, dst)
}
#[cfg(unix)]
pub fn symlink_file(src: &Path, dst: &Path) -> io::Result<()> {
std::os::unix::fs::symlink(src, dst)
}
#[cfg(windows)]
pub fn symlink_file(src: &Path, dst: &Path) -> io::Result<()> {
std::os::windows::fs::symlink_file(src, dst)
}
pub fn copy_file(from: &Path, to: &Path) -> crate::Result<()> {
if !from.exists() {
anyhow::bail!("{:?} does not exist", from);
}
if !from.is_file() {
anyhow::bail!("{:?} is not a file", from);
}
let dest_dir = to.parent().unwrap();
fs::create_dir_all(dest_dir).with_context(|| format!("Failed to create {dest_dir:?}"))?;
fs::copy(from, to).with_context(|| format!("Failed to copy {from:?} to {to:?}"))?;
Ok(())
}
pub fn read_file(file: &Path) -> crate::Result<String> {
if !file.exists() {
anyhow::bail!("{:?} does not exist", file);
}
if !file.is_file() {
anyhow::bail!("{:?} is not a file", file);
}
let contents = fs::read_to_string(file).with_context(|| format!("Failed to read {file:?}"))?;
Ok(contents)
}
pub fn copy_dir(from: &Path, to: &Path) -> crate::Result<()> {
if !from.exists() {
anyhow::bail!("{:?} does not exist", from);
}
if !from.is_dir() {
anyhow::bail!("{:?} is not a directory", from);
}
if to.exists() {
anyhow::bail!("{:?} already exists", to);
}
let parent = to.parent().unwrap();
fs::create_dir_all(parent).with_context(|| format!("Failed to create {parent:?}"))?;
for entry in walkdir::WalkDir::new(from) {
let entry = entry?;
debug_assert!(entry.path().starts_with(from));
let rel_path = entry.path().strip_prefix(from).unwrap();
let dest_path = to.join(rel_path);
if entry.file_type().is_symlink() {
let target = fs::read_link(entry.path())?;
if entry.path().is_dir() {
symlink_dir(&target, &dest_path)?;
} else {
symlink_file(&target, &dest_path)?;
}
} else if entry.file_type().is_dir() {
fs::create_dir(dest_path)?;
} else {
fs::copy(entry.path(), dest_path)?;
}
}
Ok(())
}
pub fn resource_relpath(path: &Path) -> PathBuf {
let mut dest = PathBuf::new();
for component in path.components() {
match component {
Component::Prefix(_) => {}
Component::RootDir => dest.push("_root_"),
Component::CurDir => {}
Component::ParentDir => dest.push("_up_"),
Component::Normal(string) => dest.push(string),
}
}
dest
}
pub fn print_bundling(filename: &str) -> crate::Result<()> {
print_progress("Bundling", filename)
}
pub fn print_finished(output_paths: &Vec<PathBuf>) -> crate::Result<()> {
let pluralised = if output_paths.len() == 1 {
"bundle"
} else {
"bundles"
};
let msg = format!("{} {} at:", output_paths.len(), pluralised);
print_progress("Finished", &msg)?;
for path in output_paths {
println!(" {}", path.display());
}
Ok(())
}
fn safe_term_attr<T: term::Terminal + ?Sized>(
output: &mut Box<T>,
attr: term::Attr,
) -> term::Result<()> {
match output.supports_attr(attr) {
true => output.attr(attr),
false => Ok(()),
}
}
fn print_progress(step: &str, msg: &str) -> crate::Result<()> {
if let Some(mut output) = term::stderr() {
safe_term_attr(&mut output, term::Attr::Bold)?;
if output.supports_color() {
output.fg(term::color::GREEN)?;
}
write!(output, " {step}")?;
if output.supports_reset() {
output.reset()?;
}
writeln!(output, " {msg}")?;
output.flush()?;
Ok(())
} else {
let mut output = io::stderr();
write!(output, " {step}")?;
writeln!(output, " {msg}")?;
output.flush()?;
Ok(())
}
}
pub fn print_warning(message: &str) -> crate::Result<()> {
if let Some(mut output) = term::stderr() {
safe_term_attr(&mut output, term::Attr::Bold)?;
if output.supports_color() {
output.fg(term::color::YELLOW)?;
}
write!(output, "warning:")?;
output.reset()?;
writeln!(output, " {message}")?;
output.flush()?;
Ok(())
} else {
let mut output = io::stderr();
write!(output, "warning:")?;
writeln!(output, " {message}")?;
output.flush()?;
Ok(())
}
}
pub fn print_error(error: &anyhow::Error) -> crate::Result<()> {
if let Some(mut output) = term::stderr() {
safe_term_attr(&mut output, term::Attr::Bold)?;
if output.supports_color() {
output.fg(term::color::RED)?;
}
write!(output, "error:")?;
if output.supports_reset() {
output.reset()?;
}
safe_term_attr(&mut output, term::Attr::Bold)?;
writeln!(output, " {error}")?;
if output.supports_reset() {
output.reset()?;
}
for cause in error.chain().skip(1) {
writeln!(output, " Caused by: {cause}")?;
}
let backtrace = error.backtrace();
writeln!(output, "{backtrace:?}")?;
output.flush()?;
Ok(())
} else {
let mut output = io::stderr();
write!(output, "error:")?;
writeln!(output, " {error}")?;
for cause in error.chain().skip(1) {
writeln!(output, " Caused by: {cause}")?;
}
let backtrace = error.backtrace();
writeln!(output, "{backtrace:?}")?;
output.flush()?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::{copy_dir, create_file, is_retina, read_file, resource_relpath, symlink_file};
use std::io::Write;
use std::path::{Path, PathBuf};
#[test]
fn create_file_with_parent_dirs() {
let tmp = tempfile::tempdir().unwrap();
assert!(!tmp.path().join("parent").exists());
{
let mut file = create_file(&tmp.path().join("parent/file.txt")).unwrap();
writeln!(file, "Hello, world!").unwrap();
}
assert!(tmp.path().join("parent").is_dir());
assert!(tmp.path().join("parent/file.txt").is_file());
}
#[test]
fn copy_dir_with_symlinks() {
let tmp = tempfile::tempdir().unwrap();
{
let mut file = create_file(&tmp.path().join("orig/sub/file.txt")).unwrap();
writeln!(file, "Hello, world!").unwrap();
}
symlink_file(
&tmp.path().join("orig/sub/file.txt"),
&tmp.path().join("orig/link"),
)
.unwrap();
assert_eq!(
std::fs::read(tmp.path().join("orig/link"))
.unwrap()
.as_slice(),
b"Hello, world!\n"
);
copy_dir(&tmp.path().join("orig"), &tmp.path().join("parent/copy")).unwrap();
assert!(tmp.path().join("parent/copy").is_dir());
assert!(tmp.path().join("parent/copy/sub").is_dir());
assert!(tmp.path().join("parent/copy/sub/file.txt").is_file());
assert_eq!(
std::fs::read(tmp.path().join("parent/copy/sub/file.txt"))
.unwrap()
.as_slice(),
b"Hello, world!\n"
);
assert!(tmp.path().join("parent/copy/link").exists());
assert_eq!(
std::fs::read_link(tmp.path().join("parent/copy/link")).unwrap(),
tmp.path().join("orig/sub/file.txt")
);
assert_eq!(
std::fs::read(tmp.path().join("parent/copy/link"))
.unwrap()
.as_slice(),
b"Hello, world!\n"
);
}
#[test]
fn retina_icon_paths() {
assert!(!is_retina("data/icons/512x512.png"));
assert!(is_retina("data/icons/512x512@2x.png"));
}
#[test]
fn resource_relative_paths() {
assert_eq!(
resource_relpath(&PathBuf::from("./data/images/button.png")),
PathBuf::from("data/images/button.png")
);
assert_eq!(
resource_relpath(&PathBuf::from("../../images/wheel.png")),
PathBuf::from("_up_/_up_/images/wheel.png")
);
assert_eq!(
resource_relpath(&PathBuf::from("/home/ferris/crab.png")),
PathBuf::from("_root_/home/ferris/crab.png")
);
}
#[test]
fn read_files() {
const HELLO_WORLD: &str = "Hello, world!";
const FILE: &str = "some/sub/file.txt";
let tmp = tempfile::tempdir().unwrap();
{
let mut file = create_file(&tmp.path().join(FILE)).unwrap();
write!(file, "{HELLO_WORLD}").unwrap();
}
let read = read_file(&tmp.path().join(FILE)).unwrap();
assert_eq!(read, HELLO_WORLD);
assert!(read_file(&tmp.path().join("other/path")).is_err());
assert!(read_file(&tmp.path().join(Path::new(FILE).parent().unwrap())).is_err());
}
}