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

Make the justfile more portable and slightly more resilient #1904

Merged
merged 9 commits into from
Mar 22, 2025

Conversation

EliahKagan
Copy link
Member

@EliahKagan EliahKagan commented Mar 21, 2025

See the commit messages (and diff) for full details. Some highlights are described below.

When cargo metadata fails and it's not a problem

Before these changes:

ek in 🌐 catenary in gitoxide on ξ‚  main is πŸ“¦ v0.41.0 via πŸ¦€ v1.85.1
❯ rm -r gix-packetline-blocking/src

ek in 🌐 catenary in gitoxide on ξ‚  main [✘] is πŸ“¦ v0.41.0 via πŸ¦€ v1.85.1
❯ just copy-packetline
error: failed to load manifest for workspace member `/home/ek/repos/gitoxide/gix-ref`
referenced by workspace at `/home/ek/repos/gitoxide/Cargo.toml`

Caused by:
  failed to load manifest for dependency `gix-object`

Caused by:
  failed to load manifest for dependency `gix-odb`

Caused by:
  failed to load manifest for dependency `gix-pack`

Caused by:
  failed to load manifest for dependency `gix-diff`

Caused by:
  failed to load manifest for dependency `gix-filter`

Caused by:
  failed to load manifest for dependency `gix-packetline-blocking`

Caused by:
  failed to parse manifest at `/home/ek/repos/gitoxide/gix-packetline-blocking/Cargo.toml`

Caused by:
  can't find library `gix_packetline_blocking`, rename file to `src/lib.rs` or specify lib.path
./etc/copy-packetline.sh

The copy-packetline.sh script actually succeeded, because it didn't need the cargo manifest output. But cargo manifest was called anytime just was used to run any recipe, or even just to list the recipes. Besides occasionally making the first invocation take noticeably longer, this is misleading, because it looks like something relevant to the requested operation failed. (See also #1310 (comment).)

After these changes:

ek in 🌐 catenary in gitoxide on ξ‚  run-ci/just is πŸ“¦ v0.41.0 via πŸ¦€ v1.85.1
❯ rm -r gix-packetline-blocking/src

ek in 🌐 catenary in gitoxide on ξ‚  run-ci/just [✘] is πŸ“¦ v0.41.0 via πŸ¦€ v1.85.1
❯ just copy-packetline
etc/copy-packetline.sh

Making future additions more discoverable

Related to the above, there are various approaches we can use to have Rust code that builds and runs even when the workspace is broken. This is relevant if we decide to introduce Rust code that generates Rust code that, if absent, may cause the workspace to be considered broken. One of the approaches for this, which requires no extra tools, is to have a project that opts out of being in any workspace by having an empty [workspace] section in its Cargo.toml.

Such a project is then less discoverable. But it can be made discoverable by including commands that use it in the justfile. That is only feasible if the justfile avoids eagerly failing when the workspace is broken.

That's actually may main motivation for doing this PR: to open up the ability to do things like copy-packetline.sh in Rust, as I fear may be needed for #1886 (depending on what the answer to #1886 (comment) turns out to be).

When cargo metadata fails and it is a problem

Recipes that actually use the output of cargo metadata, such as journey-tests, showed the same errors twice. In principle, they could in the future come to have been written in a way that would cause them to behave even more strangely, if they did something first that did not use cargo. Now that we only use cargo metadata when we need the output, this is made into a hard error, and thus is only run once.

It may be that this was originally intended--though then, since it was run eagerly before all recipes, it would have kept anything from working even when failures was not a problem. The reason it was not a hard error before is that the failing command is on the left side of a pipe, where it does not cause the whole pipeline to fail, and the jq parsing command on the right side of the pipe produced empty output but still reported success even when its input was empty.

Quoting

Before these changes, on Windows:

C:\Users\ek\source\repos\gitoxide [main ≑]> just
C:\Users\ek\.cargo\bin\just.exe --list
/usr/bin/bash: line 1: C:Usersek.cargobinjust.exe: command not found
error: Recipe `default` failed on line 6 with exit code 127

After these changes, on Windows:

