feat(logging): add tracing logger

This commit is contained in:
Ray Sinurat 2026-02-05 23:18:33 -06:00
parent ca86eaae6e
commit 9490328bfb
49 changed files with 686 additions and 295 deletions

115
Cargo.lock generated
View file

@ -121,6 +121,15 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
@ -1031,6 +1040,8 @@ dependencies = [
"serde_json",
"smol",
"thiserror 2.0.18",
"tracing",
"tracing-subscriber",
]
[[package]]
@ -1055,6 +1066,7 @@ dependencies = [
"smol",
"thiserror 2.0.18",
"toml 0.8.23",
"tracing",
"walkdir",
"which",
]
@ -1084,6 +1096,7 @@ dependencies = [
"tempfile",
"thiserror 2.0.18",
"toml 0.8.23",
"tracing",
"walkdir",
]
@ -2016,6 +2029,15 @@ dependencies = [
"hashbrown 0.15.5",
]
[[package]]
name = "matchers"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata",
]
[[package]]
name = "memchr"
version = "2.7.6"
@ -2087,6 +2109,15 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@ -2693,12 +2724,29 @@ dependencies = [
"thiserror 2.0.18",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-lite"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973"
[[package]]
name = "regex-syntax"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
[[package]]
name = "rust-embed"
version = "8.11.0"
@ -3004,6 +3052,15 @@ dependencies = [
"digest 0.10.7",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "1.3.0"
@ -3338,6 +3395,15 @@ dependencies = [
"syn 2.0.114",
]
[[package]]
name = "thread_local"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
dependencies = [
"cfg-if",
]
[[package]]
name = "time"
version = "0.2.27"
@ -3467,6 +3533,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
@ -3479,6 +3546,48 @@ dependencies = [
"tracing",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-serde"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
dependencies = [
"serde",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex-automata",
"serde",
"serde_json",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
"tracing-serde",
]
[[package]]
name = "type-map"
version = "0.5.1"
@ -3599,6 +3708,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "value-bag"
version = "1.12.0"

View file

@ -34,3 +34,5 @@ ratatui = "0.29"
crossterm = "0.28"
thiserror = "2"
anyhow = "1"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "json"] }

View file

@ -21,3 +21,5 @@ thiserror.workspace = true
anyhow.workspace = true
dirs.workspace = true
blake3.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true

View file

@ -9,18 +9,12 @@ use std::io::{self, Write};
use std::path::PathBuf;
use std::process::Command;
pub fn run(
config_path: Option<PathBuf>,
dry_run: bool,
parallel: bool,
verbose: bool,
) -> anyhow::Result<()> {
#[tracing::instrument(skip_all, fields(dry_run, parallel))]
pub fn run(config_path: Option<PathBuf>, dry_run: bool, parallel: bool) -> anyhow::Result<()> {
let path = find_config_file(config_path)?;
let source = std::fs::read_to_string(&path)?;
if verbose {
println!("parsing {}", path.display());
}
tracing::debug!(path = %path.display(), "parsing config");
let program = parse_config(&path)?;
type_check(&program, &source, &path.display().to_string())?;
@ -45,18 +39,18 @@ pub fn run(
// Handle errors
if !validation.errors.is_empty() {
eprintln!("\nDotfile configuration errors:");
tracing::error!("dotfile configuration errors detected");
for error in &validation.errors {
match error {
DotfileConflict::Duplicate { index_a, index_b } => {
let a = &result.dotfiles[*index_a];
let b = &result.dotfiles[*index_b];
eprintln!(
" [error] duplicate entry: '{}' -> '{}' appears twice (entries {} and {})",
a.source.display(),
a.target.display(),
index_a + 1,
index_b + 1
tracing::error!(
source = %a.source.display(),
target = %a.target.display(),
index_a = index_a + 1,
index_b = index_b + 1,
"duplicate entry"
);
let _ = b; // silence unused warning
}
@ -66,12 +60,12 @@ pub fn run(
} => {
let parent = &result.dotfiles[*parent_index];
let child = &result.dotfiles[*child_index];
eprintln!(
" [error] redundant overlap: '{}' already includes '{}' (entries {} and {})",
parent.source.display(),
child.source.display(),
parent_index + 1,
child_index + 1
tracing::error!(
parent_source = %parent.source.display(),
child_source = %child.source.display(),
parent_index = parent_index + 1,
child_index = child_index + 1,
"redundant overlap"
);
let _ = child; // silence unused warning
}
@ -82,11 +76,9 @@ pub fn run(
// Show warnings
if !validation.warnings.is_empty() {
eprintln!("\nDotfile configuration warnings:");
for warning in &validation.warnings {
eprintln!(" [warn] {}", warning.message);
tracing::warn!(message = %warning.message, "dotfile configuration warning");
}
eprintln!();
}
// Reorder dotfiles based on dependency analysis
@ -98,7 +90,6 @@ pub fn run(
let config = Config::new(source_dir.clone())
.dry_run(dry_run)
.verbose(verbose)
.parallel(parallel);
let state_file = config.state_file.clone();
@ -133,16 +124,12 @@ pub fn run(
| SyncStatus::SourceChanged => {
// Can auto-merge: just copy from source
has_changes = true;
if verbose {
println!(" [source changed] {}", src.display());
}
tracing::debug!(source = %src.display(), "source changed");
}
SyncStatus::TargetChanged => {
// Target changed but source didn't - keep target, will update state
has_changes = true;
if verbose {
println!(" [target changed, keeping] {}", tgt.display());
}
tracing::debug!(target = %tgt.display(), "target changed, keeping");
}
SyncStatus::Conflict => {
// Real conflict - both sides changed this file
@ -151,9 +138,7 @@ pub fn run(
}
SyncStatus::SourceMissing => {
has_changes = true;
if verbose {
println!(" [removed from source] {}", tgt.display());
}
tracing::debug!(target = %tgt.display(), "removed from source");
}
}
}
@ -162,16 +147,14 @@ pub fn run(
conflicts.push((dotfile, SyncStatus::Conflict));
} else if has_changes {
to_deploy.push(dotfile);
} else if verbose {
println!(" [synced] {}", dotfile.target.display());
} else {
tracing::debug!(target = %dotfile.target.display(), "synced");
}
} else {
// Single file handling (unchanged)
match status {
SyncStatus::Synced => {
if verbose {
println!(" [synced] {}", dotfile.target.display());
}
tracing::debug!(target = %dotfile.target.display(), "synced");
}
SyncStatus::NotDeployed | SyncStatus::TargetMissing => {
to_deploy.push(dotfile);
@ -191,7 +174,7 @@ pub fn run(
conflicts.push((dotfile, status));
}
SyncStatus::SourceMissing => {
eprintln!(" [error] source missing: {}", dotfile.source.display());
tracing::error!(source = %dotfile.source.display(), "source missing");
}
}
}
@ -358,7 +341,7 @@ pub fn run(
}
// Run before_deploy hooks
run_hooks(&result.hooks, HookStage::BeforeDeploy, verbose, &hook_env)?;
run_hooks(&result.hooks, HookStage::BeforeDeploy, &hook_env)?;
if to_deploy.is_empty() {
println!("\nNothing to deploy (all files synced).");
@ -386,13 +369,11 @@ pub fn run(
println!(" errors: {}", deploy_result.errors.len());
for deployed in &deploy_result.deployed {
if verbose {
println!(
" [ok] {} -> {}",
deployed.source.display(),
deployed.target.display()
);
}
tracing::debug!(
source = %deployed.source.display(),
target = %deployed.target.display(),
"deployed"
);
}
for skipped in &deploy_result.skipped {
@ -400,21 +381,21 @@ pub fn run(
}
for error in &deploy_result.errors {
eprintln!(
" [err] {} -> {}: {}",
error.source.display(),
error.target.display(),
error.error
tracing::error!(
source = %error.source.display(),
target = %error.target.display(),
error = %error.error,
"deployment failed"
);
}
}
// Run after_deploy hooks
run_hooks(&result.hooks, HookStage::AfterDeploy, verbose, &hook_env)?;
run_hooks(&result.hooks, HookStage::AfterDeploy, &hook_env)?;
if !result.packages.is_empty() {
// Run before_package hooks
run_hooks(&result.hooks, HookStage::BeforePackage, verbose, &hook_env)?;
run_hooks(&result.hooks, HookStage::BeforePackage, &hook_env)?;
if let Some(manager) = doot_core::package::detect_package_manager() {
// Filter out already installed packages
@ -430,10 +411,13 @@ pub fn run(
}
}
if !already_installed.is_empty() && verbose {
println!("\npackages already installed:");
if !already_installed.is_empty() {
tracing::debug!(
count = already_installed.len(),
"packages already installed"
);
for pkg in &already_installed {
println!(" [ok] {}", pkg);
tracing::debug!(package = %pkg, "already installed");
}
}
@ -460,12 +444,13 @@ pub fn run(
}
// Run after_package hooks
run_hooks(&result.hooks, HookStage::AfterPackage, verbose, &hook_env)?;
run_hooks(&result.hooks, HookStage::AfterPackage, &hook_env)?;
}
Ok(())
}
#[tracing::instrument(skip_all)]
fn show_diff(source: &PathBuf, target: &PathBuf) {
use std::process::Command;
@ -485,10 +470,10 @@ fn show_diff(source: &PathBuf, target: &PathBuf) {
}
}
#[tracing::instrument(skip_all)]
fn run_hooks(
hooks: &[HookConfig],
stage: HookStage,
verbose: bool,
env_vars: &std::collections::HashMap<String, String>,
) -> anyhow::Result<()> {
let stage_hooks: Vec<_> = hooks.iter().filter(|h| h.stage == stage).collect();
@ -504,14 +489,10 @@ fn run_hooks(
HookStage::AfterPackage => "after_package",
};
if verbose {
println!("\nrunning {} hooks...", stage_name);
}
tracing::debug!(stage = stage_name, "running hooks");
for hook in stage_hooks {
if verbose {
println!(" $ {}", hook.run);
}
tracing::debug!(command = %hook.run, "executing hook");
let status = Command::new("sh")
.arg("-c")
@ -527,6 +508,7 @@ fn run_hooks(
Ok(())
}
#[tracing::instrument(skip_all)]
fn merge_in_editor(source: &PathBuf, target: &PathBuf) -> anyhow::Result<bool> {
use std::process::Command;

View file

@ -1,13 +1,12 @@
use super::{find_config_file, parse_config, type_check};
use std::path::PathBuf;
pub fn run(config_path: Option<PathBuf>, verbose: bool) -> anyhow::Result<()> {
#[tracing::instrument(skip_all)]
pub fn run(config_path: Option<PathBuf>) -> anyhow::Result<()> {
let path = find_config_file(config_path)?;
let source = std::fs::read_to_string(&path)?;
if verbose {
println!("checking {}", path.display());
}
tracing::debug!(path = %path.display(), "checking config");
let program = parse_config(&path)?;
println!("syntax: ok");

View file

@ -1,7 +1,8 @@
use doot_core::{Config, encryption::AgeEncryption};
use std::path::PathBuf;
pub fn run(file: PathBuf, identity: Option<PathBuf>, verbose: bool) -> anyhow::Result<()> {
#[tracing::instrument(skip_all, fields(file = %file.display()))]
pub fn run(file: PathBuf, identity: Option<PathBuf>) -> anyhow::Result<()> {
let config = Config::default();
let identity_key = if let Some(path) = identity {
std::fs::read_to_string(&path)?.trim().to_string()
@ -18,9 +19,7 @@ pub fn run(file: PathBuf, identity: Option<PathBuf>, verbose: bool) -> anyhow::R
);
};
if verbose {
println!("decrypting {}", file.display());
}
tracing::debug!(file = %file.display(), "decrypting file");
let encryption = AgeEncryption::new().with_identity(&identity_key)?;

View file

@ -3,7 +3,8 @@ use doot_core::deploy::DiffDisplay;
use doot_lang::Evaluator;
use std::path::PathBuf;
pub fn run(config_path: Option<PathBuf>, all: bool, verbose: bool) -> anyhow::Result<()> {
#[tracing::instrument(skip_all, fields(all))]
pub fn run(config_path: Option<PathBuf>, all: bool) -> anyhow::Result<()> {
let path = find_config_file(config_path)?;
let source = std::fs::read_to_string(&path)?;
@ -22,9 +23,7 @@ pub fn run(config_path: Option<PathBuf>, all: bool, verbose: bool) -> anyhow::Re
let target_path = &dotfile.target;
if !source_path.exists() {
if verbose {
println!("[missing] {} (source not found)", source_path.display());
}
tracing::debug!(source = %source_path.display(), "source not found, skipping");
continue;
}

View file

@ -9,12 +9,12 @@ use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
#[tracing::instrument(skip_all, fields(target = %target, auto_apply, skip_prompt))]
pub fn run(
config_path: Option<PathBuf>,
target: String,
auto_apply: bool,
skip_prompt: bool,
verbose: bool,
) -> anyhow::Result<()> {
let path = find_config_file(config_path)?;
let source = std::fs::read_to_string(&path)?;
@ -33,9 +33,7 @@ pub fn run(
let (source_file, dotfile) =
find_source_and_dotfile(&target_path, &result.dotfiles, &source_dir, &state)?;
if verbose {
println!("editing source: {}", source_file.display());
}
tracing::debug!(source = %source_file.display(), "editing source file");
// Get hash before editing
let hash_before = hash_file(&source_file);
@ -67,7 +65,7 @@ pub fn run(
if should_apply {
if let Some(df) = dotfile {
apply_single(&source_file, &df.target, df, &config, verbose)?;
apply_single(&source_file, &df.target, df, &config)?;
println!("applied changes to {}", df.target.display());
} else {
println!("hint: run 'doot apply' to deploy changes");
@ -95,12 +93,12 @@ fn hash_file(path: &PathBuf) -> String {
.unwrap_or_default()
}
#[tracing::instrument(skip_all, fields(source = %source.display(), target = %target.display()))]
fn apply_single(
source: &PathBuf,
target: &PathBuf,
dotfile: &doot_lang::evaluator::DotfileConfig,
config: &Config,
verbose: bool,
) -> anyhow::Result<()> {
let deploy_mode = match dotfile.deploy {
doot_lang::evaluator::DeployMode::Copy => DeployMode::Copy,
@ -122,9 +120,11 @@ fn apply_single(
.map_err(|e| anyhow::anyhow!("template error: {}", e))?;
std::fs::write(target, rendered)?;
if verbose {
println!("rendered {} -> {}", source.display(), target.display());
}
tracing::debug!(
source = %source.display(),
target = %target.display(),
"rendered template"
);
state.record_deployment_with_template(source, target, DeployMode::Copy, true);
state.save()?;
@ -135,9 +135,11 @@ fn apply_single(
DeployMode::Link => {
let linker = Linker::new(config.clone());
linker.link(source, target)?;
if verbose {
println!("linked {} -> {}", source.display(), target.display());
}
tracing::debug!(
source = %source.display(),
target = %target.display(),
"linked"
);
}
DeployMode::Copy => {
if let Some(parent) = target.parent() {
@ -148,9 +150,11 @@ fn apply_single(
} else {
std::fs::copy(source, target)?;
}
if verbose {
println!("copied {} -> {}", source.display(), target.display());
}
tracing::debug!(
source = %source.display(),
target = %target.display(),
"copied"
);
}
}
@ -177,14 +181,17 @@ fn copy_dir_recursive(src: &PathBuf, dst: &PathBuf) -> std::io::Result<()> {
Ok(())
}
#[tracing::instrument(level = "trace")]
fn expand_tilde(path: &str) -> PathBuf {
if path.starts_with("~/")
&& let Some(home) = dirs::home_dir() {
return home.join(&path[2..]);
}
&& let Some(home) = dirs::home_dir()
{
return home.join(&path[2..]);
}
PathBuf::from(path)
}
#[tracing::instrument(skip_all)]
fn find_source_and_dotfile<'a>(
target: &PathBuf,
dotfiles: &'a [doot_lang::evaluator::DotfileConfig],

View file

@ -1,7 +1,8 @@
use doot_core::{Config, encryption::AgeEncryption};
use std::path::PathBuf;
pub fn run(file: PathBuf, recipient: Option<String>, verbose: bool) -> anyhow::Result<()> {
#[tracing::instrument(skip_all, fields(file = %file.display()))]
pub fn run(file: PathBuf, recipient: Option<String>) -> anyhow::Result<()> {
let config_dir = Config::default_config_dir();
let recipient_key = if let Some(r) = recipient {
r
@ -19,13 +20,11 @@ pub fn run(file: PathBuf, recipient: Option<String>, verbose: bool) -> anyhow::R
}
};
if verbose {
println!(
"encrypting {} with recipient {}",
file.display(),
&recipient_key[..20]
);
}
tracing::debug!(
file = %file.display(),
recipient_prefix = &recipient_key[..20.min(recipient_key.len())],
"encrypting file"
);
let mut encryption = AgeEncryption::new();
encryption.add_recipient(&recipient_key)?;

View file

@ -1,7 +1,8 @@
use super::find_config_file;
use std::path::PathBuf;
pub fn run(config_path: Option<PathBuf>, check: bool, _verbose: bool) -> anyhow::Result<()> {
#[tracing::instrument(skip_all, fields(check))]
pub fn run(config_path: Option<PathBuf>, check: bool) -> anyhow::Result<()> {
let path = find_config_file(config_path)?;
let source = std::fs::read_to_string(&path)?;
@ -24,6 +25,7 @@ pub fn run(config_path: Option<PathBuf>, check: bool, _verbose: bool) -> anyhow:
Ok(())
}
#[tracing::instrument(level = "trace", skip_all)]
fn format_source(source: &str) -> String {
let mut result = String::new();
let mut indent_level = 0;

View file

@ -1,17 +1,19 @@
use doot_core::Config;
use std::path::{Path, PathBuf};
pub fn run(path: Option<PathBuf>, verbose: bool) -> anyhow::Result<()> {
#[tracing::instrument(skip_all)]
pub fn run(path: Option<PathBuf>) -> anyhow::Result<()> {
let source_dir = path.unwrap_or_else(Config::default_source_dir);
let config = Config::new(source_dir.clone());
let is_default = source_dir == Config::default_config_dir();
if verbose {
println!("config dir: {}", config.config_dir.display());
println!("state dir: {}", config.state_dir.display());
if !is_default {
println!("source dir: {}", source_dir.display());
}
tracing::debug!(
config_dir = %config.config_dir.display(),
state_dir = %config.state_dir.display(),
"initializing doot"
);
if !is_default {
tracing::debug!(source_dir = %source_dir.display(), "custom source directory");
}
config.ensure_dirs()?;
@ -60,6 +62,7 @@ pub fn run(path: Option<PathBuf>, verbose: bool) -> anyhow::Result<()> {
Ok(())
}
#[tracing::instrument(skip_all)]
fn example_config_with_source(source_dir: &Path) -> String {
format!(
r#"# doot.doot

View file

@ -1,3 +1,4 @@
#[tracing::instrument]
pub fn run() -> anyhow::Result<()> {
println!("doot language server");
println!("LSP support is not yet implemented");

View file

@ -17,6 +17,7 @@ use doot_core::Config;
use doot_lang::{Lexer, Parser, TypeChecker};
use std::path::PathBuf;
#[tracing::instrument(skip_all)]
pub fn find_config_file(base: Option<PathBuf>) -> anyhow::Result<PathBuf> {
if let Some(path) = base {
if path.exists() {
@ -47,6 +48,7 @@ fn byte_offset_to_line(source: &str, offset: usize) -> usize {
+ 1
}
#[tracing::instrument(skip_all, fields(path = %path.display()))]
pub fn parse_config(path: &PathBuf) -> anyhow::Result<doot_lang::Program> {
let source = std::fs::read_to_string(path)?;
let tokens = Lexer::lex(&source).map_err(|errs| {
@ -76,6 +78,7 @@ pub fn parse_config(path: &PathBuf) -> anyhow::Result<doot_lang::Program> {
Ok(program)
}
#[tracing::instrument(skip_all)]
pub fn type_check(
program: &doot_lang::Program,
source: &str,

View file

@ -2,7 +2,8 @@ use super::{find_config_file, parse_config, type_check};
use doot_lang::Evaluator;
use std::path::PathBuf;
pub fn install(config_path: Option<PathBuf>, verbose: bool) -> anyhow::Result<()> {
#[tracing::instrument(skip_all)]
pub fn install(config_path: Option<PathBuf>) -> anyhow::Result<()> {
let path = find_config_file(config_path)?;
let source = std::fs::read_to_string(&path)?;
@ -20,9 +21,7 @@ pub fn install(config_path: Option<PathBuf>, verbose: bool) -> anyhow::Result<()
let manager = doot_core::package::detect_package_manager()
.ok_or_else(|| anyhow::anyhow!("no supported package manager found"))?;
if verbose {
println!("using package manager: {}", manager.name());
}
tracing::debug!(manager = %manager.name(), "using package manager");
let package_names: Vec<String> = result
.packages
@ -43,9 +42,7 @@ pub fn install(config_path: Option<PathBuf>, verbose: bool) -> anyhow::Result<()
println!("installing {} packages...", package_names.len());
for name in &package_names {
if verbose {
println!(" {}", name);
}
tracing::debug!(package = %name, "queued for install");
}
manager.install(&package_names)?;
@ -54,13 +51,12 @@ pub fn install(config_path: Option<PathBuf>, verbose: bool) -> anyhow::Result<()
Ok(())
}
pub fn update(verbose: bool) -> anyhow::Result<()> {
#[tracing::instrument(skip_all)]
pub fn update() -> anyhow::Result<()> {
let manager = doot_core::package::detect_package_manager()
.ok_or_else(|| anyhow::anyhow!("no supported package manager found"))?;
if verbose {
println!("updating package index with {}", manager.name());
}
tracing::debug!(manager = %manager.name(), "updating package index");
manager.update()?;
println!("package index updated");
@ -68,7 +64,8 @@ pub fn update(verbose: bool) -> anyhow::Result<()> {
Ok(())
}
pub fn list(config_path: Option<PathBuf>, _verbose: bool) -> anyhow::Result<()> {
#[tracing::instrument(skip_all)]
pub fn list(config_path: Option<PathBuf>) -> anyhow::Result<()> {
let path = find_config_file(config_path)?;
let source = std::fs::read_to_string(&path)?;

View file

@ -4,11 +4,8 @@ use doot_core::{
};
use std::path::PathBuf;
pub fn run(
_config_path: Option<PathBuf>,
snapshot_name: Option<String>,
verbose: bool,
) -> anyhow::Result<()> {
#[tracing::instrument(skip_all)]
pub fn run(_config_path: Option<PathBuf>, snapshot_name: Option<String>) -> anyhow::Result<()> {
let config = Config::default();
let name = if let Some(n) = snapshot_name {
@ -33,10 +30,11 @@ pub fn run(
anyhow::bail!("please specify a snapshot name or 'last'");
};
if verbose {
println!("rolling back to snapshot: {}", name);
println!(" snapshot dir: {}", config.snapshot_dir.display());
}
tracing::debug!(
snapshot = %name,
snapshot_dir = %config.snapshot_dir.display(),
"rolling back to snapshot"
);
let snapshot = Snapshot::load(&name, &config.snapshot_dir)?;
@ -44,32 +42,26 @@ pub fn run(
let target = PathBuf::from(target_str);
if target.is_symlink() {
if verbose {
println!("removing symlink: {}", target.display());
}
tracing::debug!(target = %target.display(), "removing symlink");
std::fs::remove_file(&target)?;
}
match record.mode {
DeployMode::Link => {
if verbose {
println!(
"recreating symlink: {} -> {}",
record.source.display(),
target.display()
);
}
tracing::debug!(
source = %record.source.display(),
target = %target.display(),
"recreating symlink"
);
#[cfg(unix)]
std::os::unix::fs::symlink(&record.source, &target)?;
}
DeployMode::Copy => {
if verbose {
println!(
"restoring copy: {} -> {}",
record.source.display(),
target.display()
);
}
tracing::debug!(
source = %record.source.display(),
target = %target.display(),
"restoring copy"
);
if record.source.exists() {
std::fs::copy(&record.source, &target)?;
}

View file

@ -4,17 +4,18 @@ use doot_core::{
};
use std::path::PathBuf;
pub fn run(_config_path: Option<PathBuf>, name: String, verbose: bool) -> anyhow::Result<()> {
#[tracing::instrument(skip_all, fields(name = %name))]
pub fn run(_config_path: Option<PathBuf>, name: String) -> anyhow::Result<()> {
let config = Config::default();
config.ensure_dirs()?;
let mut state = StateStore::new(&config.state_file);
if verbose {
println!("creating snapshot: {}", name);
println!(" state file: {}", config.state_file.display());
println!(" snapshot dir: {}", config.snapshot_dir.display());
}
tracing::debug!(
state_file = %config.state_file.display(),
snapshot_dir = %config.snapshot_dir.display(),
"creating snapshot"
);
let state_content =
std::fs::read_to_string(&config.state_file).unwrap_or_else(|_| "{}".to_string());

View file

@ -3,7 +3,8 @@ use doot_core::state::StateStore;
use doot_lang::Evaluator;
use std::path::PathBuf;
pub fn run(config_path: Option<PathBuf>, verbose: bool) -> anyhow::Result<()> {
#[tracing::instrument(skip_all)]
pub fn run(config_path: Option<PathBuf>) -> anyhow::Result<()> {
let path = find_config_file(config_path)?;
let source = std::fs::read_to_string(&path)?;
@ -57,8 +58,12 @@ pub fn run(config_path: Option<PathBuf>, verbose: bool) -> anyhow::Result<()> {
target.display()
);
if verbose && status != "ok" && status != "deployed" {
println!(" status: {}", status);
if status != "ok" && status != "deployed" {
tracing::debug!(
target = %target.display(),
status = status,
"dotfile status detail"
);
}
}

View file

@ -19,6 +19,7 @@ use ratatui::{
use std::io;
use std::path::PathBuf;
#[tracing::instrument(skip_all)]
pub fn run(config_path: Option<PathBuf>) -> anyhow::Result<()> {
enable_raw_mode()?;
let mut stdout = io::stdout();
@ -108,6 +109,7 @@ enum FileStatus {
}
impl App {
#[tracing::instrument(skip_all)]
fn new(config_path: Option<PathBuf>) -> anyhow::Result<Self> {
let path = find_config_file(config_path)?;
let source = std::fs::read_to_string(&path)?;
@ -296,15 +298,17 @@ impl App {
Tab::Dotfiles => {
if let Some(i) = self.dotfile_state.selected()
&& let Some(item) = self.dotfiles.get_mut(i)
&& item.status != FileStatus::Error {
item.selected = !item.selected;
}
&& item.status != FileStatus::Error
{
item.selected = !item.selected;
}
}
Tab::Packages => {
if let Some(i) = self.package_state.selected()
&& let Some(item) = self.packages.get_mut(i) {
item.selected = !item.selected;
}
&& let Some(item) = self.packages.get_mut(i)
{
item.selected = !item.selected;
}
}
_ => {}
}
@ -352,6 +356,7 @@ impl App {
}
}
#[tracing::instrument(skip(self))]
fn apply(&mut self) {
if self.apply_state == ApplyState::Applying {
return;
@ -404,14 +409,14 @@ impl App {
let has_packages = self.packages.iter().any(|p| p.selected && !p.installed);
let has_owner = self.dotfiles.iter().any(|d| d.selected);
if has_packages
&& let Some(manager) = doot_core::package::detect_package_manager() {
return manager.needs_sudo();
}
if has_packages && let Some(manager) = doot_core::package::detect_package_manager() {
return manager.needs_sudo();
}
has_owner
}
#[tracing::instrument(skip(self))]
fn apply_with_sudo(&mut self) {
let selected_dotfiles: Vec<_> = self
.dotfiles
@ -541,6 +546,7 @@ impl App {
}
}
#[tracing::instrument(skip_all)]
fn run_app(
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
config_path: Option<PathBuf>,
@ -551,69 +557,68 @@ fn run_app(
terminal.draw(|f| ui(f, &mut app))?;
if let Event::Key(key) = event::read()?
&& key.kind == KeyEventKind::Press {
match app.input_mode {
InputMode::Password => match key.code {
KeyCode::Enter => {
app.sudo_password = Some(app.password_input.clone());
app.password_input.clear();
app.input_mode = InputMode::Normal;
app.apply_state = ApplyState::Applying;
app.apply_with_sudo();
&& key.kind == KeyEventKind::Press
{
match app.input_mode {
InputMode::Password => match key.code {
KeyCode::Enter => {
app.sudo_password = Some(app.password_input.clone());
app.password_input.clear();
app.input_mode = InputMode::Normal;
app.apply_state = ApplyState::Applying;
app.apply_with_sudo();
}
KeyCode::Esc => {
app.password_input.clear();
app.input_mode = InputMode::Normal;
app.apply_state = ApplyState::Idle;
}
KeyCode::Backspace => {
app.password_input.pop();
}
KeyCode::Char(c) => {
app.password_input.push(c);
}
_ => {}
},
InputMode::Normal => match app.apply_state {
ApplyState::Idle => match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Tab => app.next_tab(),
KeyCode::BackTab => app.prev_tab(),
KeyCode::Down | KeyCode::Char('j') => app.next_item(),
KeyCode::Up | KeyCode::Char('k') => app.prev_item(),
KeyCode::Char(' ') => app.toggle_selected(),
KeyCode::Char('a') => app.select_all(),
KeyCode::Char('n') => app.select_none(),
KeyCode::Enter => app.apply(),
KeyCode::Char('1') => app.tab = Tab::Dotfiles,
KeyCode::Char('2') => app.tab = Tab::Packages,
KeyCode::Char('3') => app.tab = Tab::Secrets,
KeyCode::Char('4') => app.tab = Tab::Status,
_ => {}
},
ApplyState::Applying => {
// Can't do anything while applying
}
ApplyState::NeedsSudo => match key.code {
KeyCode::Char('y') | KeyCode::Enter => {
app.input_mode = InputMode::Password;
}
KeyCode::Esc => {
app.password_input.clear();
app.input_mode = InputMode::Normal;
KeyCode::Char('n') | KeyCode::Esc => {
app.apply_state = ApplyState::Idle;
}
KeyCode::Backspace => {
app.password_input.pop();
}
KeyCode::Char(c) => {
app.password_input.push(c);
}
_ => {}
},
InputMode::Normal => match app.apply_state {
ApplyState::Idle => match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Tab => app.next_tab(),
KeyCode::BackTab => app.prev_tab(),
KeyCode::Down | KeyCode::Char('j') => app.next_item(),
KeyCode::Up | KeyCode::Char('k') => app.prev_item(),
KeyCode::Char(' ') => app.toggle_selected(),
KeyCode::Char('a') => app.select_all(),
KeyCode::Char('n') => app.select_none(),
KeyCode::Enter => app.apply(),
KeyCode::Char('1') => app.tab = Tab::Dotfiles,
KeyCode::Char('2') => app.tab = Tab::Packages,
KeyCode::Char('3') => app.tab = Tab::Secrets,
KeyCode::Char('4') => app.tab = Tab::Status,
_ => {}
},
ApplyState::Applying => {
// Can't do anything while applying
}
ApplyState::NeedsSudo => match key.code {
KeyCode::Char('y') | KeyCode::Enter => {
app.input_mode = InputMode::Password;
}
KeyCode::Char('n') | KeyCode::Esc => {
app.apply_state = ApplyState::Idle;
}
_ => {}
},
ApplyState::Done => match key.code {
KeyCode::Enter | KeyCode::Esc | KeyCode::Char('q') => {
app.dismiss_apply()
}
KeyCode::Up | KeyCode::Char('k') => app.scroll_log_up(),
KeyCode::Down | KeyCode::Char('j') => app.scroll_log_down(),
_ => {}
},
ApplyState::Done => match key.code {
KeyCode::Enter | KeyCode::Esc | KeyCode::Char('q') => app.dismiss_apply(),
KeyCode::Up | KeyCode::Char('k') => app.scroll_log_up(),
KeyCode::Down | KeyCode::Char('j') => app.scroll_log_down(),
_ => {}
},
}
},
}
}
}
}

View file

@ -1,7 +1,8 @@
mod commands;
use clap::{Parser, Subcommand};
use clap::{Parser, Subcommand, ValueEnum};
use std::path::PathBuf;
use tracing_subscriber::EnvFilter;
#[derive(Parser)]
#[command(name = "doot")]
@ -10,11 +11,39 @@ struct Cli {
#[command(subcommand)]
command: Commands,
/// Increase verbosity (-v = info, -vv = debug, -vvv = trace)
#[arg(short, long, global = true, action = clap::ArgAction::Count)]
verbose: u8,
/// Suppress all log output
#[arg(short, long, global = true)]
verbose: bool,
quiet: bool,
#[arg(short = 'C', long, global = true)]
config: Option<PathBuf>,
/// Log level (overrides -v/-q flags)
#[arg(long, global = true)]
log_level: Option<LogLevelArg>,
/// Log output format
#[arg(long, global = true, default_value = "text")]
log_format: LogFormatArg,
}
#[derive(Clone, ValueEnum)]
enum LogLevelArg {
Trace,
Debug,
Info,
Warn,
Error,
}
#[derive(Clone, ValueEnum)]
enum LogFormatArg {
Text,
Json,
}
#[derive(Subcommand)]
@ -102,32 +131,82 @@ enum PackageAction {
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
// Priority: DOOT_LOG env > --log-level flag > -q/v flags > default (warn)
let default_level = if let Some(ref level) = cli.log_level {
match level {
LogLevelArg::Trace => "trace",
LogLevelArg::Debug => "debug",
LogLevelArg::Info => "info",
LogLevelArg::Warn => "warn",
LogLevelArg::Error => "error",
}
} else if cli.quiet {
"off"
} else {
match cli.verbose {
0 => "warn",
1 => "info",
2 => "debug",
_ => "trace",
}
};
// Scope doot crates to chosen level, third-party at warn
// -vvvv+ enables trace for ALL crates including deps
let default_directive = if default_level == "off" {
"off".to_string()
} else if cli.verbose >= 4 {
default_level.to_string()
} else {
format!(
"doot_cli={l},doot_core={l},doot_lang={l},warn",
l = default_level
)
};
let env_filter =
EnvFilter::try_from_env("DOOT_LOG").unwrap_or_else(|_| EnvFilter::new(&default_directive));
match cli.log_format {
LogFormatArg::Json => {
tracing_subscriber::fmt()
.json()
.with_env_filter(env_filter)
.with_target(true)
.with_writer(std::io::stderr)
.init();
}
LogFormatArg::Text => {
tracing_subscriber::fmt()
.with_env_filter(env_filter)
.with_target(true)
.with_writer(std::io::stderr)
.init();
}
}
match cli.command {
Commands::Init { path } => commands::init::run(path, cli.verbose),
Commands::Init { path } => commands::init::run(path),
Commands::Apply { dry_run, parallel } => {
commands::apply::run(cli.config, dry_run, parallel, cli.verbose)
commands::apply::run(cli.config, dry_run, parallel)
}
Commands::Diff { all } => commands::diff::run(cli.config, all, cli.verbose),
Commands::Status => commands::status::run(cli.config, cli.verbose),
Commands::Check => commands::check::run(cli.config, cli.verbose),
Commands::Fmt { check } => commands::fmt::run(cli.config, check, cli.verbose),
Commands::Rollback { snapshot } => {
commands::rollback::run(cli.config, snapshot, cli.verbose)
}
Commands::Snapshot { name } => commands::snapshot::run(cli.config, name, cli.verbose),
Commands::Encrypt { file, recipient } => {
commands::encrypt::run(file, recipient, cli.verbose)
}
Commands::Decrypt { file, identity } => commands::decrypt::run(file, identity, cli.verbose),
Commands::Diff { all } => commands::diff::run(cli.config, all),
Commands::Status => commands::status::run(cli.config),
Commands::Check => commands::check::run(cli.config),
Commands::Fmt { check } => commands::fmt::run(cli.config, check),
Commands::Rollback { snapshot } => commands::rollback::run(cli.config, snapshot),
Commands::Snapshot { name } => commands::snapshot::run(cli.config, name),
Commands::Encrypt { file, recipient } => commands::encrypt::run(file, recipient),
Commands::Decrypt { file, identity } => commands::decrypt::run(file, identity),
Commands::Package { action } => match action {
PackageAction::Install => commands::package::install(cli.config, cli.verbose),
PackageAction::Update => commands::package::update(cli.verbose),
PackageAction::List => commands::package::list(cli.config, cli.verbose),
PackageAction::Install => commands::package::install(cli.config),
PackageAction::Update => commands::package::update(),
PackageAction::List => commands::package::list(cli.config),
},
Commands::Lsp => commands::lsp::run(),
Commands::Tui => commands::tui::run(cli.config),
Commands::Edit { target, apply, yes } => {
commands::edit::run(cli.config, target, apply, yes, cli.verbose)
commands::edit::run(cli.config, target, apply, yes)
}
}
}

View file

@ -24,3 +24,4 @@ regex-lite = "0.1"
glob = "0.3"
minijinja = { version = "2", features = ["builtins"] }
which = "7"
tracing.workspace = true

View file

@ -30,6 +30,7 @@ pub struct Config {
impl Config {
/// Creates a new config with the given source directory.
#[tracing::instrument(skip_all, fields(source_dir = %source_dir.display()))]
pub fn new(source_dir: PathBuf) -> Self {
let config_dir = Self::default_config_dir();
let state_dir = Self::default_state_dir();
@ -106,6 +107,7 @@ impl Config {
}
/// Creates all required directories.
#[tracing::instrument(skip(self))]
pub fn ensure_dirs(&self) -> std::io::Result<()> {
std::fs::create_dir_all(&self.config_dir)?;
std::fs::create_dir_all(&self.state_dir)?;

View file

@ -8,6 +8,7 @@ pub struct DiffDisplay;
impl DiffDisplay {
/// Diffs two files and returns a formatted string.
#[tracing::instrument(skip_all)]
pub fn diff_files(source: &PathBuf, target: &PathBuf) -> Result<String, std::io::Error> {
let source_content = std::fs::read_to_string(source)?;
let target_content = if target.exists() {
@ -37,6 +38,7 @@ impl DiffDisplay {
}
/// Checks if source and target differ.
#[tracing::instrument(skip_all)]
pub fn has_changes(source: &PathBuf, target: &PathBuf) -> Result<bool, std::io::Error> {
if !target.exists() {
return Ok(true);
@ -54,6 +56,7 @@ impl DiffDisplay {
}
/// Returns a unified diff format.
#[tracing::instrument(skip_all)]
pub fn unified_diff(source: &PathBuf, target: &PathBuf) -> Result<String, std::io::Error> {
let source_content = std::fs::read_to_string(source)?;
let target_content = if target.exists() {

View file

@ -11,11 +11,13 @@ pub struct Linker {
impl Linker {
/// Creates a new linker.
#[tracing::instrument(skip_all)]
pub fn new(config: Config) -> Self {
Self { config }
}
/// Creates a symlink from source to target.
#[tracing::instrument(skip(self), fields(source = %source.display(), target = %target.display()))]
pub fn link(&self, source: &PathBuf, target: &PathBuf) -> Result<DeployAction, DeployError> {
if target.is_symlink() {
let current_target = std::fs::read_link(target)?;
@ -59,6 +61,7 @@ impl Linker {
}
/// Removes a symlink.
#[tracing::instrument(skip(self), fields(target = %target.display()))]
pub fn unlink(&self, target: &PathBuf) -> Result<(), DeployError> {
if target.is_symlink() && !self.config.dry_run {
std::fs::remove_file(target)?;
@ -67,6 +70,7 @@ impl Linker {
}
/// Checks if target is linked to source.
#[tracing::instrument(level = "trace", skip(self))]
pub fn is_linked(&self, source: &PathBuf, target: &PathBuf) -> bool {
if !target.is_symlink() {
return false;

View file

@ -102,6 +102,7 @@ pub struct Deployer {
impl Deployer {
/// Creates a new deployer.
#[tracing::instrument(skip_all)]
pub fn new(config: Config, sandbox: bool) -> Self {
let state = StateStore::new(&config.state_file);
Self {
@ -113,6 +114,7 @@ impl Deployer {
}
}
#[tracing::instrument(skip(self), fields(target = %target.display()))]
fn check_sandbox(&self, target: &Path) -> Result<(), DeployError> {
if !self.sandbox {
return Ok(());
@ -131,6 +133,7 @@ impl Deployer {
}
/// Deploys all dotfiles.
#[tracing::instrument(skip_all)]
pub fn deploy(&mut self, dotfiles: &[DotfileConfig]) -> Result<DeployResult, DeployError> {
let mut result = DeployResult {
deployed: Vec::new(),
@ -162,6 +165,7 @@ impl Deployer {
Ok(result)
}
#[tracing::instrument(skip(self), fields(source = %dotfile.source.display(), target = %dotfile.target.display()))]
fn deploy_single(&mut self, dotfile: &DotfileConfig) -> Result<DeployedFile, DeployError> {
let source = self.config.source_dir.join(&dotfile.source);
let target = &dotfile.target;
@ -181,11 +185,10 @@ impl Deployer {
}
// For files or link mode, handle as before
if target.exists() && !target.is_symlink()
&& !self.config.dry_run {
self.backup_existing(target)?;
std::fs::remove_file(target)?;
}
if target.exists() && !target.is_symlink() && !self.config.dry_run {
self.backup_existing(target)?;
std::fs::remove_file(target)?;
}
let action = if dotfile.template {
self.deploy_template(&source, target)?
@ -206,9 +209,10 @@ impl Deployer {
// Set owner if specified
if let Some(ref owner) = dotfile.owner
&& !self.config.dry_run {
set_owner(target, owner)?;
}
&& !self.config.dry_run
{
set_owner(target, owner)?;
}
self.state
.record_deployment_with_template(&source, target, deploy_mode, dotfile.template);
@ -220,6 +224,7 @@ impl Deployer {
})
}
#[tracing::instrument(skip(self), fields(source = %source.display(), target = %target.display()))]
fn deploy_directory(
&mut self,
dotfile: &DotfileConfig,
@ -230,6 +235,10 @@ impl Deployer {
use crate::state::SyncStatus;
let changed_files = self.state.get_changed_files_in_dir(source, target);
tracing::trace!(
changed_count = changed_files.len(),
"directory file changes"
);
if changed_files.is_empty() {
return Ok(DeployedFile {
@ -305,9 +314,10 @@ impl Deployer {
// Set owner if specified (for entire directory)
if let Some(ref owner) = dotfile.owner
&& !self.config.dry_run {
set_owner(target, owner)?;
}
&& !self.config.dry_run
{
set_owner(target, owner)?;
}
// Also record the directory-level deployment for sync status checks
self.state.record_deployment(source, target, deploy_mode);
@ -327,6 +337,7 @@ impl Deployer {
})
}
#[tracing::instrument(level = "trace", skip(self))]
fn resolve_deploy_mode(&self, dotfile: &DotfileConfig, source: &Path) -> DeployMode {
let relative_path = source
.strip_prefix(&self.config.source_dir)
@ -338,29 +349,33 @@ impl Deployer {
doot_lang::evaluator::DeployMode::Copy => DeployMode::Copy,
doot_lang::evaluator::DeployMode::Link => DeployMode::Link,
};
tracing::trace!(mode = ?base_mode, "resolved deploy mode");
match base_mode {
DeployMode::Copy => {
for pattern in &dotfile.link_patterns {
if let Ok(p) = Pattern::new(pattern)
&& p.matches(&relative_path) {
return DeployMode::Link;
}
&& p.matches(&relative_path)
{
return DeployMode::Link;
}
}
DeployMode::Copy
}
DeployMode::Link => {
for pattern in &dotfile.copy_patterns {
if let Ok(p) = Pattern::new(pattern)
&& p.matches(&relative_path) {
return DeployMode::Copy;
}
&& p.matches(&relative_path)
{
return DeployMode::Copy;
}
}
DeployMode::Link
}
}
}
#[tracing::instrument(skip(self), fields(source = %source.display(), target = %target.display()))]
fn copy_single_file(&self, source: &Path, target: &Path) -> Result<DeployAction, DeployError> {
if target.exists() {
let source_content = std::fs::read(source)?;
@ -384,6 +399,7 @@ impl Deployer {
})
}
#[tracing::instrument(skip(self), fields(source = %source.display(), target = %target.display()))]
fn deploy_template(
&self,
source: &PathBuf,
@ -416,6 +432,7 @@ impl Deployer {
})
}
#[tracing::instrument(skip(self), fields(target = %target.display()))]
fn backup_existing(&self, target: &PathBuf) -> Result<(), DeployError> {
let backup_path = self.config.backup_dir.join(
target
@ -471,6 +488,7 @@ fn copy_dir_recursive(src: &Path, dst: &Path) -> std::io::Result<()> {
use doot_lang::evaluator::PermissionRule;
#[tracing::instrument(level = "trace", skip_all)]
fn apply_permissions(target: &Path, rules: &[PermissionRule]) -> Result<(), DeployError> {
if target.is_file() {
// For single files, apply first matching rule
@ -524,10 +542,11 @@ fn apply_permissions_recursive(
}
PermissionRule::Pattern { pattern, mode } => {
if let Ok(p) = Pattern::new(pattern)
&& p.matches(&relative) {
set_file_permissions(&path, *mode)?;
break;
}
&& p.matches(&relative)
{
set_file_permissions(&path, *mode)?;
break;
}
}
}
}
@ -550,6 +569,7 @@ fn set_file_permissions(_path: &Path, _mode: u32) -> Result<(), DeployError> {
}
#[cfg(unix)]
#[tracing::instrument(skip_all)]
fn set_owner(path: &Path, owner: &str) -> Result<(), DeployError> {
use std::process::Command;
@ -586,6 +606,7 @@ fn set_owner(path: &Path, owner: &str) -> Result<(), DeployError> {
}
#[cfg(not(unix))]
#[tracing::instrument(skip_all)]
fn set_owner(_path: &Path, _owner: &str) -> Result<(), DeployError> {
Ok(())
}

View file

@ -12,6 +12,7 @@ pub struct TemplateEngine {
impl TemplateEngine {
/// Creates a new engine with default variables and functions.
#[tracing::instrument(skip_all)]
pub fn new() -> Self {
let mut env = Environment::new();
@ -30,6 +31,7 @@ impl TemplateEngine {
}
/// Renders a template string.
#[tracing::instrument(skip_all)]
pub fn render(&self, template: &str) -> Result<String, String> {
// Add template to environment
let tmpl = self
@ -52,6 +54,7 @@ impl Default for TemplateEngine {
}
/// Builds the default template variables.
#[tracing::instrument(skip_all)]
fn build_default_variables() -> HashMap<String, Value> {
let mut vars = HashMap::new();
@ -91,9 +94,10 @@ fn build_default_variables() -> HashMap<String, Value> {
// Detect Linux distro
if std::env::consts::OS == "linux"
&& let Some(distro) = detect_distro() {
vars.insert("distro".to_string(), Value::from(distro));
}
&& let Some(distro) = detect_distro()
{
vars.insert("distro".to_string(), Value::from(distro));
}
// Environment variables as a nested object
let env_vars: HashMap<String, Value> =
@ -367,6 +371,7 @@ fn register_functions(env: &mut Environment<'static>) {
}
/// Expands ~ to home directory in paths.
#[tracing::instrument(level = "trace")]
fn expand_path(s: &str) -> PathBuf {
if let Some(stripped) = s.strip_prefix('~') {
let home = dirs::home_dir().unwrap_or_default();
@ -377,6 +382,7 @@ fn expand_path(s: &str) -> PathBuf {
}
/// Detects the Linux distribution.
#[tracing::instrument(level = "trace")]
fn detect_distro() -> Option<String> {
if std::env::consts::OS != "linux" {
return None;

View file

@ -40,6 +40,7 @@ impl AgeEncryption {
}
/// Sets the identity for decryption.
#[tracing::instrument(skip_all)]
pub fn with_identity(mut self, identity_str: &str) -> Result<Self, EncryptionError> {
let identity = identity_str
.parse::<age::x25519::Identity>()
@ -49,6 +50,7 @@ impl AgeEncryption {
}
/// Adds a recipient public key.
#[tracing::instrument(skip_all)]
pub fn add_recipient(&mut self, recipient_str: &str) -> Result<(), EncryptionError> {
let recipient = recipient_str
.parse::<age::x25519::Recipient>()
@ -58,6 +60,7 @@ impl AgeEncryption {
}
/// Encrypts data for all recipients.
#[tracing::instrument(skip_all)]
pub fn encrypt(&self, data: &[u8]) -> Result<Vec<u8>, EncryptionError> {
if self.recipients.is_empty() {
return Err(EncryptionError::EncryptionFailed(
@ -90,6 +93,7 @@ impl AgeEncryption {
}
/// Decrypts data using the configured identity.
#[tracing::instrument(skip_all)]
pub fn decrypt(&self, data: &[u8]) -> Result<Vec<u8>, EncryptionError> {
let identity = self.identity.as_ref().ok_or_else(|| {
EncryptionError::DecryptionFailed("no identity configured".to_string())
@ -119,6 +123,7 @@ impl AgeEncryption {
}
/// Encrypts a file to a target path.
#[tracing::instrument(skip(self), fields(source = %source.display(), target = %target.display()))]
pub fn encrypt_file(&self, source: &PathBuf, target: &PathBuf) -> Result<(), EncryptionError> {
let data = std::fs::read(source)?;
let encrypted = self.encrypt(&data)?;
@ -127,6 +132,7 @@ impl AgeEncryption {
}
/// Decrypts a file to a target path.
#[tracing::instrument(skip(self), fields(source = %source.display(), target = %target.display()))]
pub fn decrypt_file(&self, source: &PathBuf, target: &PathBuf) -> Result<(), EncryptionError> {
let data = std::fs::read(source)?;
let decrypted = self.decrypt(&data)?;

View file

@ -49,6 +49,7 @@ impl HookRunner {
}
/// Runs all hooks for a given stage.
#[tracing::instrument(skip(self))]
pub fn run_stage(&self, stage: HookStage) -> Result<(), HookError> {
for hook in self.hooks.iter().filter(|h| h.stage == stage) {
self.run_hook(hook)?;
@ -56,6 +57,7 @@ impl HookRunner {
Ok(())
}
#[tracing::instrument(skip(self), fields(command = %hook.command))]
fn run_hook(&self, hook: &Hook) -> Result<(), HookError> {
if self.dry_run {
println!("[dry-run] would run: {}", hook.command);

View file

@ -25,6 +25,7 @@ pub enum OsType {
impl OsInfo {
/// Detects the current operating system.
#[tracing::instrument]
pub fn detect() -> Self {
let info = os_info::get();
@ -79,6 +80,7 @@ impl OsInfo {
}
/// Detects the available package manager.
#[tracing::instrument(skip(self))]
pub fn detect_package_manager(&self) -> Option<&'static str> {
match self.os_type {
OsType::MacOS => Some("brew"),
@ -116,6 +118,7 @@ impl Default for OsInfo {
static COMMAND_CACHE: OnceLock<std::sync::Mutex<HashMap<String, bool>>> = OnceLock::new();
/// Checks if a command exists in PATH or common bin directories (cached).
#[tracing::instrument(level = "trace")]
fn command_exists(cmd: &str) -> bool {
let cache = COMMAND_CACHE.get_or_init(|| std::sync::Mutex::new(HashMap::new()));
let mut cache = cache.lock().unwrap();

View file

@ -25,6 +25,7 @@ impl Apt {
self
}
#[tracing::instrument(skip(self))]
fn run_apt(&self, args: &[&str]) -> Result<(), PackageError> {
if self.dry_run {
let prefix = if self.use_sudo { "sudo " } else { "" };
@ -48,6 +49,7 @@ impl Apt {
Ok(())
}
#[tracing::instrument(skip(self, password))]
fn run_apt_with_password(&self, args: &[&str], password: &str) -> Result<(), PackageError> {
if self.dry_run {
println!("[dry-run] sudo apt {}", args.join(" "));

View file

@ -15,6 +15,7 @@ impl Brew {
self
}
#[tracing::instrument(skip(self))]
fn run_brew(&self, args: &[&str]) -> Result<(), PackageError> {
if self.dry_run {
println!("[dry-run] brew {}", args.join(" "));

View file

@ -134,6 +134,7 @@ impl PackageManager for MockPackageManager {
}
/// Detects the available package manager.
#[tracing::instrument]
pub fn detect_package_manager() -> Option<Box<dyn PackageManager>> {
if is_test_mode() {
return Some(Box::new(MockPackageManager::new()));
@ -151,6 +152,7 @@ pub fn detect_package_manager() -> Option<Box<dyn PackageManager>> {
}
/// Gets a package manager by name.
#[tracing::instrument]
pub fn get_package_manager(name: &str) -> Option<Box<dyn PackageManager>> {
if is_test_mode() {
return Some(Box::new(MockPackageManager::new()));

View file

@ -25,6 +25,7 @@ impl Pacman {
self
}
#[tracing::instrument(skip(self))]
fn run_pacman(&self, args: &[&str]) -> Result<(), PackageError> {
if self.dry_run {
let prefix = if self.use_sudo { "sudo " } else { "" };
@ -48,6 +49,7 @@ impl Pacman {
Ok(())
}
#[tracing::instrument(skip(self, password))]
fn run_pacman_with_password(&self, args: &[&str], password: &str) -> Result<(), PackageError> {
if self.dry_run {
println!("[dry-run] sudo pacman {}", args.join(" "));

View file

@ -15,6 +15,7 @@ impl Yay {
self
}
#[tracing::instrument(skip(self))]
fn run_yay(&self, args: &[&str]) -> Result<(), PackageError> {
if self.dry_run {
println!("[dry-run] yay {}", args.join(" "));

View file

@ -12,6 +12,7 @@ pub struct Snapshot {
impl Snapshot {
/// Creates and saves a new snapshot.
#[tracing::instrument(skip_all, fields(name))]
pub fn create(name: &str, state: &State, snapshot_dir: &Path) -> Result<Self, StateError> {
let created_at = chrono_now();
let snapshot = Self {
@ -25,6 +26,7 @@ impl Snapshot {
}
/// Loads a snapshot by name.
#[tracing::instrument(skip_all, fields(name))]
pub fn load(name: &str, snapshot_dir: &Path) -> Result<Self, StateError> {
let path = snapshot_dir.join(format!("{}.json", name));
let content = std::fs::read_to_string(&path)?;
@ -38,6 +40,7 @@ impl Snapshot {
}
/// Saves the snapshot to disk.
#[tracing::instrument(skip(self))]
pub fn save(&self, snapshot_dir: &Path) -> Result<(), StateError> {
std::fs::create_dir_all(snapshot_dir)?;
let path = snapshot_dir.join(format!("{}.json", self.name));
@ -47,6 +50,7 @@ impl Snapshot {
}
/// Lists all snapshots.
#[tracing::instrument(skip_all)]
pub fn list(snapshot_dir: &Path) -> Result<Vec<String>, StateError> {
if !snapshot_dir.exists() {
return Ok(Vec::new());
@ -57,9 +61,10 @@ impl Snapshot {
let entry = entry?;
let path = entry.path();
if path.extension().map(|e| e == "json").unwrap_or(false)
&& let Some(name) = path.file_stem() {
snapshots.push(name.to_string_lossy().to_string());
}
&& let Some(name) = path.file_stem()
{
snapshots.push(name.to_string_lossy().to_string());
}
}
snapshots.sort();
@ -67,6 +72,7 @@ impl Snapshot {
}
/// Deletes a snapshot.
#[tracing::instrument(skip_all, fields(name))]
pub fn delete(name: &str, snapshot_dir: &Path) -> Result<(), StateError> {
let path = snapshot_dir.join(format!("{}.json", name));
if path.exists() {

View file

@ -75,6 +75,7 @@ pub struct StateStore {
impl StateStore {
/// Loads or creates a state store at the given path.
#[tracing::instrument(skip_all, fields(path = %path.display()))]
pub fn new(path: &Path) -> Self {
let state = if path.exists() {
std::fs::read_to_string(path)
@ -93,11 +94,13 @@ impl StateStore {
}
/// Records a deployment with both source and target hashes.
#[tracing::instrument(skip(self), fields(source = %source.display(), target = %target.display()))]
pub fn record_deployment(&mut self, source: &Path, target: &Path, mode: DeployMode) {
self.record_deployment_with_template(source, target, mode, false);
}
/// Records a deployment with template flag.
#[tracing::instrument(skip(self), fields(source = %source.display(), target = %target.display()))]
pub fn record_deployment_with_template(
&mut self,
source: &Path,
@ -125,11 +128,13 @@ impl StateStore {
}
/// Checks sync status by comparing current hashes with recorded state.
#[tracing::instrument(level = "trace", skip(self))]
pub fn check_sync_status(&self, source: &Path, target: &Path) -> SyncStatus {
self.check_sync_status_with_config(source, target, None, None)
}
/// Checks sync status, also detecting if template flag changed in config.
#[tracing::instrument(level = "trace", skip(self))]
pub fn check_sync_status_with_template(
&self,
source: &Path,
@ -140,6 +145,7 @@ impl StateStore {
}
/// Checks sync status, also detecting if config flags changed.
#[tracing::instrument(level = "trace", skip(self))]
pub fn check_sync_status_with_config(
&self,
source: &Path,
@ -153,15 +159,17 @@ impl StateStore {
// If template flag changed in config, force re-deploy
if let Some(is_template) = current_template
&& is_template != record.template {
return SyncStatus::SourceChanged;
}
&& is_template != record.template
{
return SyncStatus::SourceChanged;
}
// If deploy mode changed in config, force re-deploy
if let Some(mode) = current_mode
&& mode != record.mode {
return SyncStatus::SourceChanged;
}
&& mode != record.mode
{
return SyncStatus::SourceChanged;
}
if !source.exists() {
return SyncStatus::SourceMissing;
@ -190,6 +198,7 @@ impl StateStore {
let current_source_hash = hash_path(source);
let current_target_hash = hash_path(target);
tracing::trace!(source_hash = %current_source_hash, target_hash = %current_target_hash, "computed hashes");
let source_changed = current_source_hash != record.source_hash;
let target_changed = current_target_hash != record.target_hash;
@ -203,6 +212,7 @@ impl StateStore {
}
/// Records a package installation.
#[tracing::instrument(skip(self))]
pub fn record_package(&mut self, name: &str, manager: &str) {
let record = PackageRecord {
name: name.to_string(),
@ -242,6 +252,7 @@ impl StateStore {
}
/// Saves state to disk if dirty.
#[tracing::instrument(skip(self))]
pub fn save(&mut self) -> Result<(), StateError> {
if !self.dirty {
return Ok(());
@ -274,6 +285,7 @@ impl StateStore {
}
/// Records a directory deployment by tracking each file individually.
#[tracing::instrument(skip(self))]
pub fn record_directory_deployment(
&mut self,
source_dir: &Path,
@ -293,6 +305,7 @@ impl StateStore {
/// Returns files that have changed in a directory.
/// Returns (source_path, target_path, status) for each changed file.
#[tracing::instrument(skip(self), fields(source_dir = %source_dir.display(), target_dir = %target_dir.display()))]
pub fn get_changed_files_in_dir(
&self,
source_dir: &Path,
@ -339,6 +352,7 @@ impl StateStore {
}
/// Removes all deployment records for files within a directory.
#[tracing::instrument(skip(self))]
pub fn remove_directory_deployment(&mut self, target_dir: &Path) {
let target_prefix = target_dir.display().to_string();
let to_remove: Vec<String> = self

View file

@ -25,6 +25,7 @@ glob = "0.3"
hostname = "0.4"
age = "0.10"
ordered-float = "5"
tracing.workspace = true
[dev-dependencies]
tempfile = "3"

View file

@ -1,13 +1,16 @@
use crate::evaluator::{EvalError, Value};
#[tracing::instrument(level = "trace", skip_all)]
pub fn all(args: &[Value]) -> Result<Value, EvalError> {
Ok(Value::List(args.to_vec()))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn race(args: &[Value]) -> Result<Value, EvalError> {
Ok(args.first().cloned().unwrap_or(Value::None))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn fetch(args: &[Value]) -> Result<Value, EvalError> {
let url = match args.first() {
Some(Value::Str(s)) => s,
@ -32,6 +35,7 @@ pub fn fetch(args: &[Value]) -> Result<Value, EvalError> {
})
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn fetch_json(args: &[Value]) -> Result<Value, EvalError> {
let url = match args.first() {
Some(Value::Str(s)) => s,
@ -56,6 +60,7 @@ pub fn fetch_json(args: &[Value]) -> Result<Value, EvalError> {
})
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn fetch_bytes(args: &[Value]) -> Result<Value, EvalError> {
let url = match args.first() {
Some(Value::Str(s)) => s,
@ -81,6 +86,7 @@ pub fn fetch_bytes(args: &[Value]) -> Result<Value, EvalError> {
})
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn post(args: &[Value]) -> Result<Value, EvalError> {
let url = match args.first() {
Some(Value::Str(s)) => s,
@ -111,6 +117,7 @@ pub fn post(args: &[Value]) -> Result<Value, EvalError> {
})
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn post_json(args: &[Value]) -> Result<Value, EvalError> {
let url = match args.first() {
Some(Value::Str(s)) => s,
@ -140,6 +147,7 @@ pub fn post_json(args: &[Value]) -> Result<Value, EvalError> {
})
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn download(args: &[Value]) -> Result<Value, EvalError> {
let url = match args.first() {
Some(Value::Str(s)) => s,

View file

@ -1,6 +1,7 @@
use crate::ast::Expr;
use crate::evaluator::{EvalError, Evaluator, Value};
#[tracing::instrument(level = "trace", skip_all)]
pub fn map(eval: &mut Evaluator, args: &[Value], _arg_exprs: &[Expr]) -> Result<Value, EvalError> {
let list = match args.first() {
Some(Value::List(items)) => items.clone(),
@ -43,6 +44,7 @@ pub fn map(eval: &mut Evaluator, args: &[Value], _arg_exprs: &[Expr]) -> Result<
}
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn filter(
eval: &mut Evaluator,
args: &[Value],
@ -95,6 +97,7 @@ pub fn filter(
}
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn fold(eval: &mut Evaluator, args: &[Value], _arg_exprs: &[Expr]) -> Result<Value, EvalError> {
let list = match args.first() {
Some(Value::List(items)) => items.clone(),
@ -140,6 +143,7 @@ pub fn fold(eval: &mut Evaluator, args: &[Value], _arg_exprs: &[Expr]) -> Result
}
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn flatten(args: &[Value]) -> Result<Value, EvalError> {
let list = match args.first() {
Some(Value::List(items)) => items,
@ -156,6 +160,7 @@ pub fn flatten(args: &[Value]) -> Result<Value, EvalError> {
Ok(Value::List(result))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn concat(args: &[Value]) -> Result<Value, EvalError> {
let mut result = Vec::new();
for arg in args {
@ -167,6 +172,7 @@ pub fn concat(args: &[Value]) -> Result<Value, EvalError> {
Ok(Value::List(result))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn zip(args: &[Value]) -> Result<Value, EvalError> {
if args.len() < 2 {
return Err(EvalError::TypeError(
@ -194,6 +200,7 @@ pub fn zip(args: &[Value]) -> Result<Value, EvalError> {
Ok(Value::List(result))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn enumerate(args: &[Value]) -> Result<Value, EvalError> {
let list = match args.first() {
Some(Value::List(items)) => items,
@ -209,6 +216,7 @@ pub fn enumerate(args: &[Value]) -> Result<Value, EvalError> {
Ok(Value::List(result))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn first(args: &[Value]) -> Result<Value, EvalError> {
match args.first() {
Some(Value::List(items)) => Ok(items.first().cloned().unwrap_or(Value::None)),
@ -216,6 +224,7 @@ pub fn first(args: &[Value]) -> Result<Value, EvalError> {
}
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn last(args: &[Value]) -> Result<Value, EvalError> {
match args.first() {
Some(Value::List(items)) => Ok(items.last().cloned().unwrap_or(Value::None)),
@ -223,6 +232,7 @@ pub fn last(args: &[Value]) -> Result<Value, EvalError> {
}
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn len(args: &[Value]) -> Result<Value, EvalError> {
match args.first() {
Some(Value::List(items)) => Ok(Value::Int(items.len() as i64)),
@ -233,6 +243,7 @@ pub fn len(args: &[Value]) -> Result<Value, EvalError> {
}
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn contains(args: &[Value]) -> Result<Value, EvalError> {
let list = match args.first() {
Some(Value::List(items)) => items,
@ -243,6 +254,7 @@ pub fn contains(args: &[Value]) -> Result<Value, EvalError> {
Ok(Value::Bool(list.iter().any(|v| values_equal(v, needle))))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn unique(args: &[Value]) -> Result<Value, EvalError> {
let list = match args.first() {
Some(Value::List(items)) => items,
@ -262,6 +274,7 @@ pub fn unique(args: &[Value]) -> Result<Value, EvalError> {
Ok(Value::List(result))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn sort(args: &[Value]) -> Result<Value, EvalError> {
let list = match args.first() {
Some(Value::List(items)) => items.clone(),
@ -285,6 +298,7 @@ pub fn sort(args: &[Value]) -> Result<Value, EvalError> {
Ok(Value::List(sortable.into_iter().map(|(v, _)| v).collect()))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn sort_by(
eval: &mut Evaluator,
args: &[Value],
@ -316,6 +330,7 @@ pub fn sort_by(
}
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn reverse(args: &[Value]) -> Result<Value, EvalError> {
let list = match args.first() {
Some(Value::List(items)) => items.clone(),
@ -327,6 +342,7 @@ pub fn reverse(args: &[Value]) -> Result<Value, EvalError> {
Ok(Value::List(reversed))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn seq(eval: &mut Evaluator, args: &[Value], _arg_exprs: &[Expr]) -> Result<Value, EvalError> {
let list = match args.first() {
Some(Value::List(items)) => items.clone(),
@ -351,6 +367,7 @@ pub fn seq(eval: &mut Evaluator, args: &[Value], _arg_exprs: &[Expr]) -> Result<
}
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn batch(
eval: &mut Evaluator,
args: &[Value],

View file

@ -1,6 +1,7 @@
use crate::evaluator::{EvalError, Value};
use std::path::PathBuf;
#[tracing::instrument(level = "trace", skip_all)]
pub fn hash_file(args: &[Value]) -> Result<Value, EvalError> {
let path = match args.first() {
Some(Value::Path(p)) => p.clone(),
@ -13,6 +14,7 @@ pub fn hash_file(args: &[Value]) -> Result<Value, EvalError> {
Ok(Value::Str(hash.to_hex().to_string()))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn hash_str(args: &[Value]) -> Result<Value, EvalError> {
let s = match args.first() {
Some(Value::Str(s)) => s,
@ -27,6 +29,7 @@ pub fn hash_str(args: &[Value]) -> Result<Value, EvalError> {
Ok(Value::Str(hash.to_hex().to_string()))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn encrypt_age(args: &[Value]) -> Result<Value, EvalError> {
let content = match args.first() {
Some(Value::Str(s)) => s,
@ -69,6 +72,7 @@ pub fn encrypt_age(args: &[Value]) -> Result<Value, EvalError> {
Ok(Value::Str(base64_encode(&encrypted)))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn decrypt_age(args: &[Value]) -> Result<Value, EvalError> {
let encrypted = match args.first() {
Some(Value::Str(s)) => s,

View file

@ -3,12 +3,14 @@ use std::path::PathBuf;
use std::process::Command;
use walkdir::WalkDir;
#[tracing::instrument(level = "trace", skip_all)]
pub fn read_file(args: &[Value]) -> Result<Value, EvalError> {
let path = get_path(args)?;
let content = std::fs::read_to_string(&path)?;
Ok(Value::Str(content))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn read_file_lines(args: &[Value]) -> Result<Value, EvalError> {
let path = get_path(args)?;
let content = std::fs::read_to_string(&path)?;
@ -16,6 +18,7 @@ pub fn read_file_lines(args: &[Value]) -> Result<Value, EvalError> {
Ok(Value::List(lines))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn write_file(args: &[Value]) -> Result<Value, EvalError> {
let path = get_path(args)?;
let content = match args.get(1) {
@ -30,6 +33,7 @@ pub fn write_file(args: &[Value]) -> Result<Value, EvalError> {
Ok(Value::Bool(true))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn copy_file(args: &[Value]) -> Result<Value, EvalError> {
let src = get_path(args)?;
let dst = match args.get(1) {
@ -45,28 +49,33 @@ pub fn copy_file(args: &[Value]) -> Result<Value, EvalError> {
Ok(Value::Bool(true))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn delete_file(args: &[Value]) -> Result<Value, EvalError> {
let path = get_path(args)?;
std::fs::remove_file(&path)?;
Ok(Value::Bool(true))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn file_exists(args: &[Value]) -> Result<Value, EvalError> {
let path = get_path(args)?;
Ok(Value::Bool(path.is_file()))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn dir_exists(args: &[Value]) -> Result<Value, EvalError> {
let path = get_path(args)?;
Ok(Value::Bool(path.is_dir()))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn create_dir_all(args: &[Value]) -> Result<Value, EvalError> {
let path = get_path(args)?;
std::fs::create_dir_all(&path)?;
Ok(Value::Bool(true))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn list_dir(args: &[Value]) -> Result<Value, EvalError> {
let path = get_path(args)?;
let entries: Vec<Value> = std::fs::read_dir(&path)?
@ -76,6 +85,7 @@ pub fn list_dir(args: &[Value]) -> Result<Value, EvalError> {
Ok(Value::List(entries))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn glob_files(args: &[Value]) -> Result<Value, EvalError> {
let pattern = match args.first() {
Some(Value::Str(s)) => s,
@ -95,6 +105,7 @@ pub fn glob_files(args: &[Value]) -> Result<Value, EvalError> {
Ok(Value::List(entries))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn walk_dir(args: &[Value]) -> Result<Value, EvalError> {
let path = get_path(args)?;
let entries: Vec<Value> = WalkDir::new(&path)
@ -105,10 +116,12 @@ pub fn walk_dir(args: &[Value]) -> Result<Value, EvalError> {
Ok(Value::List(entries))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn temp_dir() -> Result<Value, EvalError> {
Ok(Value::Path(std::env::temp_dir()))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn temp_file(args: &[Value]) -> Result<Value, EvalError> {
let prefix = match args.first() {
Some(Value::Str(s)) => s.as_str(),
@ -122,17 +135,20 @@ pub fn temp_file(args: &[Value]) -> Result<Value, EvalError> {
Ok(Value::Path(path))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn is_symlink(args: &[Value]) -> Result<Value, EvalError> {
let path = get_path(args)?;
Ok(Value::Bool(path.is_symlink()))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn read_link(args: &[Value]) -> Result<Value, EvalError> {
let path = get_path(args)?;
let target = std::fs::read_link(&path)?;
Ok(Value::Path(target))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn path_join(args: &[Value]) -> Result<Value, EvalError> {
let mut result = PathBuf::new();
for arg in args {
@ -149,6 +165,7 @@ pub fn path_join(args: &[Value]) -> Result<Value, EvalError> {
Ok(Value::Path(result))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn path_parent(args: &[Value]) -> Result<Value, EvalError> {
let path = get_path(args)?;
Ok(Value::Path(
@ -156,6 +173,7 @@ pub fn path_parent(args: &[Value]) -> Result<Value, EvalError> {
))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn path_filename(args: &[Value]) -> Result<Value, EvalError> {
let path = get_path(args)?;
Ok(Value::Str(
@ -165,6 +183,7 @@ pub fn path_filename(args: &[Value]) -> Result<Value, EvalError> {
))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn path_extension(args: &[Value]) -> Result<Value, EvalError> {
let path = get_path(args)?;
Ok(Value::Str(
@ -174,14 +193,17 @@ pub fn path_extension(args: &[Value]) -> Result<Value, EvalError> {
))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn home() -> Result<Value, EvalError> {
Ok(Value::Path(dirs::home_dir().unwrap_or_default()))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn config_dir() -> Result<Value, EvalError> {
Ok(Value::Path(dirs::config_dir().unwrap_or_default()))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn config_path(args: &[Value]) -> Result<Value, EvalError> {
let app = match args.first() {
Some(Value::Str(s)) => s,
@ -195,14 +217,17 @@ pub fn config_path(args: &[Value]) -> Result<Value, EvalError> {
Ok(Value::Path(config.join(app)))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn data_dir() -> Result<Value, EvalError> {
Ok(Value::Path(dirs::data_dir().unwrap_or_default()))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn cache_dir() -> Result<Value, EvalError> {
Ok(Value::Path(dirs::cache_dir().unwrap_or_default()))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn exec(args: &[Value]) -> Result<Value, EvalError> {
let cmd = match args.first() {
Some(Value::Str(s)) => s,
@ -220,6 +245,7 @@ pub fn exec(args: &[Value]) -> Result<Value, EvalError> {
))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn exec_with_status(args: &[Value]) -> Result<Value, EvalError> {
let cmd = match args.first() {
Some(Value::Str(s)) => s,
@ -235,10 +261,12 @@ pub fn exec_with_status(args: &[Value]) -> Result<Value, EvalError> {
Ok(Value::Int(status.code().unwrap_or(-1) as i64))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn shell(args: &[Value]) -> Result<Value, EvalError> {
exec(args)
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn which(args: &[Value]) -> Result<Value, EvalError> {
let cmd = match args.first() {
Some(Value::Str(s)) => s,
@ -259,12 +287,14 @@ pub fn which(args: &[Value]) -> Result<Value, EvalError> {
}
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn to_json(args: &[Value]) -> Result<Value, EvalError> {
let val = args.first().unwrap_or(&Value::None);
let json = value_to_json(val);
Ok(Value::Str(json.to_string()))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn from_json(args: &[Value]) -> Result<Value, EvalError> {
let s = match args.first() {
Some(Value::Str(s)) => s,
@ -281,6 +311,7 @@ pub fn from_json(args: &[Value]) -> Result<Value, EvalError> {
Ok(json_to_value(&json))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn to_toml(args: &[Value]) -> Result<Value, EvalError> {
let val = args.first().unwrap_or(&Value::None);
let toml_val = value_to_toml(val);
@ -289,6 +320,7 @@ pub fn to_toml(args: &[Value]) -> Result<Value, EvalError> {
Ok(Value::Str(s))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn from_toml(args: &[Value]) -> Result<Value, EvalError> {
let s = match args.first() {
Some(Value::Str(s)) => s,
@ -305,6 +337,7 @@ pub fn from_toml(args: &[Value]) -> Result<Value, EvalError> {
Ok(toml_to_value(&toml_val))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn to_yaml(args: &[Value]) -> Result<Value, EvalError> {
let val = args.first().unwrap_or(&Value::None);
let json = value_to_json(val);
@ -313,6 +346,7 @@ pub fn to_yaml(args: &[Value]) -> Result<Value, EvalError> {
))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn from_yaml(args: &[Value]) -> Result<Value, EvalError> {
from_json(args)
}

View file

@ -10,6 +10,7 @@ use crate::ast::Expr;
use crate::evaluator::{EvalError, Evaluator, Value};
/// Dispatches a built-in function call.
#[tracing::instrument(level = "trace", skip_all, fields(name))]
pub fn call_builtin(
eval: &mut Evaluator,
name: &str,
@ -126,6 +127,7 @@ pub fn call_builtin(
}
/// Dispatches a method call on a value.
#[tracing::instrument(level = "trace", skip_all, fields(method))]
pub fn call_method(
eval: &mut Evaluator,
obj: &Value,
@ -216,9 +218,10 @@ pub fn call_method(
}
"replace" => {
if args.len() >= 2
&& let (Value::Str(from), Value::Str(to)) = (&args[0], &args[1]) {
return Ok(Value::Str(s.replace(from, to)));
}
&& let (Value::Str(from), Value::Str(to)) = (&args[0], &args[1])
{
return Ok(Value::Str(s.replace(from, to)));
}
Ok(Value::Str(s.clone()))
}
"starts_with" => {
@ -286,9 +289,10 @@ pub fn call_method(
}
}
if let Some(field) = fields.get(method)
&& let Value::Function(func, env) = field {
return eval.call_function(func, env, args);
}
&& let Value::Function(func, env) = field
{
return eval.call_function(func, env, args);
}
Err(EvalError::FieldNotFound {
ty: name.clone(),
field: method.to_string(),

View file

@ -1,5 +1,6 @@
use crate::evaluator::{EvalError, Value};
#[tracing::instrument(level = "trace", skip_all)]
pub fn join(args: &[Value]) -> Result<Value, EvalError> {
let list = match args.first() {
Some(Value::List(items)) => items,
@ -20,6 +21,7 @@ pub fn join(args: &[Value]) -> Result<Value, EvalError> {
Ok(Value::Str(result))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn split(args: &[Value]) -> Result<Value, EvalError> {
let s = match args.first() {
Some(Value::Str(s)) => s,
@ -35,6 +37,7 @@ pub fn split(args: &[Value]) -> Result<Value, EvalError> {
Ok(Value::List(parts))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn upper(args: &[Value]) -> Result<Value, EvalError> {
match args.first() {
Some(Value::Str(s)) => Ok(Value::Str(s.to_uppercase())),
@ -42,6 +45,7 @@ pub fn upper(args: &[Value]) -> Result<Value, EvalError> {
}
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn lower(args: &[Value]) -> Result<Value, EvalError> {
match args.first() {
Some(Value::Str(s)) => Ok(Value::Str(s.to_lowercase())),
@ -49,6 +53,7 @@ pub fn lower(args: &[Value]) -> Result<Value, EvalError> {
}
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn trim(args: &[Value]) -> Result<Value, EvalError> {
match args.first() {
Some(Value::Str(s)) => Ok(Value::Str(s.trim().to_string())),
@ -56,6 +61,7 @@ pub fn trim(args: &[Value]) -> Result<Value, EvalError> {
}
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn replace(args: &[Value]) -> Result<Value, EvalError> {
let s = match args.first() {
Some(Value::Str(s)) => s,
@ -83,6 +89,7 @@ pub fn replace(args: &[Value]) -> Result<Value, EvalError> {
Ok(Value::Str(s.replace(from.as_str(), to.as_str())))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn starts_with(args: &[Value]) -> Result<Value, EvalError> {
let s = match args.first() {
Some(Value::Str(s)) => s,
@ -105,6 +112,7 @@ pub fn starts_with(args: &[Value]) -> Result<Value, EvalError> {
Ok(Value::Bool(s.starts_with(prefix.as_str())))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn ends_with(args: &[Value]) -> Result<Value, EvalError> {
let s = match args.first() {
Some(Value::Str(s)) => s,
@ -127,6 +135,7 @@ pub fn ends_with(args: &[Value]) -> Result<Value, EvalError> {
Ok(Value::Bool(s.ends_with(suffix.as_str())))
}
#[tracing::instrument(level = "trace", skip_all)]
pub fn format(args: &[Value]) -> Result<Value, EvalError> {
let template = match args.first() {
Some(Value::Str(s)) => s.clone(),

View file

@ -335,6 +335,7 @@ pub struct Evaluator {
impl Evaluator {
/// Creates a new evaluator with built-in bindings.
#[tracing::instrument(level = "trace")]
pub fn new() -> Self {
let mut env = Env::new();
Self::init_builtins(&mut env);
@ -344,6 +345,7 @@ impl Evaluator {
}
}
#[tracing::instrument(level = "trace", skip_all)]
fn init_builtins(env: &mut Env) {
// Register the Os enum so Os::Linux, Os::MacOS, etc. can be used
env.define_enum(
@ -387,6 +389,7 @@ impl Evaluator {
}
/// Evaluates the program and returns collected configuration.
#[tracing::instrument(level = "trace", skip_all)]
pub fn eval(&mut self, program: &Program) -> Result<EvalResult, EvalError> {
for stmt in &program.statements {
self.eval_statement(&stmt.node)?;
@ -395,6 +398,7 @@ impl Evaluator {
}
/// Returns all variables as environment variables for hooks.
#[tracing::instrument(level = "trace", skip(self))]
pub fn get_hook_env(&self) -> std::collections::HashMap<String, String> {
let mut vars = self.env.get_all_variables();
@ -417,22 +421,26 @@ impl Evaluator {
vars
}
#[tracing::instrument(level = "trace", skip_all)]
fn eval_statement(&mut self, stmt: &Statement) -> Result<Option<Value>, EvalError> {
match stmt {
Statement::VarDecl(decl) => {
tracing::trace!(name = %decl.name, "eval var declaration");
let value = self.eval_expr(&decl.value)?;
// Handle special config variables
if decl.name == "sandbox"
&& let Value::Bool(b) = &value {
self.result.sandbox = *b;
}
&& let Value::Bool(b) = &value
{
self.result.sandbox = *b;
}
self.env.define(decl.name.clone(), value);
Ok(None)
}
Statement::FnDecl(decl) => {
tracing::trace!(name = %decl.name, "eval fn declaration");
self.env
.define_function(decl.name.clone(), decl.clone(), self.env.clone());
Ok(None)
@ -526,6 +534,7 @@ impl Evaluator {
}
Statement::Dotfile(dotfile) => {
tracing::trace!("eval dotfile");
if let Some(ref when) = dotfile.when {
let cond = self.eval_expr(when)?;
if !cond.is_truthy() {
@ -569,6 +578,7 @@ impl Evaluator {
}
Statement::Package(pkg) => {
tracing::trace!("eval package");
if let Some(ref when) = pkg.when {
let cond = self.eval_expr(when)?;
if !cond.is_truthy() {
@ -626,6 +636,7 @@ impl Evaluator {
}
Statement::Hook(hook) => {
tracing::trace!("eval hook");
if let Some(ref when) = hook.when {
let cond = self.eval_expr(when)?;
if !cond.is_truthy() {
@ -659,6 +670,7 @@ impl Evaluator {
}
}
#[tracing::instrument(level = "trace", skip_all)]
fn eval_expr(&mut self, expr: &Expr) -> Result<Value, EvalError> {
match expr {
Expr::Literal(lit) => Ok(match lit {
@ -884,6 +896,7 @@ impl Evaluator {
}
}
#[tracing::instrument(level = "trace", skip_all)]
fn eval_binary_op(&self, left: &Value, op: &BinOp, right: &Value) -> Result<Value, EvalError> {
match op {
BinOp::Add => match (left, right) {
@ -1048,6 +1061,7 @@ impl Evaluator {
}
}
#[tracing::instrument(level = "trace", skip_all)]
fn pattern_matches(&self, pattern: &Pattern, value: &Value) -> bool {
match pattern {
Pattern::Wildcard => true,
@ -1067,6 +1081,7 @@ impl Evaluator {
}
}
#[tracing::instrument(level = "trace", skip_all, fields(name = %func.name))]
pub fn call_function(
&mut self,
func: &FnDecl,
@ -1094,6 +1109,7 @@ impl Evaluator {
Ok(result)
}
#[tracing::instrument(level = "trace", skip_all)]
fn call_lambda(
&mut self,
params: &[FnParam],
@ -1115,6 +1131,7 @@ impl Evaluator {
Ok(result)
}
#[tracing::instrument(level = "trace", skip_all, fields(name))]
fn call_builtin(
&mut self,
name: &str,
@ -1124,6 +1141,7 @@ impl Evaluator {
builtins::call_builtin(self, name, args, arg_exprs)
}
#[tracing::instrument(level = "trace", skip_all, fields(method))]
fn call_method(
&mut self,
obj: &Value,
@ -1134,6 +1152,7 @@ impl Evaluator {
builtins::call_method(self, obj, method, args, arg_exprs)
}
#[tracing::instrument(level = "trace", skip_all)]
fn eval_to_path(&mut self, expr: &Expr) -> Result<PathBuf, EvalError> {
let val = self.eval_expr(expr)?;
match val {
@ -1154,12 +1173,14 @@ impl Evaluator {
}
/// Returns DOOT_HOME if set, otherwise the real home directory.
#[tracing::instrument(level = "trace")]
fn home_dir() -> PathBuf {
std::env::var("DOOT_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| dirs::home_dir().unwrap_or_default())
}
#[tracing::instrument(level = "trace", skip_all)]
fn eval_to_string(&mut self, expr: &Expr) -> Result<String, EvalError> {
let val = self.eval_expr(expr)?;
Ok(val.to_string_repr())
@ -1181,6 +1202,7 @@ impl Default for Evaluator {
}
impl Evaluator {
#[tracing::instrument(level = "trace", skip_all)]
pub fn eval_in_env(&mut self, expr: &Expr, env: Env) -> Result<Value, EvalError> {
let old_env = std::mem::replace(&mut self.env, env);
let result = self.eval_expr(expr);
@ -1188,6 +1210,7 @@ impl Evaluator {
result
}
#[tracing::instrument(level = "trace", skip_all, fields(name = %func.name))]
pub fn call_fn(
&mut self,
func: &FnDecl,

View file

@ -333,12 +333,14 @@ impl Lexer {
}
/// Tokenizes the input string with indentation processing.
#[tracing::instrument(skip_all)]
pub fn lex(input: &str) -> Result<Vec<Spanned<Token>>, Vec<Simple<char>>> {
let tokens = Self::lexer().parse(input)?;
Ok(Self::process_indentation(tokens))
}
/// Converts whitespace into indent/dedent tokens.
#[tracing::instrument(level = "trace", skip_all)]
fn process_indentation(tokens: Vec<Spanned<Token>>) -> Vec<Spanned<Token>> {
let mut result = Vec::new();
let mut indent_stack = vec![0usize];

View file

@ -10,6 +10,7 @@ pub struct MacroExpander {
impl MacroExpander {
/// Creates a new macro expander.
#[tracing::instrument(level = "trace")]
pub fn new() -> Self {
Self {
macros: HashMap::new(),
@ -17,11 +18,13 @@ impl MacroExpander {
}
/// Registers a macro definition.
#[tracing::instrument(level = "trace", skip(self), fields(name = %decl.name))]
pub fn register(&mut self, decl: MacroDecl) {
self.macros.insert(decl.name.clone(), decl);
}
/// Expands a macro call into statements.
#[tracing::instrument(level = "trace", skip(self), fields(name = %call.name))]
pub fn expand(&self, call: &MacroCall) -> Option<Vec<Spanned<Statement>>> {
let decl = self.macros.get(&call.name)?;
@ -44,6 +47,7 @@ impl MacroExpander {
Some(expanded)
}
#[tracing::instrument(level = "trace", skip_all)]
fn substitute_statement(&self, stmt: &Statement, subs: &HashMap<String, &Expr>) -> Statement {
match stmt {
Statement::VarDecl(decl) => Statement::VarDecl(VarDecl {
@ -121,6 +125,7 @@ impl MacroExpander {
}
}
#[tracing::instrument(level = "trace", skip_all)]
fn substitute_expr(&self, expr: &Expr, subs: &HashMap<String, &Expr>) -> Expr {
match expr {
Expr::Ident(name) => {

View file

@ -13,6 +13,7 @@ type ParserInput = crate::lexer::Spanned<Token>;
impl Parser {
/// Parses a token stream into a program AST.
#[tracing::instrument(skip_all)]
pub fn parse(tokens: Vec<ParserInput>) -> Result<Program, Vec<Simple<Token>>> {
let stream = tokens
.into_iter()
@ -888,9 +889,10 @@ impl Parser {
}
if parts.len() == 1
&& let InterpolatedPart::Literal(s) = &parts[0] {
return Expr::Literal(Literal::Str(s.clone()));
}
&& let InterpolatedPart::Literal(s) = &parts[0]
{
return Expr::Literal(Literal::Str(s.clone()));
}
Expr::Interpolated(parts)
}

View file

@ -47,6 +47,7 @@ pub struct Executor<H: TaskHandler> {
impl<H: TaskHandler + 'static> Executor<H> {
/// Creates a new executor.
#[tracing::instrument(level = "trace", skip_all)]
pub fn new(graph: DependencyGraph, handler: H) -> Self {
Self {
graph,
@ -62,6 +63,7 @@ impl<H: TaskHandler + 'static> Executor<H> {
}
/// Executes tasks sequentially.
#[tracing::instrument(skip(self))]
pub fn execute_sequential(&self) -> Result<ExecutionReport, ExecutionError> {
let order = self
.graph
@ -85,6 +87,7 @@ impl<H: TaskHandler + 'static> Executor<H> {
}
/// Executes tasks in parallel batches.
#[tracing::instrument(skip(self))]
pub fn execute_parallel(&self) -> Result<ExecutionReport, ExecutionError> {
let batches =
self.graph
@ -125,6 +128,7 @@ impl<H: TaskHandler + 'static> Executor<H> {
Ok(Arc::try_unwrap(report).unwrap().into_inner().unwrap())
}
#[tracing::instrument(level = "trace", skip(self))]
fn execute_node(&self, node: &Node) -> TaskResult {
if self.dry_run {
return Ok(());

View file

@ -11,6 +11,7 @@ pub struct Scheduler {
impl Scheduler {
/// Creates an empty scheduler.
#[tracing::instrument(level = "trace")]
pub fn new() -> Self {
Self {
graph: DependencyGraph::new(),
@ -18,6 +19,7 @@ impl Scheduler {
}
/// Creates a scheduler from evaluation results.
#[tracing::instrument(skip_all)]
pub fn from_eval_result(result: &EvalResult) -> Self {
let mut scheduler = Self::new();
@ -79,11 +81,13 @@ impl Scheduler {
}
/// Returns task IDs in execution order.
#[tracing::instrument(level = "trace", skip(self))]
pub fn get_execution_order(&self) -> Result<Vec<String>, String> {
self.graph.topological_sort()
}
/// Returns tasks grouped into parallel batches.
#[tracing::instrument(level = "trace", skip(self))]
pub fn get_parallel_batches(&self) -> Result<Vec<Vec<String>>, String> {
self.graph.get_parallel_batches()
}
@ -134,6 +138,7 @@ pub struct DotfileValidation {
/// - Overlapping directories (both dirs, one target is ancestor) with same settings → Warning
/// - Overlapping directories with different settings → OK, add dependency
/// - Directory + file inside → OK, add dependency
#[tracing::instrument(skip_all)]
pub fn validate_dotfile_targets(
dotfiles: &[DotfileConfig],
source_dir: &Path,

View file

@ -93,6 +93,7 @@ pub struct TypeChecker {
impl TypeChecker {
/// Creates a new type checker with built-in types.
#[tracing::instrument(level = "trace")]
pub fn new() -> Self {
Self {
env: TypeEnv::new(),
@ -101,6 +102,7 @@ impl TypeChecker {
}
/// Type checks a program, returning errors if any.
#[tracing::instrument(level = "trace", skip_all)]
pub fn check(&mut self, program: &Program) -> Result<(), Vec<TypeError>> {
for stmt in &program.statements {
self.check_statement(stmt);
@ -113,6 +115,7 @@ impl TypeChecker {
}
}
#[tracing::instrument(level = "trace", skip_all)]
fn check_statement(&mut self, stmt: &Spanned<Statement>) {
match &stmt.node {
Statement::VarDecl(decl) => {
@ -307,6 +310,7 @@ impl TypeChecker {
}
}
#[tracing::instrument(level = "trace", skip_all)]
fn infer_expr(&mut self, expr: &Expr, span: &std::ops::Range<usize>) -> Type {
match expr {
Expr::Literal(lit) => match lit {
@ -630,10 +634,7 @@ impl TypeChecker {
Type::Function(param_types, Box::new(return_ty))
}
Expr::Await(expr) => {
self.infer_expr(expr, span)
}
Expr::Await(expr) => self.infer_expr(expr, span),
Expr::Path(left, right) => {
self.infer_expr(left, span);
@ -654,6 +655,7 @@ impl TypeChecker {
}
}
#[tracing::instrument(level = "trace", skip_all, fields(name))]
fn infer_builtin_call(
&mut self,
name: &str,
@ -761,6 +763,7 @@ impl TypeChecker {
}
}
#[tracing::instrument(level = "trace", skip_all, fields(method))]
fn infer_list_method(
&mut self,
method: &str,
@ -778,6 +781,7 @@ impl TypeChecker {
}
}
#[tracing::instrument(level = "trace", skip_all, fields(method))]
fn infer_str_method(
&mut self,
method: &str,
@ -793,6 +797,7 @@ impl TypeChecker {
}
}
#[tracing::instrument(level = "trace", skip_all)]
fn resolve_type(&self, ty: &TypeAnnotation) -> Type {
match ty {
TypeAnnotation::Simple(name) => match name.as_str() {