@@ -544,6 +544,104 @@ function common::detect_os_arch {
544544 export TARGETOS TARGETARCH
545545}
546546
547+ # ######################################################################
548+ # Download and install one tool version into a private, per-process
549+ # staging directory, then atomically publish the resulting binary
550+ # into the shared cache.
551+ #
552+ # `tools/install/<tool>.sh` installer scripts use a fixed filename
553+ # relative to their CWD (e.g. `terraform.zip`), which is only safe
554+ # with one process per CWD at a time - true for their original
555+ # purpose (one `RUN` per tool, per Docker build), not for N pre-commit
556+ # hook processes racing the same cache miss. Without this, two such
557+ # processes could interleave on the same shared directory: one's
558+ # `rm "$PKG"` deleting the archive out from under the other's still-
559+ # running `unzip` ("cannot find or open ... .zip"), or `unzip` meeting
560+ # a binary a sibling already extracted and blocking on an interactive
561+ # overwrite prompt that hangs under pre-commit's non-interactive
562+ # stdin. Giving every caller its own staging directory removes the
563+ # shared CWD these scripts were never designed to share.
564+ # Globals:
565+ # GITHUB_TOKEN - forwarded automatically; read directly by the
566+ # invoked installer script
567+ # Arguments:
568+ # tool_name (string) tool name, matching a `tools/install/<tool>.sh`
569+ # file and its expected `${TOOL^^}_VERSION` environment variable
570+ # version (string) exact version to install
571+ # installer_script (string) absolute path to the
572+ # `tools/install/<tool>.sh` script to invoke
573+ # env_var_name (string) name of the environment variable the
574+ # installer script reads its requested version from (e.g.
575+ # "TERRAFORM_VERSION")
576+ # cache_dir (string) final, shared cache directory for this
577+ # (tool, version) pair. Must already exist.
578+ # cached_bin (string) expected absolute path to the resolved binary
579+ # inside `cache_dir` once installed
580+ # Outputs:
581+ # Returns 0 once `cached_bin` exists - either from this process' own
582+ # install, or from another process that won the race first. Returns
583+ # 1 with an error message if the download/install itself failed.
584+ # ######################################################################
585+ function common::populate_tool_cache {
586+ local -r tool_name=" $1 "
587+ local -r version=" $2 "
588+ local -r installer_script=" $3 "
589+ local -r env_var_name=" $4 "
590+ local -r cache_dir=" $5 "
591+ local -r cached_bin=" $6 "
592+
593+ # `mktemp -d` guarantees a directory no other process is also using,
594+ # so each process' installer run is isolated - regardless of how
595+ # many processes hit the same cache miss at once.
596+ local staging_dir
597+ staging_dir=$( mktemp -d " ${cache_dir} .XXXXXXXXXX" ) || {
598+ common::colorify " red" " ERROR: Failed to create a staging directory for '$tool_name ' version '$version '."
599+ return 1
600+ }
601+
602+ # Redirect the installer's own stdout to stderr: this function's stdout is
603+ # a contract (the resolved path, captured via "$(...)" by every caller),
604+ # and installers like terraform.sh/tflint.sh call bare `unzip` (no `-q`),
605+ # which prints "Archive: ... inflating: ..." to stdout by default -
606+ # harmless noise in a Docker build log, but it would otherwise corrupt
607+ # the path this function returns.
608+ if ! (
609+ cd " $staging_dir " || exit 1
610+ export " $env_var_name =$version "
611+ " $installer_script " 1>&2
612+ ); then
613+ common::colorify " red" " ERROR: Failed to download '$tool_name ' version '$version ' via '$installer_script '."
614+ rm -rf " $staging_dir "
615+ return 1
616+ fi
617+
618+ # Another process may have already published `cached_bin` while this
619+ # one was downloading its own copy - if so, prefer that result over
620+ # ours (both are the same requested version) instead of racing again
621+ # on the `mv` below.
622+ if [[ -x $cached_bin ]]; then
623+ rm -rf " $staging_dir "
624+ return 0
625+ fi
626+
627+ # `mv` between two paths on the same filesystem is atomic (POSIX
628+ # `rename(2)`), and both sides here are plain files, not
629+ # directories, so a concurrent reader of `cached_bin` always sees
630+ # either the previous state or the fully-written new one - never a
631+ # partially-written file - on both GNU and BSD `mv`.
632+ if ! mv " $staging_dir /$( basename " $cached_bin " ) " " $cached_bin " 2> /dev/null; then
633+ rm -rf " $staging_dir "
634+ # Lost the race between the check above and this `mv`: some other
635+ # process' `mv` landed on `cached_bin` first. That's fine, its
636+ # result is equally valid, ours is simply redundant.
637+ [[ -x $cached_bin ]] && return 0
638+ common::colorify " red" " ERROR: Failed to move '$tool_name ' version '$version ' into place at '$cached_bin '."
639+ return 1
640+ fi
641+
642+ rm -rf " $staging_dir "
643+ }
644+
547645# ######################################################################
548646# Resolve a specific version of a wrapped tool's binary, downloading
549647# and caching it on demand if it isn't already cached.
@@ -682,20 +780,7 @@ function common::resolve_tool_path {
682780
683781 mkdir -p " $cache_dir "
684782
685- # Redirect the installer's own stdout to stderr: this function's stdout is
686- # a contract (the resolved path, captured via "$(...)" by every caller),
687- # and installers like terraform.sh/tflint.sh call bare `unzip` (no `-q`),
688- # which prints "Archive: ... inflating: ..." to stdout by default -
689- # harmless noise in a Docker build log, but it would otherwise corrupt
690- # the path this function returns.
691- if ! (
692- cd " $cache_dir " || exit 1
693- export " $env_var_name =$version "
694- " $installer_script " 1>&2
695- ); then
696- common::colorify " red" " ERROR: Failed to download '$tool_name ' version '$version ' via '$installer_script '."
697- exit 1
698- fi
783+ common::populate_tool_cache " $tool_name " " $version " " $installer_script " " $env_var_name " " $cache_dir " " $cached_bin " || exit $?
699784
700785 if [[ ! -x $cached_bin ]]; then
701786 common::colorify " red" " ERROR: '$tool_name ' installer completed but expected binary was not found at '$cached_bin '."
0 commit comments