Skip to content

Commit

Permalink
feat(tasks): optional automatic outputs
Browse files Browse the repository at this point in the history
Fixes #2621
  • Loading branch information
jdx committed Dec 13, 2024
1 parent 09640a4 commit 058944f
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 20 deletions.
14 changes: 10 additions & 4 deletions src/cli/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -586,14 +586,15 @@ impl Run {
}

fn sources_are_fresh(&self, task: &Task) -> Result<bool> {
if task.sources.is_empty() && task.outputs.is_empty() {
let outputs = task.outputs.paths(task);
if task.sources.is_empty() && outputs.is_empty() {
return Ok(false);
}
let run = || -> Result<bool> {
let mut sources = task.sources.clone();
sources.push(task.config_source.to_string_lossy().to_string());
let sources = self.get_last_modified(&self.cwd(task)?, &sources)?;
let outputs = self.get_last_modified(&self.cwd(task)?, &task.outputs)?;
let outputs = self.get_last_modified(&self.cwd(task)?, &outputs)?;
trace!("sources: {sources:?}, outputs: {outputs:?}");
match (sources, outputs) {
(Some(sources), Some(outputs)) => Ok(sources < outputs),
Expand Down Expand Up @@ -658,7 +659,12 @@ impl Run {
if task.sources.is_empty() {
return Ok(());
}
// TODO
if task.outputs.is_auto() {
for p in task.outputs.paths(task) {
debug!("touching auto output file: {p}");
file::touch_file(&PathBuf::from(&p))?;
}
}
Ok(())
}

Expand Down Expand Up @@ -913,4 +919,4 @@ pub fn get_task_lists(args: &[String], prompt: bool) -> Result<Vec<Task>> {
})
.flatten_ok()
.collect()
}
}
5 changes: 3 additions & 2 deletions src/cli/tasks/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ impl TasksInfo {
if !task.sources.is_empty() {
info::inline_section("Sources", task.sources.join(", "))?;
}
if !task.outputs.is_empty() {
info::inline_section("Outputs", task.outputs.join(", "))?;
let outputs = task.outputs.paths(task);
if !outputs.is_empty() {
info::inline_section("Outputs", outputs.join(", "))?;
}
if let Some(file) = &task.file {
info::inline_section("File", display_path(file))?;
Expand Down
43 changes: 30 additions & 13 deletions src/task/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,16 @@ use xx::regex;
mod deps;
mod task_dep;
mod task_script_parser;
pub mod task_sources;

use crate::config::config_file::ConfigFile;
use crate::file::display_path;
use crate::ui::style;
pub use deps::Deps;
use task_dep::TaskDep;
use task_sources::TaskOutputs;

#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
#[derive(Debug, Clone, Deserialize)]
pub struct Task {
#[serde(skip)]
pub name: String,
Expand Down Expand Up @@ -65,7 +67,7 @@ pub struct Task {
#[serde(default)]
pub sources: Vec<String>,
#[serde(default)]
pub outputs: Vec<String>,
pub outputs: TaskOutputs,
#[serde(default)]
pub shell: Option<String>,
#[serde(default)]
Expand All @@ -91,6 +93,10 @@ pub struct Task {
// file type
#[serde(default)]
pub file: Option<PathBuf>,
#[serde(skip)]
pub tera: tera::Tera,
#[serde(skip)]
pub tera_ctx: tera::Context,
}

#[derive(Clone, PartialEq, Eq, Deserialize, Serialize)]
Expand Down Expand Up @@ -156,9 +162,10 @@ impl Task {
map
});
let info = toml::Value::Table(info);
let mut tera_ctx = BASE_CONTEXT.clone();
tera_ctx.insert("config_root", &config_root);
let p = TomlParser::new(&info, get_tera(Some(config_root)), tera_ctx);
task.tera_ctx = BASE_CONTEXT.clone();
task.tera_ctx.insert("config_root", &config_root);
task.tera = get_tera(Some(config_root));
let p = TomlParser::new(&info, task.tera.clone(), task.tera_ctx.clone());
// trace!("task info: {:#?}", info);

task.hide = !file::is_executable(path) || p.parse_bool("hide").unwrap_or_default();
Expand All @@ -170,7 +177,8 @@ impl Task {
.unwrap_or_default();
task.description = p.parse_str("description")?.unwrap_or_default();
task.sources = p.parse_array("sources")?.unwrap_or_default();
task.outputs = p.parse_array("outputs")?.unwrap_or_default();
task.outputs = info.get("outputs").map(|to| to.into()).unwrap_or_default();
task.outputs.render(&mut task.tera, &task.tera_ctx)?;
task.depends = p.parse_array("depends")?.unwrap_or_default();
task.depends_post = p.parse_array("depends_post")?.unwrap_or_default();
task.wait_for = p.parse_array("wait_for")?.unwrap_or_default();
Expand Down Expand Up @@ -337,14 +345,14 @@ impl Task {
style::ereset() + &style::estyle(self.prefix()).fg(COLORS[idx]).to_string()
}

pub fn tera_render(&self, s: &str) -> Result<String> {
let mut tera = get_tera(self.config_root.as_deref());
Ok(tera.render_str(s, &self.tera_ctx)?)
}

pub fn dir(&self) -> Result<Option<PathBuf>> {
let render = |dir| {
let mut tera = get_tera(self.config_root.as_deref());
let mut ctx = BASE_CONTEXT.clone();
if let Some(config_root) = &self.config_root {
ctx.insert("config_root", config_root);
}
let dir = tera.render_str(dir, &ctx)?;
let dir = self.tera_render(dir)?;
let dir = file::replace_path(&dir);
if dir.is_absolute() {
Ok(Some(dir.to_path_buf()))
Expand Down Expand Up @@ -441,14 +449,16 @@ impl Default for Task {
hide: false,
raw: false,
sources: vec![],
outputs: vec![],
outputs: Default::default(),
shell: None,
silent: false,
run: vec![],
run_windows: vec![],
args: vec![],
file: None,
quiet: false,
tera: get_tera(None),
tera_ctx: BASE_CONTEXT.clone(),
}
}
}
Expand Down Expand Up @@ -492,6 +502,13 @@ impl Hash for Task {
}
}

impl Eq for Task {}
impl PartialEq for Task {
fn eq(&self, other: &Self) -> bool {
self.name == other.name && self.args == other.args
}
}

impl TreeItem for (&Graph<Task, ()>, NodeIndex) {
type Child = Self;

Expand Down
191 changes: 191 additions & 0 deletions src/task/task_sources.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
use crate::dirs;
use crate::task::Task;
use serde::ser::{SerializeMap, SerializeSeq};
use serde::{Deserialize, Deserializer, Serialize};
use std::hash::{DefaultHasher, Hash, Hasher};

#[derive(Debug, Clone, Eq, PartialEq, strum::EnumIs)]
pub enum TaskOutputs {
Files(Vec<String>),
Auto,
}

impl Default for TaskOutputs {
fn default() -> Self {
TaskOutputs::Files(vec![])
}
}

impl TaskOutputs {
pub fn paths(&self, task: &Task) -> Vec<String> {
match self {
TaskOutputs::Files(files) => files.clone(),
TaskOutputs::Auto => vec![self.auto_path(task)],
}
}

fn auto_path(&self, task: &Task) -> String {
let mut hasher = DefaultHasher::new();
task.hash(&mut hasher);
task.config_source.hash(&mut hasher);
let hash = format!("{:x}", hasher.finish());
dirs::STATE
.join("task-auto-outputs")
.join(&hash)
.to_string_lossy()
.to_string()
}

pub fn render(&mut self, tera: &mut tera::Tera, ctx: &tera::Context) -> eyre::Result<()> {
match self {
TaskOutputs::Files(files) => {
for file in files.iter_mut() {
*file = tera.render_str(file, ctx)?;
}
}
TaskOutputs::Auto => {}
}
Ok(())
}
}

impl From<&toml::Value> for TaskOutputs {
fn from(value: &toml::Value) -> Self {
match value {
toml::Value::String(file) => TaskOutputs::Files(vec![file.to_string()]),
toml::Value::Array(files) => TaskOutputs::Files(
files
.iter()
.map(|v| v.as_str().unwrap().to_string())
.collect(),
),
toml::Value::Table(table) => {
let auto = table
.get("auto")
.and_then(|v| v.as_bool())
.unwrap_or_default();
if auto {
TaskOutputs::Auto
} else {
TaskOutputs::default()
}
}
_ => TaskOutputs::default(),
}
}
}

impl<'de> Deserialize<'de> for TaskOutputs {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct TaskOutputsVisitor;

impl<'de> serde::de::Visitor<'de> for TaskOutputsVisitor {
type Value = TaskOutputs;

fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a string, a sequence of strings, or a map")
}

fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
Ok(TaskOutputs::Files(vec![value.to_string()]))
}

fn visit_seq<A: serde::de::SeqAccess<'de>>(
self,
mut seq: A,
) -> Result<Self::Value, A::Error> {
let mut files = vec![];
while let Some(file) = seq.next_element()? {
files.push(file);
}
Ok(TaskOutputs::Files(files))
}

fn visit_map<A: serde::de::MapAccess<'de>>(
self,
mut map: A,
) -> Result<Self::Value, A::Error> {
if let Some(key) = map.next_key::<String>()? {
if key == "auto" {
if map.next_value::<bool>()? {
Ok(TaskOutputs::Auto)
} else {
Ok(TaskOutputs::default())
}
} else {
Err(serde::de::Error::custom("Invalid TaskOutputs map"))
}
} else {
Ok(TaskOutputs::default())
}
}
}

deserializer.deserialize_any(TaskOutputsVisitor)
}
}

impl Serialize for TaskOutputs {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
TaskOutputs::Files(files) => {
let mut seq = serializer.serialize_seq(Some(files.len()))?;
for file in files {
seq.serialize_element(file)?;
}
seq.end()
}
TaskOutputs::Auto => {
let mut m = serializer.serialize_map(Some(1))?;
m.serialize_entry("auto", &true)?;
m.end()
}
}
}
}

