Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cached recipes #1906

Draft
wants to merge 36 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
a649e1d
Add ExpressionWalker and refactor Variables
nmay231 Feb 13, 2024
47976a7
Cache recipe by evaluated body, initial
nmay231 Feb 15, 2024
7d24508
Tests
nmay231 Feb 16, 2024
626dd08
Include working directory/project dir in serialized cache
nmay231 Feb 16, 2024
ed9f0aa
Rename RecipeCache.{hash => body_hash}
nmay231 Feb 16, 2024
a1f740c
Shorten cache filename
nmay231 Feb 16, 2024
d885519
Rename JustfileCache.{recipe_caches => recipes}
nmay231 Feb 16, 2024
5a959d1
Use working directory AND justfile path for cache filename
nmay231 Feb 16, 2024
d82e9e1
Remove InvalidCachedRecipe error
nmay231 Mar 7, 2024
c18a0cc
Print 'skipping cached recipe' message by default
nmay231 Mar 7, 2024
6307eb0
Remove `CacheFilename` for now
nmay231 Mar 7, 2024
ed8c05d
Use .as_encoded_bytes() on os strings
nmay231 Mar 7, 2024
bbf7f84
Fix tests
nmay231 Mar 7, 2024
cf3c95a
Make cached_recipes require unstable
nmay231 Mar 7, 2024
36482fd
Put cache files in working directory by default
nmay231 Mar 7, 2024
d5edf65
Added versioning to cache files
nmay231 Mar 7, 2024
f32cc86
Make errors look the same
nmay231 Mar 7, 2024
219c646
Revert "Add ExpressionWalker and refactor Variables"
nmay231 Mar 7, 2024
3a341ea
Clarify error messages
nmay231 Mar 7, 2024
9acd827
Incompatible old versions of cache should also silently be ignored
nmay231 Mar 7, 2024
29d3459
Merge branch 'master' into cached-recipes
nmay231 Mar 12, 2024
0812866
Doc comments
nmay231 Mar 14, 2024
707844c
Move cache_file into Search struct
nmay231 Mar 14, 2024
70fe6ac
Starting to implement cached dependencies
nmay231 Mar 21, 2024
e014285
Return instead of mutate
nmay231 Mar 21, 2024
e9686df
Cached dependencies mostly work
nmay231 Mar 24, 2024
e859bd6
Only impl Recipe::run() for recipes with resolved dependencies
nmay231 Mar 26, 2024
fbc87dd
Finalize cached recipes with deps
nmay231 Mar 26, 2024
8e8e046
Merge branch 'master' into cached-recipes
nmay231 Mar 26, 2024
4961730
Rename src/{cache => justfile_cache}.rs
nmay231 Mar 26, 2024
a17b8dd
Simplify JustfileCache
nmay231 Mar 26, 2024
d12dbc3
Docs
nmay231 Mar 26, 2024
1ca5122
note to add to gitignore
nmay231 Mar 26, 2024
db89a9a
Just make the cache mutable directly, don't use refcell
nmay231 Apr 15, 2024
aa8c2db
Run just check
nmay231 Apr 15, 2024
e666bf5
Merge branch 'master' into cached-recipes
nmay231 Apr 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1503,6 +1503,7 @@ Recipes may be annotated with attributes that change their behavior.
| Name | Description |
|------|-------------|
| `[confirm]`<sup>1.17.0</sup> | Require confirmation prior to executing recipe. |
| `[cached]`<sup>Latest</sup> | See [Cached Recipes](#cached-recipes) |
| `[confirm("prompt")]`<sup>1.23.0</sup> | Require confirmation prior to executing recipe with a custom prompt. |
| `[linux]`<sup>1.8.0</sup> | Enable recipe on Linux. |
| `[macos]`<sup>1.8.0</sup> | Enable recipe on MacOS. |
Expand Down Expand Up @@ -2487,6 +2488,26 @@ polyglot: python js perl sh ruby

Run `just --help` to see all the options.

### Cached Recipes

Cached recipes only run when the recipe body changes, where the body is compared
*after `{{interpolations}}` are evaluated*. This gives you fine control for when
a recipe should rerun. It is recommended you add `.justcache/` to your
`.gitignore`. **Note: This is currently an unstable feature and requires
`--unstable`**.

```just
[cached]
build:
@# This only runs when the hash of the file changes {{sha256_file("input.c")}}
gcc input.c -o output

[cached]
bad-example:
@# This will never rerun since the body never changes
gcc input.c -o output
```

### Private Recipes

Recipes and aliases whose name starts with a `_` are omitted from `just --list`:
Expand Down
1 change: 1 addition & 0 deletions src/attribute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use super::*;
#[serde(rename_all = "kebab-case")]
pub(crate) enum Attribute<'src> {
Confirm(Option<StringLiteral<'src>>),
Cached,
Linux,
Macos,
NoCd,
Expand Down
6 changes: 6 additions & 0 deletions src/compile_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ impl Display for CompileError<'_> {
recipe_line.ordinal(),
),
BacktickShebang => write!(f, "Backticks may not start with `#!`"),
CachedDependsOnUncached { cached, uncached } => {
write!(
f,
"Cached recipes cannot depend on preceding uncached ones, yet `{cached}` depends on `{uncached}`",
)
}
CircularRecipeDependency { recipe, ref circle } => {
if circle.len() == 2 {
write!(f, "Recipe `{recipe}` depends on itself")
Expand Down
4 changes: 4 additions & 0 deletions src/compile_error_kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ pub(crate) enum CompileErrorKind<'src> {
recipe_line: usize,
},
BacktickShebang,
CachedDependsOnUncached {
cached: &'src str,
uncached: &'src str,
},
CircularRecipeDependency {
recipe: &'src str,
circle: Vec<&'src str>,
Expand Down
10 changes: 10 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ pub(crate) enum Error<'src> {
token: Token<'src>,
output_error: OutputError,
},
CacheFileRead {
cache_filename: PathBuf,
io_error: io::Error,
},
CacheFileWrite {
cache_filename: PathBuf,
io_error: io::Error,
},
ChooserInvoke {
shell_binary: String,
shell_arguments: String,
Expand Down Expand Up @@ -267,6 +275,8 @@ impl<'src> ColorDisplay for Error<'src> {
}?,
OutputError::Utf8(utf8_error) => write!(f, "Backtick succeeded but stdout was not utf8: {utf8_error}")?,
}
CacheFileRead {cache_filename, io_error} => write!(f, "Failed to read cache file ({}): {}", cache_filename.display(), io_error)?,
CacheFileWrite{cache_filename, io_error} => write!(f, "Failed to write cache file ({}): {}", cache_filename.display(), io_error)?,
ChooserInvoke { shell_binary, shell_arguments, chooser, io_error} => {
let chooser = chooser.to_string_lossy();
write!(f, "Chooser `{shell_binary} {shell_arguments} {chooser}` invocation failed: {io_error}")?;
Expand Down
24 changes: 17 additions & 7 deletions src/justfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,12 +272,15 @@ impl<'src> Justfile<'src> {
}

let mut ran = Ran::default();
let mut cache = JustfileCache::new(search)?;

for invocation in invocations {
let context = RecipeContext {
let mut context = RecipeContext {
settings: invocation.settings,
config,
scope: invocation.scope,
search,
cache: &mut cache,
};

Self::run_recipe(
Expand All @@ -287,15 +290,15 @@ impl<'src> Justfile<'src> {
.copied()
.map(str::to_string)
.collect::<Vec<String>>(),
&context,
&mut context,
&dotenv,
&mut ran,
invocation.recipe,
search,
)?;
}

Ok(())
return cache.save(search);
}

pub(crate) fn get_alias(&self, name: &str) -> Option<&Alias<'src>> {
Expand Down Expand Up @@ -403,12 +406,12 @@ impl<'src> Justfile<'src> {

fn run_recipe(
arguments: &[String],
context: &RecipeContext<'src, '_>,
context: &mut RecipeContext<'src, '_>,
dotenv: &BTreeMap<String, String>,
ran: &mut Ran<'src>,
recipe: &Recipe<'src>,
search: &Search,
) -> RunResult<'src> {
) -> RunResult<'src, ()> {
if ran.has_run(&recipe.namepath, arguments) {
return Ok(());
}
Expand Down Expand Up @@ -445,9 +448,16 @@ impl<'src> Justfile<'src> {
}
}

recipe.run(context, dotenv, scope.child(), search, &positional)?;
let updated_hash = recipe.run(context, dotenv, scope.child(), search, &positional)?;
let recipe_hash_changed = updated_hash.is_some();

if !context.config.no_dependencies {
if let Some(body_hash) = updated_hash {
context
.cache
.insert_recipe(recipe.name.to_string(), body_hash);
}

if !context.config.no_dependencies && (!recipe.should_cache() || recipe_hash_changed) {
let mut ran = Ran::default();

for Dependency { recipe, arguments } in recipe.dependencies.iter().skip(recipe.priors) {
Expand Down
87 changes: 87 additions & 0 deletions src/justfile_cache.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;

use super::*;

#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct JustfileCache {
/// Only serialized for user debugging
pub(crate) justfile_path: PathBuf,
/// Only serialized for user debugging
pub(crate) working_directory: PathBuf,

pub(crate) recipes: HashMap<String, RecipeCache>,
}

#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct RecipeCache {
/// The hash of the recipe body after evaluation
pub(crate) body_hash: String,
#[serde(skip)]
/// Has the hash changed this run. Needed for nested cached dependencies.
pub(crate) hash_changed: bool,
}

impl JustfileCache {
fn new_empty(search: &Search) -> Self {
Self {
justfile_path: search.justfile.clone(),
working_directory: search.working_directory.clone(),
recipes: HashMap::new(),
}
}

pub(crate) fn new<'run>(search: &Search) -> RunResult<'run, Self> {
let cache_file = &search.cache_file;
let this = if cache_file.exists() {
let file_contents =
fs::read_to_string(cache_file).map_err(|io_error| Error::CacheFileRead {
cache_filename: cache_file.clone(),
io_error,
})?;
// Ignore newer versions, incompatible old versions or corrupted cache files
serde_json::from_str(&file_contents)
.or(Err(()))
.unwrap_or_else(|()| Self::new_empty(search))
} else {
Self::new_empty(search)
};
Ok(this)
}

pub(crate) fn insert_recipe(&mut self, name: String, body_hash: String) {
self.recipes.insert(
name,
RecipeCache {
body_hash,
hash_changed: true,
},
);
}

pub(crate) fn save<'run>(&self, search: &Search) -> RunResult<'run, ()> {
let cache = serde_json::to_string(self).map_err(|_| Error::Internal {
message: format!("Failed to serialize cache: {self:?}"),
})?;

search
.cache_file
.parent()
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::Unsupported,
format!(
"Cannot create parent directory of {}",
search.cache_file.display()
),
)
})
.and_then(fs::create_dir_all)
.and_then(|()| fs::write(&search.cache_file, cache))
.map_err(|io_error| Error::CacheFileWrite {
cache_filename: search.cache_file.clone(),
io_error,
})
}
}
26 changes: 14 additions & 12 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,19 @@ pub(crate) use {
enclosure::Enclosure, error::Error, evaluator::Evaluator, expression::Expression,
fragment::Fragment, function::Function, function_context::FunctionContext,
interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item,
justfile::Justfile, keyed::Keyed, keyword::Keyword, lexer::Lexer, line::Line, list::List,
load_dotenv::load_dotenv, loader::Loader, name::Name, namepath::Namepath, ordinal::Ordinal,
output::output, output_error::OutputError, parameter::Parameter, parameter_kind::ParameterKind,
parser::Parser, platform::Platform, platform_interface::PlatformInterface, position::Position,
positional::Positional, ran::Ran, range_ext::RangeExt, recipe::Recipe,
recipe_context::RecipeContext, recipe_resolver::RecipeResolver, scope::Scope, search::Search,
search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting,
settings::Settings, shebang::Shebang, shell::Shell, show_whitespace::ShowWhitespace,
source::Source, string_kind::StringKind, string_literal::StringLiteral, subcommand::Subcommand,
suggestion::Suggestion, table::Table, thunk::Thunk, token::Token, token_kind::TokenKind,
unresolved_dependency::UnresolvedDependency, unresolved_recipe::UnresolvedRecipe,
use_color::UseColor, variables::Variables, verbosity::Verbosity, warning::Warning,
justfile::Justfile, justfile_cache::JustfileCache, keyed::Keyed, keyword::Keyword,
lexer::Lexer, line::Line, list::List, load_dotenv::load_dotenv, loader::Loader, name::Name,
namepath::Namepath, ordinal::Ordinal, output::output, output_error::OutputError,
parameter::Parameter, parameter_kind::ParameterKind, parser::Parser, platform::Platform,
platform_interface::PlatformInterface, position::Position, positional::Positional, ran::Ran,
range_ext::RangeExt, recipe::Recipe, recipe_context::RecipeContext,
recipe_resolver::RecipeResolver, scope::Scope, search::Search, search_config::SearchConfig,
search_error::SearchError, set::Set, setting::Setting, settings::Settings, shebang::Shebang,
shell::Shell, show_whitespace::ShowWhitespace, source::Source, string_kind::StringKind,
string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table,
thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency,
unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables,
verbosity::Verbosity, warning::Warning,
},
std::{
cmp,
Expand Down Expand Up @@ -142,6 +143,7 @@ mod interrupt_guard;
mod interrupt_handler;
mod item;
mod justfile;
mod justfile_cache;
mod keyed;
mod keyword;
mod lexer;
Expand Down
Loading