Skip to content

Commit 5ccf48b

Browse files
authored
Merge pull request #12 from Reloaded-Project/feat/cache-doc-artifacts-by-default
Add cache-workspace-crates and cache-directories inputs for rust-cache
2 parents c1aece4 + b0bfb23 commit 5ccf48b

File tree

7 files changed

+296
-3
lines changed

7 files changed

+296
-3
lines changed

.github/workflows/test-packages-input.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ on:
1717
branches: [v1-master]
1818
paths:
1919
- "action.yml"
20+
- "scripts/**"
21+
- "tests/**/*.py"
2022
- "README.MD"
2123
- ".github/workflows/test-packages-input.yml"
2224
- "tests/fixtures/packages-input-workspace/**"

.github/workflows/test-workflow.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,20 @@ on:
88
branches: [v1-master]
99
paths:
1010
- "action.yml"
11+
- "scripts/**"
12+
- "tests/**/*.py"
1113
- ".github/workflows/test-workflow.yml"
1214

1315
jobs:
16+
test-python-helper:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- name: Checkout Action Repository
20+
uses: actions/checkout@v6
21+
- name: Run helper unit tests
22+
shell: bash
23+
run: python3 -m unittest tests/test_resolve_cache_directories.py
24+
1425
test-coverage-build:
1526
strategy:
1627
matrix:

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
__pycache__/
2+
*.py[cod]

README.MD

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ To use this action in your GitHub workflow, add the following step:
3939
target: 'x86_64-unknown-linux-gnu'
4040
install-rust-toolchain: true
4141
setup-rust-cache: true
42+
cache-workspace-crates: true
4243
use-tarpaulin: true
4344
use-binstall: true
4445
install-binstall: true
@@ -66,6 +67,8 @@ To use this action in your GitHub workflow, add the following step:
6667
| `target` | The target platform for the Rust compiler | No | `''` |
6768
| `install-rust-toolchain` | Whether to install the specified Rust toolchain | No | `true` |
6869
| `setup-rust-cache` | Whether to set up Rust caching | No | `true` |
70+
| `cache-workspace-crates` | Whether `rust-cache` should preserve workspace crate artifacts | No | `true` |
71+
| `cache-directories` | Extra cache directories, relative to `rust-project-path` unless absolute. | No | `''` |
6972
| `use-tarpaulin` | Whether to use Tarpaulin for code coverage. If false, only runs tests. | No | `true` |
7073
| `use-binstall` | Use `cargo-binstall` for installing tarpaulin. Else uses cargo install. | No | `true` |
7174
| `install-binstall` | Installs `cargo-binstall`. If false, assumes it is already available. | No | `true` |
@@ -169,7 +172,7 @@ This workflow runs on every push and performs the following steps:
169172
### Multiple Action Calls
170173

171174
If you're calling this action multiple times within the same job (e.g., testing different feature combinations),
172-
you can optimize build times by being selective about certain setup options on subsequent calls:
175+
you can optimise build times by being selective about certain setup options on subsequent calls:
173176

174177
```yaml
175178
# First call - full setup
@@ -192,7 +195,7 @@ you can optimize build times by being selective about certain setup options on s
192195
# ... other options
193196
```
194197

195-
**Key optimizations:**
198+
**Key optimisations:**
196199

197200
- `setup-rust-cache: false` - Skip cache setup after the first call
198201
- `install-rust-toolchain: false` - Skip toolchain installation if already installed
@@ -209,6 +212,46 @@ The action automatically configures Rust caching based on your settings:
209212
- Because it's assumed you'd use `cargo-binstall` for all CI tooling.
210213
- When `use-binstall` is disabled, binary caching is enabled for normal `cargo install` operations
211214

215+
### Rustdoc Cache Behaviour
216+
217+
This action caches the rustdoc output directory by default so later
218+
`cargo doc` steps can reuse generated documentation artifacts.
219+
220+
It also configures `Swatinem/rust-cache` to keep workspace crate artifacts by
221+
default (`cache-workspace-crates: true`), which helps later `cargo clippy`,
222+
`cargo check`, and similar commands reuse more compiled outputs.
223+
224+
If your workflow uses an explicit target triple, the cached doc path is:
225+
226+
```text
227+
<rust-project-path>/target/<target-triple>/doc
228+
```
229+
230+
Otherwise the cached doc path is:
231+
232+
```text
233+
<rust-project-path>/target/doc
234+
```
235+
236+
You can cache additional directories too:
237+
238+
```yaml
239+
- name: Run Tests and Upload Coverage
240+
uses: Reloaded-Project/devops-rust-test-and-coverage@v1
241+
with:
242+
rust-project-path: src
243+
target: x86_64-pc-windows-msvc
244+
cache-directories: |
245+
target/custom-tool-cache
246+
generated-api
247+
```
248+
249+
In that example, the action caches all of the following:
250+
251+
- `src/target/x86_64-pc-windows-msvc/doc` (automatic)
252+
- `src/target/custom-tool-cache`
253+
- `src/generated-api`
254+
212255
### Workflow for Binary Projects
213256

214257
This action is designed for Rust libraries without binary artifacts.

action.yml

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ inputs:
2525
description: "Whether to set up Rust caching"
2626
required: false
2727
default: "true"
28+
cache-workspace-crates:
29+
description: "Whether rust-cache should cache workspace crates"
30+
required: false
31+
default: "true"
32+
cache-directories:
33+
description: >-
34+
Additional directories to cache, relative to rust-project-path unless
35+
absolute. The rustdoc directory is cached automatically.
36+
required: false
37+
default: ""
2838
use-tarpaulin:
2939
description: "Whether to use Tarpaulin for code coverage. If false, only runs tests."
3040
required: false
@@ -116,6 +126,17 @@ runs:
116126
fi
117127
echo "PACKAGES_ARG=${PACKAGES_ARG}" >> $GITHUB_ENV
118128
129+
# rust-cache prunes rustdoc output by default, so resolve the automatic
130+
# rustdoc path plus any caller-provided cache directories here.
131+
- name: Normalize rust-cache directories
132+
if: inputs.setup-rust-cache == 'true'
133+
shell: bash
134+
run: python3 "${{ github.action_path }}/scripts/resolve_cache_directories.py"
135+
env:
136+
INPUT_RUST_PROJECT_PATH: ${{ inputs.rust-project-path }}
137+
INPUT_TARGET: ${{ inputs.target }}
138+
INPUT_CACHE_DIRECTORIES: ${{ inputs.cache-directories }}
139+
119140
- name: Install Rust Toolchain
120141
if: inputs.install-rust-toolchain == 'true'
121142
uses: actions-rust-lang/setup-rust-toolchain@v1
@@ -132,12 +153,15 @@ runs:
132153
with:
133154
key:
134155
test-${{ inputs.rust-toolchain }}-${{ inputs.rust-project-path }}-${{ inputs.target }}-${{ env.SAFE_FEATURES
135-
}}-${{ inputs.no-default-features }}-${{ inputs.use-cross }}-${{ env.SAFE_ADDITIONAL_TEST_ARGS }}
156+
}}-${{ inputs.no-default-features }}-${{ inputs.use-cross }}-${{ env.SAFE_ADDITIONAL_TEST_ARGS
157+
}}-${{ inputs.cache-workspace-crates }}
136158
cache-on-failure: true
137159
cache-all-crates: true
160+
cache-workspace-crates: ${{ inputs.cache-workspace-crates }}
138161
cache-bin: ${{ inputs.use-binstall == 'true' && 'false' || 'true' }} # Disable binary caching when using binstall to avoid conflicts
139162
workspaces: |
140163
${{ inputs.rust-project-path }} -> target
164+
cache-directories: ${{ env.RUST_CACHE_DIRECTORIES }}
141165

142166
- name: Install gcc-multilib (if on Ubuntu based System, and Target is not Host)
143167
if: inputs.use-cross == 'false' && runner.os == 'Linux'
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
#!/usr/bin/env python3
2+
"""Build the cache directory list for this action.
3+
4+
We always include the rustdoc output directory, then merge any extra
5+
``cache-directories`` entries from the workflow. Keeping that logic here makes
6+
``action.yml`` smaller and easy to test.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import ntpath
12+
import os
13+
import posixpath
14+
15+
16+
def main() -> int:
17+
"""Resolve cache directories and export them to GitHub Actions environment.
18+
19+
Reads configuration from environment variables, builds the cache directory list,
20+
and either writes it to the GitHub environment file or prints to stdout.
21+
22+
Returns:
23+
0 on success, non-zero on failure.
24+
"""
25+
project_path = os.environ.get("INPUT_RUST_PROJECT_PATH", ".")
26+
target_triple = os.environ.get("INPUT_TARGET", "")
27+
extra_directories = os.environ.get("INPUT_CACHE_DIRECTORIES", "")
28+
github_env = os.environ.get("GITHUB_ENV")
29+
30+
directories = resolve_cache_directories(
31+
project_path=project_path,
32+
target_triple=target_triple,
33+
extra_directories=extra_directories,
34+
)
35+
36+
if github_env:
37+
write_github_env(github_env, directories)
38+
else:
39+
for directory in directories:
40+
print(directory)
41+
42+
return 0
43+
44+
45+
def resolve_cache_directories(
46+
project_path: str, target_triple: str, extra_directories: str
47+
) -> list[str]:
48+
"""Build the cache directory list for a Rust project.
49+
50+
Always includes the rustdoc output directory, then merges any extra directories
51+
from the workflow input, deduplicating by qualified path.
52+
53+
Arguments:
54+
project_path: Path to the Rust project root (relative or absolute).
55+
target_triple: Target triple for cross-compilation (e.g., "x86_64-unknown-linux-gnu").
56+
extra_directories: Newline-separated list of additional cache directories.
57+
58+
Returns:
59+
List of qualified cache directory paths, with rustdoc directory first.
60+
"""
61+
default_doc_dir = "target/doc"
62+
if target_triple:
63+
default_doc_dir = f"target/{target_triple}/doc"
64+
65+
resolved: list[str] = [_qualify_path(default_doc_dir, project_path)]
66+
seen = set(resolved)
67+
68+
for raw_line in extra_directories.splitlines():
69+
directory = raw_line.strip().rstrip("\r")
70+
if not directory:
71+
continue
72+
73+
qualified = _qualify_path(directory, project_path)
74+
if qualified in seen:
75+
continue
76+
77+
resolved.append(qualified)
78+
seen.add(qualified)
79+
80+
return resolved
81+
82+
83+
def write_github_env(env_file: str, directories: list[str]) -> None:
84+
"""Export the resolved cache directories to a GitHub Actions environment file.
85+
86+
Writes the directories as a multiline environment variable that downstream
87+
workflow steps can read from `RUST_CACHE_DIRECTORIES`.
88+
89+
Arguments:
90+
env_file: Path to the GitHub environment file.
91+
directories: List of cache directory paths to export.
92+
"""
93+
with open(env_file, "a", encoding="utf-8") as handle:
94+
handle.write("RUST_CACHE_DIRECTORIES<<EOF\n")
95+
for directory in directories:
96+
handle.write(f"{directory}\n")
97+
handle.write("EOF\n")
98+
99+
100+
def _qualify_path(path: str, project_path: str) -> str:
101+
"""Resolve a path relative to the Rust project root.
102+
103+
Absolute paths are normalized as-is. Relative paths are joined with
104+
the project path and normalized.
105+
106+
Arguments:
107+
path: The path to qualify (can be absolute or relative).
108+
project_path: The Rust project root path.
109+
110+
Returns:
111+
Normalized path, or empty string if path is empty.
112+
"""
113+
if not path:
114+
return ""
115+
116+
normalized_for_check = path.replace("\\", "/")
117+
if posixpath.isabs(normalized_for_check) or ntpath.isabs(path):
118+
return _normalize_path(path)
119+
120+
if project_path == ".":
121+
return _normalize_path(path)
122+
123+
return _normalize_path(posixpath.join(project_path, path))
124+
125+
126+
def _normalize_path(path: str) -> str:
127+
"""Normalize a path using POSIX forward slashes.
128+
129+
Converts backslashes to forward slashes and normalizes the path,
130+
but does not make it absolute.
131+
132+
Arguments:
133+
path: The path to normalize.
134+
135+
Returns:
136+
Normalized POSIX-style path.
137+
"""
138+
path = path.replace("\\", "/")
139+
normalized = posixpath.normpath(path)
140+
if normalized == ".":
141+
return "."
142+
return normalized
143+
144+
145+
if __name__ == "__main__":
146+
raise SystemExit(main())
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import tempfile
2+
import unittest
3+
from pathlib import Path
4+
5+
from scripts.resolve_cache_directories import resolve_cache_directories
6+
from scripts.resolve_cache_directories import write_github_env
7+
8+
9+
class ResolveCacheDirectoriesTests(unittest.TestCase):
10+
def test_defaults_to_root_doc_directory(self) -> None:
11+
self.assertEqual(resolve_cache_directories(".", "", ""), ["target/doc"])
12+
13+
def test_uses_target_specific_doc_directory(self) -> None:
14+
self.assertEqual(
15+
resolve_cache_directories("src", "x86_64-pc-windows-msvc", ""),
16+
["src/target/x86_64-pc-windows-msvc/doc"],
17+
)
18+
19+
def test_qualifies_relative_directories_from_project_root(self) -> None:
20+
self.assertEqual(
21+
resolve_cache_directories(
22+
"src",
23+
"x86_64-pc-windows-msvc",
24+
"ci-extra-cache\ntarget/custom-tool-cache\ngenerated-api\n",
25+
),
26+
[
27+
"src/target/x86_64-pc-windows-msvc/doc",
28+
"src/ci-extra-cache",
29+
"src/target/custom-tool-cache",
30+
"src/generated-api",
31+
],
32+
)
33+
34+
def test_deduplicates_and_normalizes_paths(self) -> None:
35+
self.assertEqual(
36+
resolve_cache_directories(
37+
".",
38+
"",
39+
"target/doc\n./extra\nfoo/./bar/\nfoo/bar\n",
40+
),
41+
["target/doc", "extra", "foo/bar"],
42+
)
43+
44+
def test_preserves_posix_and_windows_absolute_paths(self) -> None:
45+
self.assertEqual(
46+
resolve_cache_directories(
47+
"src",
48+
"",
49+
"/tmp/cache-dir\nC:\\cache-dir\nrelative-cache\n",
50+
),
51+
["src/target/doc", "/tmp/cache-dir", "C:/cache-dir", "src/relative-cache"],
52+
)
53+
54+
def test_writes_multiline_env_output(self) -> None:
55+
with tempfile.TemporaryDirectory() as temp_dir:
56+
env_file = Path(temp_dir) / "github-env.txt"
57+
write_github_env(str(env_file), ["target/doc", "src/generated-api"])
58+
self.assertEqual(
59+
env_file.read_text(encoding="utf-8"),
60+
"RUST_CACHE_DIRECTORIES<<EOF\ntarget/doc\nsrc/generated-api\nEOF\n",
61+
)
62+
63+
64+
if __name__ == "__main__":
65+
unittest.main()

0 commit comments

Comments
 (0)