mod tests {
#[allow(unused_imports)]
use super::*;

#[test]
fn test_task_outputs_from_toml() {
let value: toml::Table = toml::from_str("outputs = \"file1\"").unwrap();
let value = value.get("outputs").unwrap();
let outputs = TaskOutputs::from(value);
assert_eq!(outputs, TaskOutputs::Files(vec!["file1".to_string()]));

let value: toml::Table = toml::from_str("outputs = [\"file1\"]").unwrap();
let value = value.get("outputs").unwrap();
let outputs = TaskOutputs::from(value);
assert_eq!(outputs, TaskOutputs::Files(vec!["file1".to_string()]));

let value: toml::Table = toml::from_str("outputs = { auto = true }").unwrap();
let value = value.get("outputs").unwrap();
let outputs = TaskOutputs::from(value);
assert_eq!(outputs, TaskOutputs::Auto);
}

#[test]
fn test_task_outputs_serialize() {
let outputs = TaskOutputs::Files(vec!["file1".to_string()]);
let serialized = serde_json::to_string(&outputs).unwrap();
assert_eq!(serialized, "[\"file1\"]");

let outputs = TaskOutputs::Auto;
let serialized = serde_json::to_string(&outputs).unwrap();
assert_eq!(serialized, "{\"auto\":true}");
}

#[test]
fn test_task_outputs_deserialize() {
let deserialized: TaskOutputs = serde_json::from_str("\"file1\"").unwrap();
assert_eq!(deserialized, TaskOutputs::Files(vec!["file1".to_string()]));

let deserialized: TaskOutputs = serde_json::from_str("[\"file1\"]").unwrap();
assert_eq!(deserialized, TaskOutputs::Files(vec!["file1".to_string()]));

let deserialized: TaskOutputs = serde_json::from_str("{ \"auto\": true }").unwrap();
assert_eq!(deserialized, TaskOutputs::Auto);
}
}
4 changes: 3 additions & 1 deletion tasks.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#:schema ./schema/mise-task.json

clean = "cargo clean"
release = "cargo release"
signal-test = "node ./test/fixtures/signal-test.js"
Expand Down Expand Up @@ -117,4 +119,4 @@ description = "run e2e tests inside of development docker container"
run = "mise tasks run docker:mise run test:e2e"

["test:shuffle"]
run = "cargo +nightly test --all-features -- -Z unstable-options --shuffle"
run = "echo cargo +nightly test --all-features -- -Z unstable-options --shuffle"

0 comments on commit 058944f

Please sign in to comment.