diff --git a/py/private/py_venv/py_venv.bzl b/py/private/py_venv/py_venv.bzl index 7315440c..872a2e3f 100644 --- a/py/private/py_venv/py_venv.bzl +++ b/py/private/py_venv/py_venv.bzl @@ -57,19 +57,13 @@ def _py_venv_base_impl(ctx): # Check for duplicate virtual dependency names. Those that map to the same resolution target would have been merged by the depset for us. virtual_resolution = _py_library.resolve_virtuals(ctx) + + # Note that this adds the workspace root for us (sigh), don't need to add to it imports_depset = _py_library.make_imports_depset(ctx, extra_imports_depsets = virtual_resolution.imports) pth_lines = ctx.actions.args() pth_lines.use_param_file("%s", use_always = True) pth_lines.set_param_file_format("multiline") - - # FIXME: This was hardcoded in the original rule_py venv and is preserved - # for compatibility. Repo-absolute imports are Bad (TM) and shouldn't be on - # by default. I believe that as of recent rules_python, creating these - # repo-absolute imports is handled as part of the PyInfo calculation. If we - # get this from rules_python, it should be removed. Or it should be moved so - # that we calculate it as part of the imports depset logic. - pth_lines.add(".") pth_lines.add_all(imports_depset) site_packages_pth_file = ctx.actions.declare_file("{}.pth".format(ctx.attr.name)) diff --git a/py/tests/py_venv_conflict/BUILD.bazel b/py/tests/py_venv_conflict/BUILD.bazel index 3d40242d..d3a909c6 100644 --- a/py/tests/py_venv_conflict/BUILD.bazel +++ b/py/tests/py_venv_conflict/BUILD.bazel @@ -1,5 +1,16 @@ load("@bazel_skylib//rules:build_test.bzl", "build_test") -load("//py/unstable:defs.bzl", "py_venv") +load("//py:defs.bzl", "py_library") +load("//py/unstable:defs.bzl", "py_venv", "py_venv_test") + +py_library( + name = "lib", + srcs = [ + "lib.py", + ], + imports = [ + "..", + ], +) py_venv( name = "test_venv_error", @@ -9,6 +20,7 @@ py_venv( "manual", ], deps = [ + ":lib", "//py/tests/py_venv_conflict/a", "//py/tests/py_venv_conflict/b", ], @@ -18,6 +30,7 @@ py_venv( name = "test_venv_warning", package_collisions = "warning", deps = [ + ":lib", "//py/tests/py_venv_conflict/a", "//py/tests/py_venv_conflict/b", ], @@ -27,6 +40,7 @@ py_venv( name = "test_venv_ignore", package_collisions = "ignore", deps = [ + ":lib", "//py/tests/py_venv_conflict/a", "//py/tests/py_venv_conflict/b", ], @@ -39,3 +53,10 @@ build_test( ":test_venv_ignore", ], ) + +py_venv_test( + name = "validate_import_roots", + srcs = ["test_import_roots.py"], + main = "test_import_roots.py", + venv = ":test_venv_ignore", +) diff --git a/py/tests/py_venv_conflict/lib.py b/py/tests/py_venv_conflict/lib.py new file mode 100644 index 00000000..817d3441 --- /dev/null +++ b/py/tests/py_venv_conflict/lib.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 + +def add(a, b): + return a + b diff --git a/py/tests/py_venv_conflict/test_import_roots.py b/py/tests/py_venv_conflict/test_import_roots.py new file mode 100644 index 00000000..f763239c --- /dev/null +++ b/py/tests/py_venv_conflict/test_import_roots.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 + +import os +for k, v in os.environ.items(): + if k.startswith("BUILD_") or k.startswith("RUNFILES_"): + print(k, ":", v) + +print("---") + +from pathlib import Path + +# prefix components: +space = ' ' +branch = '| ' +# pointers: +tee = '+-- ' +last = '+-- ' + + +def tree(dir_path: Path, prefix: str=''): + """A recursive generator, given a directory Path object + will yield a visual tree structure line by line + with each line prefixed by the same characters + """ + contents = list(dir_path.iterdir()) + # contents each get pointers that are ├── with a final └── : + pointers = [tee] * (len(contents) - 1) + [last] + for pointer, path in zip(pointers, contents): + yield prefix + pointer + path.name + if path.is_dir(): # extend the prefix and recurse: + extension = branch if pointer == tee else space + # i.e. space because last, └── , above so no more | + yield from tree(path, prefix=prefix+extension) + +here = Path(".") +print(here.absolute().resolve()) +for line in tree(here): + print(line) + +print("---") + +import sys +for e in sys.path: + print("-", e) + +print("---") + +print(sys.prefix) + +import conflict +print(conflict.__file__) +assert conflict.__file__.startswith(sys.prefix) + +import noconflict +print(noconflict.__file__) +assert noconflict.__file__.startswith(sys.prefix) + +import py_venv_conflict.lib as srclib +print(srclib.__file__) +assert not srclib.__file__.startswith(sys.prefix) diff --git a/py/tests/py_venv_image_layer/BUILD.bazel b/py/tests/py_venv_image_layer/BUILD.bazel index 41b4c8e5..61d89467 100644 --- a/py/tests/py_venv_image_layer/BUILD.bazel +++ b/py/tests/py_venv_image_layer/BUILD.bazel @@ -45,7 +45,7 @@ platform_transition_filegroup( ) assert_tar_listing( - name = "my_app_amd64_layers_test", + name = "my_app_amd64_layers", actual = [":amd64_layers"], expected = ":my_app_amd64_layers_listing.yaml", ) @@ -57,7 +57,7 @@ platform_transition_filegroup( ) assert_tar_listing( - name = "my_app_arm64_layers_test", + name = "my_app_arm64_layers", actual = [":arm64_layers"], expected = ":my_app_arm64_layers_listing.yaml", ) diff --git a/py/tests/py_venv_image_layer/my_app_amd64_layers_listing.yaml b/py/tests/py_venv_image_layer/my_app_amd64_layers_listing.yaml index 75826ff1..7bc78804 100644 --- a/py/tests/py_venv_image_layer/my_app_amd64_layers_listing.yaml +++ b/py/tests/py_venv_image_layer/my_app_amd64_layers_listing.yaml @@ -2442,7 +2442,7 @@ files: layer: 1 files: - drwxr-xr-x 0 0 0 0 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/lib/python3.9/site-packages/ - - -rwxr-xr-x 0 0 0 356 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/lib/python3.9/site-packages/_aspect.pth + - -rwxr-xr-x 0 0 0 328 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/lib/python3.9/site-packages/_aspect.pth - -rwxr-xr-x 0 0 0 19 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/lib/python3.9/site-packages/_virtualenv.pth - -rwxr-xr-x 0 0 0 4342 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/lib/python3.9/site-packages/_virtualenv.py - drwxr-xr-x 0 0 0 0 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/lib/python3.9/site-packages/colorama-0.4.6.dist-info/ @@ -2510,10 +2510,10 @@ files: - drwxr-xr-x 0 0 0 0 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/ - drwxr-xr-x 0 0 0 0 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/ - drwxr-xr-x 0 0 0 0 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/bin/ - - -rwxr-xr-x 0 0 0 7827 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/bin/activate - - -rwxr-xr-x 0 0 0 813320 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/bin/python - - -rwxr-xr-x 0 0 0 813320 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/bin/python3 - - -rwxr-xr-x 0 0 0 813320 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/bin/python3.9 + - -rwxr-xr-x 0 0 0 8099 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/bin/activate + - -rwxr-xr-x 0 0 0 817416 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/bin/python + - -rwxr-xr-x 0 0 0 817416 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/bin/python3 + - -rwxr-xr-x 0 0 0 817416 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/bin/python3.9 - drwxr-xr-x 0 0 0 0 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/lib/ - drwxr-xr-x 0 0 0 0 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/lib/python3.9/ - -rwxr-xr-x 0 0 0 323 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/pyvenv.cfg diff --git a/py/tests/py_venv_image_layer/my_app_arm64_layers_listing.yaml b/py/tests/py_venv_image_layer/my_app_arm64_layers_listing.yaml index 1d062521..0a56adb5 100644 --- a/py/tests/py_venv_image_layer/my_app_arm64_layers_listing.yaml +++ b/py/tests/py_venv_image_layer/my_app_arm64_layers_listing.yaml @@ -2423,7 +2423,7 @@ files: layer: 1 files: - drwxr-xr-x 0 0 0 0 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/lib/python3.9/site-packages/ - - -rwxr-xr-x 0 0 0 356 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/lib/python3.9/site-packages/_aspect.pth + - -rwxr-xr-x 0 0 0 328 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/lib/python3.9/site-packages/_aspect.pth - -rwxr-xr-x 0 0 0 19 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/lib/python3.9/site-packages/_virtualenv.pth - -rwxr-xr-x 0 0 0 4342 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/lib/python3.9/site-packages/_virtualenv.py - drwxr-xr-x 0 0 0 0 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/lib/python3.9/site-packages/colorama-0.4.6.dist-info/ @@ -2491,10 +2491,10 @@ files: - drwxr-xr-x 0 0 0 0 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/ - drwxr-xr-x 0 0 0 0 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/ - drwxr-xr-x 0 0 0 0 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/bin/ - - -rwxr-xr-x 0 0 0 7828 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/bin/activate - - -rwxr-xr-x 0 0 0 693968 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/bin/python - - -rwxr-xr-x 0 0 0 693968 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/bin/python3 - - -rwxr-xr-x 0 0 0 693968 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/bin/python3.9 + - -rwxr-xr-x 0 0 0 8100 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/bin/activate + - -rwxr-xr-x 0 0 0 698064 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/bin/python + - -rwxr-xr-x 0 0 0 698064 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/bin/python3 + - -rwxr-xr-x 0 0 0 698064 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/bin/python3.9 - drwxr-xr-x 0 0 0 0 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/lib/ - drwxr-xr-x 0 0 0 0 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/lib/python3.9/ - -rwxr-xr-x 0 0 0 323 Jan 1 2023 ./py/tests/py_venv_image_layer/my_app_bin.runfiles/aspect_rules_py/py/tests/py_venv_image_layer/.my_app_bin/pyvenv.cfg diff --git a/py/tools/py/src/activate.tmpl b/py/tools/py/src/activate.tmpl index acf417b4..06e17477 100644 --- a/py/tools/py/src/activate.tmpl +++ b/py/tools/py/src/activate.tmpl @@ -58,9 +58,15 @@ deactivate nondestructive # The runfiles library code has some deps on this so we just set it :/ : "${BASH_SOURCE:=$0}" -VIRTUAL_ENV="$(realpath "$(dirname "$(dirname "${BASH_SOURCE}")")")" +VIRTUAL_ENV="$(dirname "$(dirname "${BASH_SOURCE}")")" export VIRTUAL_ENV +# HACK: (Ab)use the MacOS $PYTHONEXECUTABLE to record the `.runfiles`-relative +# interpreter path. This helps us avoid issues with the interpreter's path being +# `realpath`-ed in such a way that it escapes the `.runfiles` tree. +PYTHONEXECUTABLE="${VIRTUAL_ENV}/bin/python" +export PYTHONEXECUTABLE + # unset PYTHONHOME if set # this will fail if PYTHONHOME is set to the empty string (which is bad anyway) # could use `if (set -u; : $PYTHONHOME) ;` in bash. diff --git a/py/tools/py/src/runfiles_interpreter.tmpl b/py/tools/py/src/runfiles_interpreter.tmpl index a4e3dc2a..f53de4f3 100644 --- a/py/tools/py/src/runfiles_interpreter.tmpl +++ b/py/tools/py/src/runfiles_interpreter.tmpl @@ -80,7 +80,7 @@ source "${RUNFILES_DIR:-/dev/null}/${f}" 2>/dev/null || \ } >/dev/null # Look up the runfiles-based interpreter and put its dir _first_ on the path. -INTERPRETER="$(realpath $(rlocation {{INTERPRETER_TARGET}}))" +INTERPRETER="$(rlocation {{INTERPRETER_TARGET}})" # Figure out if we're dealing with just some program or a real install # <- possible $PYTHONHOME diff --git a/py/tools/venv_shim/src/main.rs b/py/tools/venv_shim/src/main.rs index 72bf5a8c..0faf8180 100644 --- a/py/tools/venv_shim/src/main.rs +++ b/py/tools/venv_shim/src/main.rs @@ -1,5 +1,6 @@ use miette::{miette, Context, IntoDiagnostic}; use std::env; +use std::env::VarError; use std::fs; use std::io::{self, BufRead}; use std::os::unix::process::CommandExt; @@ -45,7 +46,7 @@ fn parse_version_info(version_str: &str) -> Option { fn compare_versions(version_from_cfg: &str, executable_path: &Path) -> bool { if let Some(file_name) = executable_path.file_name().and_then(|n| n.to_str()) { - return file_name.ends_with(&format!("python{}", version_from_cfg)); + file_name.ends_with(&format!("python{}", version_from_cfg)) } else { false } @@ -64,12 +65,11 @@ fn find_python_executables(version_from_cfg: &str, exclude_dir: &Path) -> Option None } }) - .filter_map(|path| path.canonicalize().ok()) .filter(|potential_executable| potential_executable.parent() != Some(exclude_dir)) - .filter(|potential_executable| compare_versions(version_from_cfg, &potential_executable)) + .filter(|potential_executable| compare_versions(version_from_cfg, potential_executable)) .collect(); - if binaries.len() > 0 { + if !binaries.is_empty() { Some(binaries) } else { None @@ -77,15 +77,47 @@ fn find_python_executables(version_from_cfg: &str, exclude_dir: &Path) -> Option } fn main() -> miette::Result<()> { - let current_exe = env::current_exe().unwrap(); + let venv_home_path = env::var("VIRTUAL_ENV") + .map(PathBuf::from) + .into_diagnostic() + .map_err(|e| { + miette!( + help = format!( + "The activate script should be available as {:?}", + env::current_exe() + .unwrap() + .parent() + .unwrap() + .join("activate") + ), + "{e}", + ) + .wrap_err("$VIRTUAL_ENV was unbound! A venv must be activated") + })?; + + let venv_interpreter_path: PathBuf = env::var("PYTHONEXECUTABLE") + .map(PathBuf::from) + .or_else(|_| Ok::(venv_home_path.join("bin/python"))) + .into_diagnostic()?; + + let excluded_interpreters_dir = &venv_home_path.join("bin"); + let args: Vec<_> = env::args().collect(); #[cfg(feature = "debug")] - eprintln!("[aspect] Current executable path: {:?}", current_exe); + eprintln!( + "[aspect] Current executable path: {:?}", + &venv_interpreter_path + ); - let Some(pyvenv_cfg_path) = find_pyvenv_cfg(¤t_exe) else { - return Err(miette!("pyvenv.cfg not found one directory level up.")); + let Some(pyvenv_cfg_path) = find_pyvenv_cfg(&venv_interpreter_path) else { + return Err(miette!( + help = format!("VIRTUAL_ENV was {:?}", &venv_home_path), + "The virtual environment is either incorrectly structured or was incorrectly detected", + ) + .wrap_err("pyvenv.cfg not found!")); }; + #[cfg(feature = "debug")] eprintln!("[aspect] Found pyvenv.cfg at: {:?}", &pyvenv_cfg_path); @@ -98,14 +130,22 @@ fn main() -> miette::Result<()> { .unwrap(); let Some(version_info) = version_info_result else { - return Err(miette!("version_info key not found in pyvenv.cfg.")); + return Err(miette!( + help = format!("pyvenv.cfg must specify the version_info= key"), + "The virtual environment is incorrectly built or was incorrectly detected" + ) + .wrap_err("version_info key not found in pyvenv.cfg.")); }; #[cfg(feature = "debug")] - eprintln!("[aspect] version_info from pyvenv.cfg: {}", &version_info); + eprintln!("[aspect] version_info from pyvenv.cfg: {:?}", &version_info); let Some(target_python_version) = parse_version_info(&version_info) else { - return Err(miette!("Could not parse version_info as x.y.")); + return Err(miette!( + help = format!("Provided version info was {:?}", &version_info), + "Could not parse version_info as `x.y.z`" + ) + .wrap_err("Unable to determine interpreter revision")); }; #[cfg(feature = "debug")] @@ -114,12 +154,11 @@ fn main() -> miette::Result<()> { &target_python_version ); - let exclude_dir = current_exe.parent().unwrap().canonicalize().unwrap(); - #[cfg(feature = "debug")] - eprintln!("[aspect] Ignoring dir {:?}", &exclude_dir); + eprintln!("[aspect] Ignoring dir {:?}", &excluded_interpreters_dir); - let Some(python_executables) = find_python_executables(&target_python_version, &exclude_dir) + let Some(python_executables) = + find_python_executables(&target_python_version, excluded_interpreters_dir) else { return Err(miette!( "No suitable Python interpreter found in PATH matching version '{}'.", @@ -148,30 +187,27 @@ fn main() -> miette::Result<()> { } }; - let Some(interpreter_path) = python_executables.get(index) else { + let Some(actual_interpreter_path) = python_executables.get(index) else { return Err(miette!( "Unable to find another interpreter at index {}", index )); }; - let exe_path = current_exe.to_string_lossy().into_owned(); let exec_args = &args[1..]; #[cfg(feature = "debug")] eprintln!( "[aspect] Attempting to execute: {:?} with argv[0] as {:?} and args as {:?}", - interpreter_path, exe_path, exec_args, + &actual_interpreter_path, &venv_interpreter_path, exec_args, ); - let mut cmd = Command::new(&interpreter_path); + let mut cmd = Command::new(actual_interpreter_path); cmd.args(exec_args); // Lie about the value of argv0 to hoodwink the interpreter as to its // location on Linux-based platforms. - if cfg!(target_os = "linux") { - cmd.arg0(&exe_path); - } + cmd.arg0(&venv_interpreter_path); // On MacOS however, there are facilities for asking the C runtime/OS // what the real name of the interpreter executable is, and that value @@ -181,7 +217,7 @@ fn main() -> miette::Result<()> { // https://github.com/python/cpython/blob/68e72cf3a80362d0a2d57ff0c9f02553c378e537/Modules/getpath.c#L778 // https://docs.python.org/3/using/cmdline.html#envvar-PYTHONEXECUTABLE if cfg!(target_os = "macos") { - cmd.env("PYTHONEXECUTABLE", &exe_path); + cmd.env("PYTHONEXECUTABLE", &venv_interpreter_path); } // Re-export the counter so it'll go up @@ -189,5 +225,5 @@ fn main() -> miette::Result<()> { let _ = cmd.exec(); - return Ok(()); + Ok(()) }