C:\Users\ek\source\repos\gitoxide [run-ci/just ≑]> just
'C:\Users\ek\.cargo\bin\just.exe' --list
Available recipes:
    audit                           # Run various auditing tools to help us stay legal and safe
    check                           # Build all code in suitable configurations [alias: c]
    check-mode                      # Find shell scripts whose +x/-x bits and magic bytes (e.g. `#!`) disagree
    check-size                      # Run `cargo diet` on all crates to see that they are still in bounds
    ci-check-msrv                   # Check the minimal support Rust version, with the currently installed Rust version
    ci-journey-tests                # Run all journey tests - should be run in a fresh clone or after `cargo clean`
    ci-test                         # Run all tests, without clippy, and try building docs
    clear-target                    # Clean the `target` directory
    clippy *clippy-args             # Run `cargo clippy` on all crates
    clippy-fix                      # Run `cargo clippy` on all crates, fixing what can be fixed, and format all code
    copy-packetline                 # Delete `gix-packetline-blocking/src` and regenerate from `gix-packetline/src`
    default                         # List available recipes
    doc $RUSTDOCFLAGS='-D warnings' # Run `cargo doc` on all crates
    find-yanked                     # Cancel this after the first few seconds, as yanked crates will appear in warnings
    fmt                             # Run nightly `rustfmt` for its extra features, but check that it won't upset stable `rustfmt`
    journey-tests                   # Run journey tests (`max`)
    journey-tests-async             # Run journey tests (`lean-async`)
    journey-tests-pure              # Run journey tests (`max-pure`)
    journey-tests-small             # Run journey tests (`small`)
    nextest *FLAGS='--workspace'    # Run tests with `cargo nextest` (all unit-tests, no doc-tests, faster) [alias: nt]
    nix-shell-macos                 # Enter a nix-shell able to build on macOS
    summarize EXPRESSION='all()'    # Run tests with `cargo nextest`, skipping none except as filtered, omitting status reports
    test                            # Run all tests, clippy, including journey tests, try building docs [alias: t]
    unit-tests                      # Run all unit tests
    unit-tests-flaky                # These tests aren't run by default as they are flaky (even locally)

Some other quoting improvements are included. For example, summarize no longer pastes single quotes around the expression, but instead quotes it robustly.

Although this mainly benefits Windows, in principle it can be needed on any platform, such as when a path used in a shell has spaces in one of its components.

Using the same just implementation

just_executable() was already used in the default recipe to make sure we run the same just for just --list rather than a different just. (It was used in a shell without quoting, but that is fixed here as described above.) But one recipe had previously hard-coded just and used it for formatting. This fixes that.

Descriptions

All recipes have # descriptions that show when the recipes are listed, and they are revised to slightly improve clarity and to eliminate unnecessary stylistic differences. (As shown above.)


For some of the changes here, there is more than one way they could be done, and whether this way is clearer or less clear than alternatives is subjective. Accordingly, even though this is kind of thing that, along the lines of the discussion in #1883, I might consider merging myself without a review, in this case I think it's better that I check.

This makes some recipes work on Windows, and on other systems if
there are wunusual paths, that had previously broken in those
scenarios. That includes the default recipe.
This is easier to read and verify for correctness, and also more
robust.
This makes two small changes to the style in which quoting is
expressed in the `justfile`:

- Outside of commands run by a shell (i.e. when `just` interprets
  the quotes), prefer single quotes over double quotes when there
  is both no intent for `\`-escape interpretation to occur and no
  other reason to use `"` (such as a literal `'`).

- Inside commands run by a shel (i.e. when the quoting is shell
  syntax rather than `just` syntax), omit double quotes around a
  few literal arguments that contain no `$` nor other characters
  the shell treats specially. They are alredy omitted in most
  commands similar to these.
Most recipes in `justfile` that use `cargo nextest run` write it
out in a command, even though a `nextest` recipe exists. This is
often done because the command appears in a non-leading line of the
recipe, so having the recipe depend on the `nextest` recipe would
not be feasible. However, it also has the benefit of being more
clear about exactly what command is being run, especially when the
arguments are long and complicated, as in `summarize`.

This changes `summarize` so that it invokes nextest explicitly as a
step of the recipe, rather than depending on `nextest` and passing
its arguments through.
This changes paths that are excuted by a shell in a `justfile` to
omit the leading `./` when the path already has a `/` later.

A path like `./a` is needed to run `a` from the current directory,
but `./a/b` is just a longer way to express `a/b`, since `a/b`
already has a `/` in it. It is the presence of a `/`, rather than
the presence of a leading `./` specifically, that causes paths to
be looked up relative to the current directory rather than by a
`PATH` search.

The reason to make this change is that a few of the commands that
use those paths are about to get a bit more complicated. It is
hoped that removing this small amount of noise, though currently
inconsequential, will allow the immediately forthcoming change to
be made while preserving readability.
- Have the `justfile` wait to run `cargo metadata` until it is
  running a recipe that actually needs the information from it.
  (Currently, that information is always the `target` directory
  location, in whose `debug` subdirectory some binaries are found.)

  This avoids a delay if `cargo metadata` has not run. The length
  of the delay varied but was often noticeable on Windows. Because
  running it again (in the absence of a clean or relevant change)
  is cheap, it is not a performance problem that this runs the
  command multiple times instead of once.

  This also avoids error messages from `cargo metadata` if it can't
  complete, unless it is actually being used. Those messages didn't
  prevent other recipes (besides those that used the metadata) from
  running. But they created the appearance of failure, and also
  were misleading when they didn't come from the recipe being run.

- Handle errors from `cargo metadata` more robustly, by causing
  recipes that actually need the result of `cargo metadata` to fail
  if it fail -- and to fail immediately before attempting any other
  operations -- rather than attempting to use empty data.

  The metadata are piped to `jq -r .target_directory`, which
  actually succeeds even if it doesn't receive any data, giving
  empty output. Before, an attempt was made to use the empty output
  in building paths meant to go to debug builds of some binaries.

  Because the command is now running only when the output is
  actually needed, it is fine to make it hard error when it fails.

  The natural way to do this would be to `set -o pipefail`. But
  while it is in the newest POSIX standard, there remain otherwise
  largely POSIX-compatible `sh` implementations in use that don't
  support it (koalaman/shellcheck#2555).

  Our test suite requires `bash`, so the next obvious choice would
  be to use a shebang recipe or script recipe for `bash`. The
  problem is that shebang and script recipes don't usually work on
  Windows, because `just` passes a Windows path with `\` separators
  to the shell unquoted. This often results in the shell trying to
  run a file whose name is transformed by treating them specially,
  then causing them to dropped as if by quote removal. For example,
  on a recipe named `hello` with a `#!/usr/bin/env bash` shebang:

      /bin/bash: C:UsersekAppDataLocalTempjust-BVbCgphello: No such file or directory

  (One way this happens is by argument processing in `cygwin1.dll`
  or `msys-2.0.dll`, which seek to bridge the gap between the Unix
  expectation that the caller is responsible for all expansions and
  the Windows expectation that the callee is responsible for some.
  See rust-lang/rust#82227. Even when
  only globbing is attempted, `\` can be treated specially if it
  seems to escape a wildcard, or to escape another `\` that seems
  to escape a wildcard, etc.)

  Fortunately, in this case we can just check if the result of
  trying to parse out the `target` directory path gave a nonempty
  result, and treat the failure to do that as a hard error.

- Remove the `check-mode` recipe's need for cargo metadata, by
  having it run the `internal-tools` binary via `cargo run` rather
  than looking up where the `target` directory is and finding `it`
  in `debug` under it.

  That approach is needed for the paths that are passed to the
  journey test runner, but `check-mode` can just use `cargo`.

  The preceding build command could be removed, since `cargo run`
  builds unless the binary is up to date. But keeping them as
  separate commands may make the output more readable.
One of the `justfile` recipes ran `just --fmt --unstable`, and did
so as such. The problem with this is that the `just` executable in
the outer `just` call is not guaranteed to be the same `just` that
the inner call in the recipe finds. For example:

- In general, a user might use a `just` that is not in `$PATH` even
  when another `just` is in `$PATH`. This would happen when testing
  changes to `just`, and potentially in other use cases.

- The "inner" `just` is looked up by a POSIX-compatible shell that,
  on Windows, may have a changed environment. For example, the
  `sh.exe` shim provided by Git for Windows modifies environment
  variables including placing some directories in the front of
  `$PATH`. (See discussion in GitoxideLabs#1864 for details.)

Thus `just` provides a built-in function `just_executable()` that
can be used, and which we already use in the default recipe.

This uses that function instead. However, it is not as simple as
`{{ just_executable() }} --fmt --unstable`, because the path may
require quoting to be run in a shell.

In practice, it almost always needs to be quoted on Windows, where
otherwise a `\` is being given to the shell (which the shell is
required to interpret as a quoting character; this is distinct from
a scenario where a path might be passed as an argument to a shell,
in which case strange things may or may not occur). It might rarely
need to be quoted on other systems too, it it has spaces or other
even weirder contents.

So this uses `{{ quote(just_executable()) }}`. It does so through
the `j` variable that has already been assigned that value.
- Make the style of `justfile` recipe descriptions more consistent
- Lightly adjust wording and punctuaiton for clarity
- Clarify some to avert misreadings even if a reader is inattentive
- Add descriptions for the several recipes that didn't have them
@EliahKagan EliahKagan requested a review from Byron March 21, 2025 15:41
Copy link
Member

@Byron Byron left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot - this is great and much appreciated.

Indeed, I only gave it a quick look and generally only want these things to work, without maintaining them ideally 😁. So if you feel there is a delay or it's really nothing you'd think I could object to, then please do feel free to merge these kind of PRs yourself.

(I will always look at them anyway and will stay in the loop that way, should there be something to change, it can always be a follow-up as well)

@Byron Byron merged commit e82e9fa into GitoxideLabs:main Mar 22, 2025
20 checks passed
@EliahKagan EliahKagan deleted the run-ci/just branch March 22, 2025 06:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants