@@ -4,8 +4,8 @@ set -euo pipefail
44# Validates the application and Rust workspace selected by
55# setup-rust-build-cache, binds its cache keys to the application and Cargo.lock
66# bytes, and enables sccache for every later Rust build in the current GitHub
7- # Actions job. When target caching is enabled, it also publishes one confined,
8- # application-scoped CARGO_TARGET_DIR beneath RUNNER_TEMP .
7+ # Actions job. Always publishes a stable, confined, application-scoped
8+ # CARGO_TARGET_DIR. Remote target artifacts are isolated by source revision .
99#
1010# This script reads no application manifest and runs no application code. The
1111# action therefore remains independent of the package, adapter, and deployment
@@ -18,7 +18,18 @@ fail() {
1818
1919canonical_path () {
2020 local path=" $1 "
21- realpath " $path " 2> /dev/null || fail " could not resolve path '$path '"
21+ [[ ! " $path " =~ [[:cntrl:]] ]] || fail ' Rust cache paths must not contain control characters'
22+ local resolved
23+ # Preserve trailing newlines until validation, including in symlink targets.
24+ resolved=$( realpath " $path " 2> /dev/null && printf ' .' ) || fail " could not resolve path '$path '"
25+ resolved=${resolved% .}
26+ resolved=${resolved% $' \n ' }
27+ [[ ! " $resolved " =~ [[:cntrl:]] ]] || fail ' Resolved Rust cache paths must not contain control characters'
28+ printf ' %s' " $resolved "
29+ }
30+
31+ append_output () {
32+ printf ' %s=%s\n' " $1 " " $2 " >> " ${GITHUB_OUTPUT:? } "
2233}
2334
2435is_under () {
@@ -40,10 +51,12 @@ sha256_file() {
4051
4152main () {
4253 local workspace=" ${GITHUB_WORKSPACE:? GITHUB_WORKSPACE is required} "
43- local output_file= " ${GITHUB_OUTPUT:? GITHUB_OUTPUT is required} "
54+ : " ${GITHUB_OUTPUT:? GITHUB_OUTPUT is required} "
4455 local env_file=" ${GITHUB_ENV:? GITHUB_ENV is required} "
4556 local app_name=" ${EDGEZERO__RUST_CACHE__APP_NAME-} "
4657 local cache_target=" ${EDGEZERO__RUST_CACHE__CACHE_TARGET-false} "
58+ local save_target=" ${EDGEZERO__RUST_CACHE__SAVE_TARGET-false} "
59+ local revision=" ${EDGEZERO__RUST_CACHE__TARGET_CACHE_REVISION-} "
4760 local working_directory=" ${EDGEZERO__RUST_CACHE__WORKING_DIRECTORY-} "
4861
4962 [[ " $app_name " =~ ^[a-z0-9][a-z0-9._-]{0,63}$ ]] ||
@@ -52,6 +65,16 @@ main() {
5265 true | false) ;;
5366 * ) fail " input 'cache-target' must be 'true' or 'false'" ;;
5467 esac
68+ case " $save_target " in
69+ true | false) ;;
70+ * ) fail " input 'save-target' must be 'true' or 'false'" ;;
71+ esac
72+ [[ " $save_target " != true || " $cache_target " == true ]] ||
73+ fail " input 'save-target' requires cache-target true"
74+ if [[ " $cache_target " == true || -n " $revision " ]]; then
75+ [[ " $revision " =~ ^[0-9a-f]{40}$ || " $revision " =~ ^[0-9a-f]{64}$ ]] ||
76+ fail " input 'target-cache-revision' must be a full lowercase Git SHA (required when cache-target is true)"
77+ fi
5578 [[ -n " $working_directory " ]] ||
5679 fail " input 'working-directory' cannot be empty"
5780 [[ " $working_directory " != /* ]] ||
@@ -76,34 +99,67 @@ main() {
7699 [[ " $lock_sha " =~ ^[0-9a-f]{64}$ ]] ||
77100 fail " could not compute a lowercase SHA-256 for Cargo.lock"
78101
79- {
80- printf ' cache-namespace=%s\n' " $app_name "
81- printf ' cargo-lock-sha256=%s\n' " $lock_sha "
82- } >> " $output_file "
102+ local runner_temp=" ${RUNNER_TEMP:? RUNNER_TEMP is required} "
103+ [[ -d " $runner_temp " ]] || fail " RUNNER_TEMP does not exist or is not a directory"
104+ local runner_temp_real cache_root app_dir target_dir selection_file path
105+ runner_temp_real=$( canonical_path " $runner_temp " )
106+ cache_root=" $runner_temp_real /edgezero-rust-cache"
107+ app_dir=" $cache_root /$app_name "
108+ target_dir=" $app_dir /target"
109+ selection_file=" $app_dir /selection"
110+ for path in " $cache_root " " $app_dir " " $target_dir " " $selection_file " ; do
111+ [[ ! -L " $path " ]] || fail " Rust cache path must not be a symlink: '$path '"
112+ done
113+ for path in " $cache_root " " $app_dir " " $target_dir " ; do
114+ [[ ! -e " $path " || -d " $path " ]] || fail " Rust cache path is not a directory: '$path '"
115+ done
116+ [[ ! -e " $selection_file " || -f " $selection_file " ]] || fail " Rust cache selection is not a regular file"
117+
118+ local ownership_revision=" $revision " selection already_initialized=false
119+ if [[ -z " $ownership_revision " ]]; then
120+ ownership_revision=$( git -C " $project_real " rev-parse --verify HEAD 2> /dev/null || true)
121+ [[ " $ownership_revision " =~ ^[0-9a-f]{40}$ || " $ownership_revision " =~ ^[0-9a-f]{64}$ ]] || ownership_revision=' '
122+ fi
123+ selection=$( printf ' %s\n' " $project_real " " ${ownership_revision:- uncached} " " $lock_sha " " $cache_target " " $save_target " )
124+ if [[ -e " $selection_file " ]]; then
125+ [[ -n " $ownership_revision " ]] || fail " repeated non-Git cache setup requires target-cache-revision"
126+ [[ " $( < " $selection_file " ) " == " $selection " ]] ||
127+ fail " Rust target directory already belongs to a different workspace, revision, lockfile or cache settings"
128+ already_initialized=true
129+ elif [[ -d " $target_dir " ]]; then
130+ local target_contents
131+ # Quote control characters so newline-only names survive command substitution.
132+ target_contents=$( ls -Aq -- " $target_dir " ) || fail " could not inspect Rust target directory"
133+ [[ -z " $target_contents " ]] ||
134+ fail " Rust target directory contains unowned artifacts without a selection marker"
135+ fi
136+
137+ local runner_os=" ${RUNNER_OS:? RUNNER_OS is required} " runner_arch=" ${RUNNER_ARCH:? RUNNER_ARCH is required} "
138+ [[ " $runner_os " =~ ^[a-zA-Z0-9_-]+$ && " $runner_arch " =~ ^[a-zA-Z0-9_-]+$ ]] ||
139+ fail " runner OS and architecture must be safe cache-key components"
140+ local source_prefix source_key target_key=' '
141+ source_prefix=" edgezero-app-$app_name -rust-sources-$runner_os -$runner_arch -"
142+ source_key=" $source_prefix$lock_sha "
143+ [[ -z " $revision " ]] || target_key=" edgezero-app-$app_name -rust-target-v2-$runner_os -$runner_arch -$lock_sha -$revision -exact"
144+
145+ mkdir -p " $target_dir "
146+ target_dir=$( canonical_path " $target_dir " )
147+ is_under " $runner_temp_real " " $target_dir " ||
148+ fail " Cargo target cache directory must resolve inside RUNNER_TEMP"
149+ printf ' %s\n' " $selection " > " $selection_file "
150+
151+ append_output already-initialized " $already_initialized "
152+ append_output source-cache-key " $source_key "
153+ append_output source-cache-restore-prefix " $source_prefix "
154+ append_output target-cache-key " $target_key "
155+ append_output cargo-target-dir " $target_dir "
83156 {
84157 printf ' RUSTC_WRAPPER=sccache\n'
85158 printf ' SCCACHE_GHA_ENABLED=true\n'
86159 printf ' CARGO_INCREMENTAL=0\n'
160+ printf ' CARGO_TARGET_DIR=%s\n' " $target_dir "
161+ printf ' EDGEZERO__RUST_CACHE__TARGET_DIR=%s\n' " $target_dir "
87162 } >> " $env_file "
88-
89- if [[ " $cache_target " == " true" ]]; then
90- local runner_temp=" ${RUNNER_TEMP:? RUNNER_TEMP is required when cache-target is true} "
91- [[ -d " $runner_temp " ]] || fail " RUNNER_TEMP does not exist or is not a directory"
92-
93- local runner_temp_real target_dir
94- runner_temp_real=$( canonical_path " $runner_temp " )
95- target_dir=" $runner_temp_real /edgezero-rust-cache/$app_name /target"
96- mkdir -p " $target_dir "
97- target_dir=$( canonical_path " $target_dir " )
98- is_under " $runner_temp_real " " $target_dir " ||
99- fail " Cargo target cache directory must resolve inside RUNNER_TEMP"
100-
101- printf ' cargo-target-dir=%s\n' " $target_dir " >> " $output_file "
102- {
103- printf ' CARGO_TARGET_DIR=%s\n' " $target_dir "
104- printf ' EDGEZERO__RUST_CACHE__TARGET_DIR=%s\n' " $target_dir "
105- } >> " $env_file "
106- fi
107163}
108164
109165main " $@ "
0 commit comments