macsetup 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- macsetup-0.1.0/.github/workflows/ci.yml +131 -0
- macsetup-0.1.0/.github/workflows/release.yml +55 -0
- macsetup-0.1.0/.gitignore +6 -0
- macsetup-0.1.0/LICENSE +21 -0
- macsetup-0.1.0/PKG-INFO +255 -0
- macsetup-0.1.0/README.md +230 -0
- macsetup-0.1.0/files/git/gitconfig.j2 +17 -0
- macsetup-0.1.0/files/shell/zshrc-block.zsh +6 -0
- macsetup-0.1.0/files/zsh/00-homebrew.zsh +10 -0
- macsetup-0.1.0/files/zsh/10-completion.zsh +22 -0
- macsetup-0.1.0/files/zsh/cli.zsh +18 -0
- macsetup-0.1.0/files/zsh/node.zsh +8 -0
- macsetup-0.1.0/files/zsh/python.zsh +12 -0
- macsetup-0.1.0/install.sh +111 -0
- macsetup-0.1.0/manifests/modules/cli.yaml +16 -0
- macsetup-0.1.0/manifests/modules/core.yaml +7 -0
- macsetup-0.1.0/manifests/modules/editors.yaml +16 -0
- macsetup-0.1.0/manifests/modules/fonts.yaml +5 -0
- macsetup-0.1.0/manifests/modules/git-identity.yaml +15 -0
- macsetup-0.1.0/manifests/modules/lang-node.yaml +8 -0
- macsetup-0.1.0/manifests/modules/lang-python.yaml +8 -0
- macsetup-0.1.0/manifests/modules/shell.yaml +18 -0
- macsetup-0.1.0/manifests/profiles.yaml +26 -0
- macsetup-0.1.0/manifests/vars.yaml +10 -0
- macsetup-0.1.0/pyproject.toml +60 -0
- macsetup-0.1.0/src/macsetup/__init__.py +1 -0
- macsetup-0.1.0/src/macsetup/brewfile.py +59 -0
- macsetup-0.1.0/src/macsetup/cli.py +481 -0
- macsetup-0.1.0/src/macsetup/detect.py +53 -0
- macsetup-0.1.0/src/macsetup/files.py +150 -0
- macsetup-0.1.0/src/macsetup/manifest.py +321 -0
- macsetup-0.1.0/src/macsetup/planner.py +141 -0
- macsetup-0.1.0/src/macsetup/runner.py +71 -0
- macsetup-0.1.0/src/macsetup/state.py +102 -0
- macsetup-0.1.0/src/macsetup/wizard.py +169 -0
- macsetup-0.1.0/tests/test_files.py +209 -0
- macsetup-0.1.0/tests/test_manifest.py +176 -0
- macsetup-0.1.0/tests/test_planner.py +115 -0
- macsetup-0.1.0/tests/test_state.py +84 -0
- macsetup-0.1.0/tests/test_wizard.py +165 -0
- macsetup-0.1.0/uv.lock +584 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
workflow_call: # release.yml runs the full suite before publishing
|
|
9
|
+
|
|
10
|
+
concurrency:
|
|
11
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
12
|
+
cancel-in-progress: true
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
# Everything that does not need a Mac. Fast, and the job that gates a PR.
|
|
16
|
+
lint-and-test:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v7
|
|
20
|
+
# Pinned to an exact tag on purpose: setup-uv published floating major
|
|
21
|
+
# tags up to v6 and then stopped, so `@v10` is not a resolvable ref even
|
|
22
|
+
# though v10.3.2 is the current release. Bump this deliberately.
|
|
23
|
+
- uses: astral-sh/setup-uv@v10.3.2
|
|
24
|
+
with:
|
|
25
|
+
enable-cache: true
|
|
26
|
+
|
|
27
|
+
# --locked rather than plain sync: a pyproject edit that was never
|
|
28
|
+
# reflected in uv.lock should fail here, not drift silently.
|
|
29
|
+
- run: uv sync --locked
|
|
30
|
+
|
|
31
|
+
- run: uv run ruff check .
|
|
32
|
+
- run: uv run ruff format --check .
|
|
33
|
+
- run: uv run pytest -q
|
|
34
|
+
- run: shellcheck install.sh
|
|
35
|
+
|
|
36
|
+
# The suite loads the catalog and resolves every profile, so a YAML typo
|
|
37
|
+
# or a dangling `requires:` already fails above. This covers the other
|
|
38
|
+
# packaging invariant: manifests/ and files/ are force-included into the
|
|
39
|
+
# wheel as _data/, and without them `uv tool install macsetup` installs
|
|
40
|
+
# an engine with nothing to run against.
|
|
41
|
+
- name: the wheel carries the example catalog
|
|
42
|
+
run: |
|
|
43
|
+
uv build --wheel
|
|
44
|
+
for want in \
|
|
45
|
+
macsetup/_data/manifests/profiles.yaml \
|
|
46
|
+
macsetup/_data/manifests/vars.yaml \
|
|
47
|
+
macsetup/_data/manifests/modules/core.yaml \
|
|
48
|
+
macsetup/_data/files/shell/zshrc-block.zsh \
|
|
49
|
+
macsetup/_data/files/git/gitconfig.j2; do
|
|
50
|
+
if ! unzip -l dist/*.whl | grep -q "$want"; then
|
|
51
|
+
echo "::error file=pyproject.toml::$want is missing from the wheel"
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
done
|
|
55
|
+
echo "wheel contains the catalog"
|
|
56
|
+
|
|
57
|
+
# plan and apply shell out to `brew list`, so these need a runner that has
|
|
58
|
+
# Homebrew. This is the end-to-end pass: detect, plan, Brewfile generation
|
|
59
|
+
# and the file appliers, against a real brew rather than FakeRunner.
|
|
60
|
+
catalog:
|
|
61
|
+
runs-on: macos-latest
|
|
62
|
+
steps:
|
|
63
|
+
- uses: actions/checkout@v7
|
|
64
|
+
# Pinned to an exact tag on purpose: setup-uv published floating major
|
|
65
|
+
# tags up to v6 and then stopped, so `@v10` is not a resolvable ref even
|
|
66
|
+
# though v10.3.2 is the current release. Bump this deliberately.
|
|
67
|
+
- uses: astral-sh/setup-uv@v10.3.2
|
|
68
|
+
with:
|
|
69
|
+
enable-cache: true
|
|
70
|
+
- run: uv sync --locked
|
|
71
|
+
|
|
72
|
+
- name: the catalog loads
|
|
73
|
+
run: uv run macsetup modules
|
|
74
|
+
|
|
75
|
+
# No machine.toml exists on a runner, so every profile is exercised
|
|
76
|
+
# through the --profile path with picks falling back to their defaults.
|
|
77
|
+
- name: every profile plans and dry-runs
|
|
78
|
+
run: |
|
|
79
|
+
profiles=$(uv run python -c \
|
|
80
|
+
"import yaml;print(' '.join(yaml.safe_load(open('manifests/profiles.yaml'))['profiles']))")
|
|
81
|
+
echo "profiles: $profiles"
|
|
82
|
+
for p in $profiles; do
|
|
83
|
+
echo "::group::$p"
|
|
84
|
+
uv run macsetup plan --profile "$p"
|
|
85
|
+
uv run macsetup apply --profile "$p" --dry-run --yes
|
|
86
|
+
echo "::endgroup::"
|
|
87
|
+
done
|
|
88
|
+
|
|
89
|
+
- name: brewfiles generate
|
|
90
|
+
run: uv run macsetup plan --profile full --show-brewfile
|
|
91
|
+
|
|
92
|
+
- name: status and untracked run against a real machine
|
|
93
|
+
run: |
|
|
94
|
+
uv run macsetup status --profile minimal
|
|
95
|
+
uv run macsetup untracked --profile minimal
|
|
96
|
+
|
|
97
|
+
# The file appliers for real, against a throwaway HOME. `shell` has no
|
|
98
|
+
# packages, so brew is never invoked and nothing on the runner changes.
|
|
99
|
+
- name: file actions apply in a sandbox
|
|
100
|
+
run: |
|
|
101
|
+
sb=$(mktemp -d)
|
|
102
|
+
HOME="$sb" MACSETUP_CONFIG="$sb/.config" \
|
|
103
|
+
uv run macsetup apply --only shell --only git-identity --yes
|
|
104
|
+
test -f "$sb/.zshrc"
|
|
105
|
+
test -f "$sb/.config/macsetup/zsh/00-homebrew.zsh"
|
|
106
|
+
test -f "$sb/.config/macsetup/zsh/10-completion.zsh"
|
|
107
|
+
test -f "$sb/.config/macsetup/example-gitconfig"
|
|
108
|
+
# vars.yaml supplies the defaults now that the engine ships none
|
|
109
|
+
grep -q 'you@example.com' "$sb/.config/macsetup/example-gitconfig"
|
|
110
|
+
grep -q '>>> macsetup >>>' "$sb/.zshrc"
|
|
111
|
+
echo "file actions verified"
|
|
112
|
+
|
|
113
|
+
# The one-line installer, piped the way the README shows it, against a
|
|
114
|
+
# throwaway HOME. The runner already has brew and uv, so this covers the
|
|
115
|
+
# macsetup install and the hand-off; running it twice proves re-runs work.
|
|
116
|
+
- name: install.sh bootstraps and re-runs
|
|
117
|
+
run: |
|
|
118
|
+
sb=$(mktemp -d)
|
|
119
|
+
for run in 1 2; do
|
|
120
|
+
HOME="$sb" MACSETUP_SOURCE="$PWD" bash -s -- plan --profile minimal < install.sh
|
|
121
|
+
done
|
|
122
|
+
test -x "$sb/.local/bin/macsetup"
|
|
123
|
+
|
|
124
|
+
# Re-applying a converged machine must be a no-op, not a second write.
|
|
125
|
+
- name: applying twice is idempotent
|
|
126
|
+
run: |
|
|
127
|
+
sb=$(mktemp -d)
|
|
128
|
+
export HOME="$sb" MACSETUP_CONFIG="$sb/.config"
|
|
129
|
+
uv run macsetup apply --only shell --yes >/dev/null
|
|
130
|
+
uv run macsetup apply --only shell --yes | tee /tmp/second.txt
|
|
131
|
+
grep -q 'Nothing to do' /tmp/second.txt
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Pushing a tag like v0.2.0 publishes that version to PyPI. Bump `version` in
|
|
4
|
+
# pyproject.toml first; the tag has to match it or nothing is published.
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
tags: ["v*"]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
# The full CI run, Linux and macOS, gates the upload: a tag on a commit that
|
|
11
|
+
# never passed CI should not reach PyPI.
|
|
12
|
+
ci:
|
|
13
|
+
uses: ./.github/workflows/ci.yml
|
|
14
|
+
|
|
15
|
+
build:
|
|
16
|
+
needs: ci
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v7
|
|
20
|
+
# Pinned to an exact tag on purpose; see the note in ci.yml.
|
|
21
|
+
- uses: astral-sh/setup-uv@v10.3.2
|
|
22
|
+
|
|
23
|
+
# PyPI versions are permanent -- a mismatch caught here costs a re-tag,
|
|
24
|
+
# one caught after upload costs a version number.
|
|
25
|
+
- name: tag matches pyproject version
|
|
26
|
+
run: |
|
|
27
|
+
version=$(uv version --short)
|
|
28
|
+
if [ "v$version" != "$GITHUB_REF_NAME" ]; then
|
|
29
|
+
echo "::error file=pyproject.toml::tag $GITHUB_REF_NAME but pyproject says $version"
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
- run: uv build
|
|
34
|
+
- uses: actions/upload-artifact@v7
|
|
35
|
+
with:
|
|
36
|
+
name: dist
|
|
37
|
+
path: dist/
|
|
38
|
+
|
|
39
|
+
# Separate from build so the OIDC token that can publish never shares a job
|
|
40
|
+
# with the build. Trusted publishing: no API token is stored anywhere, PyPI
|
|
41
|
+
# trusts this workflow + environment directly.
|
|
42
|
+
publish:
|
|
43
|
+
needs: build
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
environment:
|
|
46
|
+
name: pypi
|
|
47
|
+
url: https://pypi.org/project/macsetup/
|
|
48
|
+
permissions:
|
|
49
|
+
id-token: write
|
|
50
|
+
steps:
|
|
51
|
+
- uses: actions/download-artifact@v8
|
|
52
|
+
with:
|
|
53
|
+
name: dist
|
|
54
|
+
path: dist/
|
|
55
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
macsetup-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sergio Infante
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
macsetup-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: macsetup
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Declarative provisioning for macOS: packages and dotfiles, with a preview
|
|
5
|
+
Project-URL: Homepage, https://github.com/neosergio/macsetup
|
|
6
|
+
Project-URL: Issues, https://github.com/neosergio/macsetup/issues
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: brew-bundle,dotfiles,homebrew,macos,provisioning
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: System :: Installation/Setup
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Requires-Dist: jinja2>=3.1.6
|
|
18
|
+
Requires-Dist: pydantic>=2.9
|
|
19
|
+
Requires-Dist: pyyaml>=6.0
|
|
20
|
+
Requires-Dist: questionary>=2.1.1
|
|
21
|
+
Requires-Dist: rich>=13.7
|
|
22
|
+
Requires-Dist: tomli-w>=1.0
|
|
23
|
+
Requires-Dist: typer>=0.15
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# macsetup
|
|
27
|
+
|
|
28
|
+
[](https://github.com/neosergio/macsetup/actions/workflows/ci.yml)
|
|
29
|
+
|
|
30
|
+
Declarative provisioning for macOS. Describe what a machine should have in
|
|
31
|
+
YAML; `macsetup plan` shows you the difference against reality, and
|
|
32
|
+
`macsetup apply` closes it.
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
$ macsetup plan
|
|
36
|
+
┏━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┓
|
|
37
|
+
┃ Module ┃ Package ┃ State ┃
|
|
38
|
+
┡━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━┩
|
|
39
|
+
│ core │ git, gh │ satisfied │
|
|
40
|
+
│ cli │ ripgrep │ install │
|
|
41
|
+
│ cli │ ~/.config/macsetup/zsh/90-… │ update │
|
|
42
|
+
│ lang-python │ uv │ satisfied │
|
|
43
|
+
└──────────────┴──────────────────────────────┴───────────┘
|
|
44
|
+
1 to install, 1 file to update, 6 satisfied
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Adding software is a YAML edit, never a code change. Re-running on a converged
|
|
48
|
+
machine is a no-op, and `plan` and `apply` share the same planner, so the
|
|
49
|
+
preview is exactly what apply acts on.
|
|
50
|
+
|
|
51
|
+
Built for the case where you have **several** Macs that should share an
|
|
52
|
+
environment without every machine getting every package.
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
One line, on a Mac with nothing on it yet:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/neosergio/macsetup/main/install.sh)"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
[`install.sh`](install.sh) installs Homebrew if it is missing, then uv, then
|
|
63
|
+
macsetup, and ends in the `init` wizard. Each step checks first, so it is safe
|
|
64
|
+
to re-run. Arguments replace the wizard, for unattended machines:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
curl -fsSL https://raw.githubusercontent.com/neosergio/macsetup/main/install.sh \
|
|
68
|
+
| bash -s -- apply --profile work --yes
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
If you already have Homebrew, the manual route is three commands:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
brew install uv
|
|
75
|
+
uv tool install macsetup
|
|
76
|
+
macsetup init
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`init` runs a wizard (machine name → profile → per-module choices), saves the
|
|
80
|
+
result to `~/.config/macsetup/machine.toml`, and applies it. After that no
|
|
81
|
+
flags are needed.
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
macsetup plan # what apply would do; changes nothing
|
|
85
|
+
macsetup apply # do it
|
|
86
|
+
macsetup status # drift: manifest vs this machine
|
|
87
|
+
macsetup untracked # installed packages no selected module accounts for
|
|
88
|
+
macsetup modules # the whole catalog and its profiles
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Every wizard answer has a flag equivalent, so a machine can be provisioned
|
|
92
|
+
unattended: `macsetup apply --profile work --yes`.
|
|
93
|
+
|
|
94
|
+
## Your own catalog
|
|
95
|
+
|
|
96
|
+
macsetup ships a small example catalog so a fresh install has something to run
|
|
97
|
+
against. It is an example — replace it with yours:
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
export MACSETUP_MANIFESTS=~/dotfiles/manifests
|
|
101
|
+
export MACSETUP_FILES=~/dotfiles/files
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Both are resolved in that order: the environment variable, then a `manifests/`
|
|
105
|
+
or `files/` directory in the current working directory, then the copy baked
|
|
106
|
+
into the wheel. Keeping your catalog in a private repo and the engine public
|
|
107
|
+
is the intended arrangement.
|
|
108
|
+
|
|
109
|
+
### Modules
|
|
110
|
+
|
|
111
|
+
One file per module in `manifests/modules/`, where `id` matches the filename
|
|
112
|
+
stem:
|
|
113
|
+
|
|
114
|
+
```yaml
|
|
115
|
+
id: cli
|
|
116
|
+
name: CLI tools
|
|
117
|
+
requires: [fonts] # dependency-closed at resolve time
|
|
118
|
+
packages:
|
|
119
|
+
- ripgrep # formula
|
|
120
|
+
- {name: ghostty, cask: true} # cask
|
|
121
|
+
files:
|
|
122
|
+
- copy: zsh/cli.zsh
|
|
123
|
+
to: ~/.config/macsetup/zsh/90-cli.zsh
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Casks must be marked explicitly.** Several names exist in both namespaces —
|
|
127
|
+
`docker` is the CLI formula *and* Docker Desktop the cask — and a bare
|
|
128
|
+
`brew install docker` silently picks the formula. Marking casks is the single
|
|
129
|
+
biggest class of bug the manifest format exists to prevent.
|
|
130
|
+
|
|
131
|
+
`always: true` puts a module on every machine regardless of profile.
|
|
132
|
+
`select: pick` turns a module into a per-package menu in the wizard, with
|
|
133
|
+
`default:` pre-ticked. Pydantic forbids unknown keys, so a typo fails at load
|
|
134
|
+
rather than halfway through an install.
|
|
135
|
+
|
|
136
|
+
### Profiles
|
|
137
|
+
|
|
138
|
+
`manifests/profiles.yaml` bundles modules into named sets; `"*"` means
|
|
139
|
+
everything.
|
|
140
|
+
|
|
141
|
+
```yaml
|
|
142
|
+
profiles:
|
|
143
|
+
work: [cli, lang-python, editors]
|
|
144
|
+
minimal: [cli]
|
|
145
|
+
full: ["*"]
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`machine.toml` stores the **resolved module list**, not just the profile name,
|
|
149
|
+
so a module added to a profile upstream cannot silently install itself on
|
|
150
|
+
every machine the next time they sync. `status` reports unadopted modules;
|
|
151
|
+
`configure` adopts them.
|
|
152
|
+
|
|
153
|
+
### Files
|
|
154
|
+
|
|
155
|
+
Three actions:
|
|
156
|
+
|
|
157
|
+
```yaml
|
|
158
|
+
files:
|
|
159
|
+
- copy: zsh/python.zsh # verbatim
|
|
160
|
+
to: ~/.config/macsetup/zsh/20-python.zsh
|
|
161
|
+
- render: git/gitconfig.j2 # Jinja2 + machine vars
|
|
162
|
+
to: ~/.gitconfig
|
|
163
|
+
mode: "600"
|
|
164
|
+
- block: shell/zshrc-block.zsh # region in a file you don't own
|
|
165
|
+
into: ~/.zshrc
|
|
166
|
+
position: top # default bottom
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`block:` maintains a marker-delimited region. Lines outside the markers are
|
|
170
|
+
preserved; re-applying rewrites only what's between them. `position: top`
|
|
171
|
+
exists because some formats are order-sensitive — an `Include` in `ssh_config`
|
|
172
|
+
is ignored if it lands after a matching block.
|
|
173
|
+
|
|
174
|
+
`render:` substitutes variables from `manifests/vars.yaml`, overridden per
|
|
175
|
+
machine by the `[vars]` table in `machine.toml`, plus `machine` and `home`
|
|
176
|
+
which are always supplied. Rendering uses `StrictUndefined`: a template
|
|
177
|
+
referencing a variable with no value is a hard error, not a silently empty
|
|
178
|
+
setting.
|
|
179
|
+
|
|
180
|
+
Before overwriting a file it did not create, `copy`/`render` leave a
|
|
181
|
+
`.macsetup-bak` copy alongside it.
|
|
182
|
+
|
|
183
|
+
**Nothing is symlinked.** A symlink would point into the installed wheel and
|
|
184
|
+
break on the next upgrade, so files are copied and you re-run `apply` after
|
|
185
|
+
editing a fragment.
|
|
186
|
+
|
|
187
|
+
## Design notes
|
|
188
|
+
|
|
189
|
+
A few decisions that are load-bearing, in case you're reading the source:
|
|
190
|
+
|
|
191
|
+
**Data is separate from code.** `manifests/` says what to install;
|
|
192
|
+
`src/macsetup/` only knows how. If adding software requires touching Python,
|
|
193
|
+
something is wrong.
|
|
194
|
+
|
|
195
|
+
**`brew list` runs exactly twice per invocation**, cached in `detect.py` —
|
|
196
|
+
never once per package. Checking 60 packages costs two subprocess calls, so a
|
|
197
|
+
converged machine plans in well under a second regardless of catalog size.
|
|
198
|
+
There is a test asserting the call count.
|
|
199
|
+
|
|
200
|
+
**Installs delegate to `brew bundle`** against a generated Brewfile rather than
|
|
201
|
+
shelling out per package. Homebrew already knows about retries, cask
|
|
202
|
+
quarantine and sudo prompts. Generated Brewfiles stay in
|
|
203
|
+
`~/.cache/macsetup/brewfiles/` so a failure can be inspected.
|
|
204
|
+
|
|
205
|
+
**`apply` never upgrades.** `brew bundle install` upgrades by default; macsetup
|
|
206
|
+
passes `--no-upgrade` so converging a machine cannot turn into a 40-minute
|
|
207
|
+
upgrade run. `--upgrade` is opt-in.
|
|
208
|
+
|
|
209
|
+
**Subprocess calls go through `runner.py`.** That protocol is the test seam —
|
|
210
|
+
the suite runs without touching the machine.
|
|
211
|
+
|
|
212
|
+
## Development
|
|
213
|
+
|
|
214
|
+
```sh
|
|
215
|
+
uv sync
|
|
216
|
+
uv run macsetup plan -p work
|
|
217
|
+
uv run pytest # no machine access required
|
|
218
|
+
uv run ruff check . && uv run ruff format .
|
|
219
|
+
uv tool install -e . # put `macsetup` on PATH, tracking this checkout
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Tests never mutate the machine: `FakeRunner` stands in for subprocesses, and
|
|
223
|
+
file steps run against a sandboxed `HOME`. To try the file actions safely:
|
|
224
|
+
|
|
225
|
+
```sh
|
|
226
|
+
SB=/tmp/ms-try; mkdir -p $SB
|
|
227
|
+
HOME=$SB MACSETUP_CONFIG=$SB/.config uv run macsetup apply --only shell --yes
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
`shell` has no packages, so that never invokes brew.
|
|
231
|
+
|
|
232
|
+
CI runs the same two things on every push. Lint, the suite and a check that
|
|
233
|
+
the wheel still carries the catalog go on Linux; `plan` and `apply` shell out
|
|
234
|
+
to `brew list`, so every profile is planned and dry-run on a macOS runner,
|
|
235
|
+
along with the file appliers against a throwaway `HOME` and a second apply to
|
|
236
|
+
prove re-running converges to nothing.
|
|
237
|
+
|
|
238
|
+
## Not built yet
|
|
239
|
+
|
|
240
|
+
- `sync` (git pull + apply), `add`, `prune`, `upgrade`, `doctor`
|
|
241
|
+
- macOS `defaults write` tweaks as a step type
|
|
242
|
+
- Mac App Store apps (`brew bundle` handles `mas` entries natively; no manifest
|
|
243
|
+
uses them yet)
|
|
244
|
+
|
|
245
|
+
## Prior art
|
|
246
|
+
|
|
247
|
+
[chezmoi](https://www.chezmoi.io/) does dotfiles thoroughly but not packages.
|
|
248
|
+
`brew bundle` does packages but has no preview, no drift report and no notion
|
|
249
|
+
of per-machine subsets. [nix-darwin](https://github.com/LnL7/nix-darwin) does
|
|
250
|
+
all of it and more, if you want to learn Nix. macsetup is the middle: plain
|
|
251
|
+
YAML, Homebrew underneath, a plan you read before anything happens.
|
|
252
|
+
|
|
253
|
+
## License
|
|
254
|
+
|
|
255
|
+
MIT
|