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

dynamic recipe dependencies #1009

Closed
foenixx opened this issue Oct 28, 2021 · 12 comments
Closed

dynamic recipe dependencies #1009

foenixx opened this issue Oct 28, 2021 · 12 comments

Comments

@foenixx
Copy link

foenixx commented Oct 28, 2021

Hello! I'm switching to just from make. For now using just is just great :-).

A feature I cannot find any information about, is dynamic dependencies. In makefile, I can write something like this:

ifeq ...
  dep=recipe2
else
  dep=recipe3
endif

recipe1: $(dep)
    echo "recipe1"

recipe2:
    echo "recipe2"

recipe3:
    echo "recipe3"

Is there a way to achieve similar funcationality in just?

@casey
Copy link
Owner

casey commented Oct 28, 2021

Currently this isn't possible. Can you tell me more about your specific use-case? There might be a workaround.

@foenixx
Copy link
Author

foenixx commented Oct 29, 2021

Actually, I have no case right now. It's just that I've been using this approach several times during my strugglings with make. So this question just poped up when I was reading thru docs. For example, if you have build: clean dependency, but you may want to skip cleaning depending on command line parameters, env vars, etc.

I think that this behaviour can be simulated in justfile like this:

dep := "dont skip me"
recipe1: recipe2
	@echo "recipe1"

recipe2 arg=dep:
	#!/usr/bin/env bash
	set -euo pipefail
	[ -z "{{arg}}" ] && exit 0
	echo "recipe2 arg: {{arg}}"

And then you can pass empty string to dependent recipe to skip it:

❯ just recipe1
recipe2 arg: dont skip me
recipe1


> just dep= recipe1
recipe1

@liquidaty
Copy link

Also new to just (also coming from make) and ran into this issue, wanting to solve without having to rely on the shell (for same reasons as are behind the latest experimental no-shell version). Is the below the best solution? Are there any edge cases where this could behave unexpectedly such as loading a different set of environment variables when the just subcommand is run?

# Conditional targets. This example arbitrarily uses the env var RUN_RECIPE2 as a trigger

# if env var RUN_RECIPE2 is non-empty, run recipe2 else run recipe3

recipe1:
    echo "recipe1"
    {{ if env('RUN_RECIPE2','') != '' { 'just -f ' + justfile() + ' recipe2' } else { 'just -f ' + justfile() + ' recipe3' } }}

recipe2:
    echo "recipe2: {{ env('RUN_RECIPE2') }}"

recipe3:
    echo "recipe3"

@casey
Copy link
Owner

casey commented Dec 22, 2024

@liquidaty Sorry the workaround is so painful! T_T

You should have the same environment variables, but keep in mind that anything that can be set on the command line might be different.

What's your use-case that you need to call recipes dynamically? I'm curious if there's a better workaround, or perhaps something that could be better supported.

The big difference between just and make is that, essentially, just is statically typed with respect to recipes and variables. The justfile is statically analyzed ahead of time, and all variable and recipe references must be resolved statically. So it isn't possibly to dynamically call a recipe or refer to a variable by name.

@liquidaty
Copy link

@casey understood re static typing-- no worries, this solution is perfectly acceptable to me.

Current use case is experimental to see if just could replace make for a Makefile that has multiple targets that either run or don't run depending on whether an environment variable is set.

For example, the Makefile might be called via:

TARGET1=yes TARGET3=yes OTHER_PARAMETER=hello-there make
# or alternatively: make TARGET1=yes TARGET3=yes OTHER_PARAMETER=hello ANOTHER_PARAMETER=there

which would run target1 and target3, but skip target2. The reason these are passed parameters, and not targets, is simply that the calling program does not have any concept of a target-- everything is a parameter, and even those aren't known to (or defined by) the caller-- they are simply passed through at run time. I suppose it could be reengineered to make the calling program differentiate between targets vs parameters, but better if that could be treated independently of this experiment to replace make with just

@casey
Copy link
Owner

casey commented Dec 22, 2024

One thing you could do is make the target return early, which can be done with a shebang recipe:

target1:
  #!/usr/bin/env bash
  if $TARGET1 != yes; then
    exit
  fi
  # the rest of the recipe

And then unconditionally call them.

You can't do this with a linewise recipe (one without a shebang) because there's no way for a linewise recipe to indicate that just should not run the rest of the recipe after a command, aside from returning an error, which causes the whole run to terminate with that error.

We have sigils which change how recipe lines execute, and you could imagine a ? sigil which treated an error code as a signal to stop executing a linewise recipe, but not to return an error:

foo:
  ?[[ $FOO == yes]]
  # the rest of the recipe

