Writing derivation without shell script/environment variables

I’m trying to write a very simple derivation that does not use a builder shell script, but rather calls a tool directly for simplicity/efficiency.

Let’s say that it looks something like the following:

derivation {
  name = "hello";
  system = "x86_64-linux";
  builder = "${pkgs.cue}/bin/cue";
  args = [
    "export"
    ./hello.cue
    "--outfile"
    "$out"
    "--out"
    "yaml"
  ];
}

I’ve randomly picked CUE as an example because it’s a pretty simple tool that takes an input and outputs a file at a given location.

This does not work, because $out is treated as a literal file path, and not substituted with the environment variable. This seems to mean

For comparison, in Buck2 (and Bazel works basically the same way), I would write a rule (i.e. a function that produces targets, roughly equivalent to a function that returns a derivation in Nix) that looks something like the following:

def cue(ctx: AnalysisContext):
    output = ctx.actions.declare_output("hello.yaml")
    # Taking `cue` from the system for brevity, it would normally be a store path.
    ctx.actions.run([
        "cue",
        "export",
        ctx.attrs.src,
        "--outfile",
        output.as_output(),
        "--out",
        "yaml",
    ])
    return output

(I’ve intentionally simplified the above a little bit to remove irrelevant boilerplate and make it more palatable to non-Buck users, don’t try running this at home as-is)

The point is: Buck can directly exec the cue binary and pass it an output path to write to, without wrapping it in a shell script just to grab $out from the environment.

Is something similar possible in Nix?

You can use builtins.placeholder "out" to get a string which will be replaced with the path of out at build time.

This is exactly what I was looking for, thanks for the very quick answer!