Skip to content

Commit fb1e8d8

Browse files
committed
✨ feat(files_glob): Add files_glob and watch_glob functions for pattern expansion
Updated the `README.md` to include an entry for the `files_glob` extension. The `files_glob` extension now supports expanding glob-like patterns into real file paths at Tiltfile load time, enhancing dependency management in builds. It includes two functions: `files_glob` for path expansion and `watch_glob` for file change detection, ensuring Tilt reloads as files are added. New tests have been added in `files_glob/test/Tiltfile` to validate functionality, including deduplication of file paths from overlapping patterns. The helper now utilizes bash/find for practical pattern resolution.
1 parent 58aeb56 commit fb1e8d8

File tree

9 files changed

+194
-0
lines changed

9 files changed

+194
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ All extensions have been vetted and approved by the Tilt team.
2121
- [`dotenv`](/dotenv): Load environment variables from `.env` or another file.
2222
- [`earthly`](/earthly): Build container images using [earthly](https://earthly.dev)
2323
- [`execute_in_pod`](/execute_in_pod): Execute a command on a pod container.
24+
- [`files_glob`](/files_glob): Expand glob-like patterns into real file paths for use in deps and other APIs.
2425
- [`file_sync_only`](/file_sync_only): No-build, no-push, file sync-only development. Useful when you want to live-reload a single config file into an existing public image, like nginx.
2526
- [`get_obj`](/get_obj): Get object yaml and the container's registry and image from an existing k8s resource such as deployment or statefulset
2627
- [`git_resource`](/git_resource): Deploy a dockerfile from a remote repository -- or specify the path to a local checkout for local development.

files_glob/README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# files_glob
2+
3+
Author: Kim Eik
4+
5+
Expand glob-like patterns (e.g., `**/*.go`, `dir/*.templ`) into real file paths at Tiltfile load time for use in APIs that require concrete paths (like `deps=` in `docker_build`/`custom_build`).
6+
7+
It also ensures Tilt reloads when matching files are added by watching the base directories implied by the patterns.
8+
9+
## Functions
10+
11+
- files_glob(*patterns) -> [str]
12+
- Expands common glob-style patterns into a de-duplicated list of file paths.
13+
- Examples:
14+
- `dir/**/*.ext` → recursively find files under `dir` matching `*.ext`
15+
- `dir/*.ext` → non-recursive match within `dir`
16+
- `*.ext` → match in current directory only
17+
- literal paths → included as-is
18+
- watch_glob(*patterns)
19+
- Adds `watch_file()` entries for base directories implied by patterns so Tilt reloads when new files appear. `files_glob()` calls this for you automatically.
20+
21+
Note: Implementation uses `bash`/`find` under the hood; it’s meant to be practical for common dev workflows, not a full glob engine.
22+
23+
## Usage
24+
25+
```python path=null start=null
26+
# Optionally set your default extension repo first if not using the shared repo:
27+
# v1alpha1.extension_repo(name='default', url='file:///path/to/tilt-extensions')
28+
29+
load('ext://files_glob', 'files_glob')
30+
31+
# Use with docker_build/custom_build deps
32+
srcs = files_glob('**/*.go', 'web/*.templ', 'scripts/*.sh')
33+
34+
docker_build('myimg', '.', deps=srcs)
35+
```
36+
37+
You can also import `watch_glob` explicitly if you want to add watches without expanding files:
38+
39+
```python path=null start=null
40+
load('ext://files_glob', 'watch_glob')
41+
watch_glob('**/*.sql', 'migrations/**/up/*.sql')
42+
```
43+
44+

files_glob/Tiltfile

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# -*- mode: Python -*-
2+
3+
# Local Tilt extension: expand glob-like patterns into real file paths for use in deps
4+
# Note: deps only accepts real paths; this helper uses bash/find to resolve common
5+
# patterns at Tiltfile load time (e.g., '**/*.ext', 'dir/*.ext', '*.ext').
6+
7+
8+
def _base_dir_from_pattern(pat):
9+
# Determine a base directory from a pattern for watch_file
10+
# Treat patterns that start with '**/' (or are '**' variants) as repo root
11+
if pat.startswith('**/') or pat == '**' or pat == '**/*' or pat.startswith('./**/'):
12+
return '.'
13+
# Handle patterns ending with '/**' or '/**/*' -> base dir before the **
14+
if pat.endswith('/**') or pat.endswith('/**/*'):
15+
base = pat.rsplit('/**', 1)[0]
16+
return base if base else '.'
17+
# Handle recursive patterns with '/**/' inside
18+
if '/**/' in pat:
19+
base = pat.split('/**/', 1)[0]
20+
return base if base else '.'
21+
# For simple 'dir/*.ext' or 'dir/name'
22+
if '/' in pat:
23+
base = pat.rsplit('/', 1)[0]
24+
return base if base else '.'
25+
# For '*.ext' or literal file name, base is current dir
26+
return '.'
27+
28+
29+
def watch_glob(*patterns):
30+
# Add Tiltfile reload watches for base dirs implied by patterns
31+
seen = {}
32+
for pat in patterns:
33+
base = _base_dir_from_pattern(pat)
34+
if base not in seen:
35+
seen[base] = True
36+
watch_file(base)
37+
38+
39+
def files_glob(*glob_patterns, **kwargs):
40+
"""
41+
Expand one or more patterns into a list of real file paths using bash/find.
42+
Supported patterns (good enough for most Tilt workflows):
43+
- 'dir/**/*.ext' => find dir -type f -name "*.ext"
44+
- 'dir/*.ext' => find dir -maxdepth 1 -type f -name "*.ext"
45+
- '*.ext' => find . -maxdepth 1 -type f -name "*.ext"
46+
- literal file/dir => included as-is
47+
Returns a de-duplicated list of paths.
48+
"""
49+
# Ensure Tiltfile reloads when new files matching these patterns are added
50+
watch_glob(*glob_patterns)
51+
results = []
52+
for pat in glob_patterns:
53+
# Handle recursive patterns with '**/' anywhere in the string (e.g., 'dir/**/x', '**/*.go')
54+
if '/**/' in pat:
55+
parts = pat.split('/**/', 1)
56+
base = parts[0] if parts[0] else '.'
57+
tail = parts[1] if len(parts) > 1 else ''
58+
if not tail or tail == '*':
59+
find_cmd = "bash --noprofile --norc -lc 'find " + base + " -type f -print'"
60+
else:
61+
if '/' in tail:
62+
# Fallback to -path for complex tails
63+
find_cmd = "bash --noprofile --norc -lc 'find " + base + " -type f -path \"*" + tail + "\" -print'"
64+
else:
65+
find_cmd = "bash --noprofile --norc -lc 'find " + base + " -type f -name \"" + tail + "\" -print'"
66+
# Handle patterns that START with '**/' (e.g., '**/*.go')
67+
elif pat.startswith('**/'):
68+
tail = pat[len('**/'):]
69+
if not tail or tail == '*':
70+
find_cmd = "bash --noprofile --norc -lc 'find . -type f -print'"
71+
else:
72+
if '/' in tail:
73+
find_cmd = "bash --noprofile --norc -lc 'find . -type f -path \"*" + tail + "\" -print'"
74+
else:
75+
find_cmd = "bash --noprofile --norc -lc 'find . -type f -name \"" + tail + "\" -print'"
76+
# Root-level wildcard like '*.go'
77+
elif pat.startswith('*.') or (pat and '*' in pat and '/' not in pat):
78+
find_cmd = "bash --noprofile --norc -lc 'find . -maxdepth 1 -type f -name \"" + pat + "\" -print'"
79+
# Directory-limited wildcard like 'dir/*.templ'
80+
elif '*' in pat and '/' in pat:
81+
dir_part, name_pat = pat.rsplit('/', 1)
82+
find_cmd = "bash --noprofile --norc -lc 'find " + dir_part + " -maxdepth 1 -type f -name \"" + name_pat + "\" -print'"
83+
else:
84+
# Literal path (file or dir). deps can watch directories too; include as-is.
85+
results.append(pat)
86+
continue
87+
88+
out = local(command=find_cmd, quiet=True)
89+
s = str(out).strip()
90+
if s:
91+
for line in s.split('\n'):
92+
p = line.strip()
93+
if p.startswith('./'):
94+
p = p[2:]
95+
if p:
96+
results.append(p)
97+
98+
# Deduplicate while preserving order
99+
seen = {}
100+
deduped = []
101+
for p in results:
102+
if p not in seen:
103+
seen[p] = True
104+
deduped.append(p)
105+
return deduped
106+

files_glob/test/Tiltfile

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
load('../Tiltfile', 'files_glob')
2+
3+
# Doesn't use any kubernetes context, therefore safe
4+
allow_k8s_contexts(k8s_context())
5+
6+
# Create some sample files for matching
7+
# (These are committed to the repo; tests assume files exist on disk.)
8+
9+
# Validate root-level pattern
10+
paths_root_txt = files_glob('*.txt')
11+
expected_root_txt = ['root.txt']
12+
if set(paths_root_txt) != set(expected_root_txt):
13+
fail('root-level pattern failed: got %s, expected %s' % (paths_root_txt, expected_root_txt))
14+
15+
# Validate non-recursive dir pattern
16+
paths_dir1_txt = files_glob('dir1/*.txt')
17+
expected_dir1_txt = ['dir1/a.txt', 'dir1/b.txt']
18+
if set(paths_dir1_txt) != set(expected_dir1_txt):
19+
fail('dir1/*.txt pattern failed: got %s, expected %s' % (paths_dir1_txt, expected_dir1_txt))
20+
21+
# Validate recursive pattern
22+
paths_dir2_txt = files_glob('dir2/**/*.txt')
23+
expected_dir2_txt = ['dir2/nested/t1.txt', 'dir2/nested/t2.txt']
24+
if set(paths_dir2_txt) != set(expected_dir2_txt):
25+
fail('dir2/**/*.txt pattern failed: got %s, expected %s' % (paths_dir2_txt, expected_dir2_txt))
26+
27+
# Validate deduplication (same files via two overlapping patterns)
28+
paths_dedupe = files_glob('dir1/*.txt', 'dir1/**/*.txt')
29+
if len(paths_dedupe) != len(set(paths_dedupe)):
30+
fail('deduplication failed: got %d items with duplicates: %s' % (len(paths_dedupe), paths_dedupe))
31+
32+
# CI expects at least one resource
33+
local_resource('dummy', 'echo ok')
34+

files_glob/test/dir1/a.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
alpha
2+

files_glob/test/dir1/b.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
bravo
2+

files_glob/test/dir2/nested/t1.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
t1 text
2+

files_glob/test/dir2/nested/t2.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
t2 text
2+

files_glob/test/root.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
root sample

0 commit comments

Comments
 (0)