Which would make this pattern possible with linewise recipes.

This is backwards incompatible, so would have to be enabled with a setting. I think this is a potentially interesting feature, I'll create an issue for it and see if anyone wants it.

Thanks for the issue! I'm always interested in how just can be used to replace make.

@casey casey closed this as completed Dec 22, 2024
@liquidaty
Copy link

The ? sigil approach would definitely be nice, to obviate the need for a nested just call. It would seem to also allow what I would imagine easily could be an idiomatic pattern which would be for target-specific checks to be placed at the top of each recipe. I think it would be even nicer if it could be followed by a condition that did not rely on the shell, such as ?{{ env('MY_VAR') != '' }}. Comparing the three, in a hypothetical that runs at least one of recipeA or recipeB, and maybe runs recipeC :

Current (for simplicity, assuming each env var has been set to an equivalent lower-case local var):

run_conditional_recipes:
    {{ if run_recipeA != '' { 'just -f ' + justfile() + ' recipeA' } else { 'just -f ' + justfile() + ' recipeB' } }}
    {{ if run_recipeC != '' { 'just -f ' + justfile() + ' recipeC' } else { '' } }}

recipeA:
    echo "recipeA"

recipeB:
    echo "recipeB"

recipeC:
    echo "recipeC"

With ? sigil:

recipeA:
   ? [ "${RUN_RECIPEA}" != "" ]
    echo "recipeA"

recipeB:
   ? [ "${RUN_RECIPEA}" = "" ]
    echo "recipeB"

recipeC:
   ? [ "${RUN_RECIPEC}" != "" ]
    echo "recipeC"

With ? sigil and no shell reliance, for simplicity using local vars instead of env vars:

recipeA:
   ? {{run_recipeA != ''}}
    echo "recipeA"

recipeB:
   ? {{run_recipeA == ''}}
    echo "recipeB"

recipeC:
   ? {{run_recipeA != ''}}
    echo "recipeC"

2nd form, which is your suggestions, seems like a significant improvement over current. The last form to me is a slight further improvement, but more of a nice-to-have (and I guess it might require supporting a whole 'nother expression datatype so maybe not worth it)

@casey
Copy link
Owner

casey commented Dec 23, 2024

Check it out: implemented in #2547. Not merged yet to collect some feedback first. Issue in #2544.

@torinthiel
Copy link

You've asked for a usecase - here's one that made me look into this issue. I'd like for the recipe to run either docker or podman, depending on availability. Thing is, not only executable name is different, but arguments as well.
What I've initially hoped for (which appears identical to what OP was trying to achieve) was

podman_or_docker := if `type -t podman || echo none` == "none" { "docker" } else { "podman" }
run: run-{{podman_or_docker}}

run-podman:
  podman run -ti --userns=keep-id whatever

run-docker:
  docker run -ti whatever

with that example it's relatively easy to make podman_or_docker contain either "docker" or "podman --userns=keep-id", but it gets messier if there's also build command (which needs same arguments either way) etc.

I see several potentials ways of solving this, each with it's own drawbacks.

  1. The impossible way, with conditional dependencies.
  2. ? sigil would achieve same result
  3. I can conditionally set run_extra_args to nothing or "--userns=keep-id". Might even end up cleanest (for whatever subjective definition of "cleanest" I'm using right now), as both 1 and 2 would suffer from having mostly identical code in more places, i.e. violating DRY

@liquidaty
Copy link

Would this work?

run:
  just run-{{podman_or_docker}}

This would not solve the issue of DRY being violated, but that seems like a different issue that should be handled separately (i.e. executing the nested just run when DRY is set, and/or making the just run an internally executed one rather than a shell-executed one). I thought I'd seen another issue posted on that topic but can't seem to find...

@torinthiel
Copy link

torinthiel commented Jan 15, 2025

technically it would, with standard set of caveats for nested just invocation - make sure you run the correct just, with correct justfile, your arguments just went bye-bye etc.

And for use-case which begs for arguments: a rule "fetch" that takes URL and uses curl or wget, depending on what's available.

I've also noticed #1309 which prooses 2 new syntaxes, one somewhat equivalent to ? sigil, so there are potential solutions 4 & 5 based on those.

@liquidaty
Copy link

technically it would, with standard set of caveats for nested just invocation - make sure you run the correct just, with correct justfile, your arguments just went bye-bye etc.

I would also like to see these issues resolved (independently of dynamic recipes). Some related issues: #1762, #2552, #744, #1829, #2538

I'm sure I've seen some other issue about running nested just calls internally instead of thru a separate shell process, but can't seem to locate

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants