ggraft 1.0.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.
Files changed (48) hide show
  1. ggraft-1.0.0/.gitattributes +1 -0
  2. ggraft-1.0.0/.github/workflows/cd.yml +60 -0
  3. ggraft-1.0.0/.gitignore +5 -0
  4. ggraft-1.0.0/.vscode/settings.json +6 -0
  5. ggraft-1.0.0/PKG-INFO +164 -0
  6. ggraft-1.0.0/README.md +151 -0
  7. ggraft-1.0.0/pyproject.toml +31 -0
  8. ggraft-1.0.0/setup.cfg +4 -0
  9. ggraft-1.0.0/src/ggraft/__init__.py +6 -0
  10. ggraft-1.0.0/src/ggraft/__main__.py +4 -0
  11. ggraft-1.0.0/src/ggraft/cli/__init__.py +3 -0
  12. ggraft-1.0.0/src/ggraft/cli/commands/__init__.py +14 -0
  13. ggraft-1.0.0/src/ggraft/cli/commands/build.py +42 -0
  14. ggraft-1.0.0/src/ggraft/cli/commands/common.py +10 -0
  15. ggraft-1.0.0/src/ggraft/cli/commands/init.py +38 -0
  16. ggraft-1.0.0/src/ggraft/cli/commands/pull.py +65 -0
  17. ggraft-1.0.0/src/ggraft/cli/commands/targets.py +24 -0
  18. ggraft-1.0.0/src/ggraft/cli/main.py +61 -0
  19. ggraft-1.0.0/src/ggraft/config.py +87 -0
  20. ggraft-1.0.0/src/ggraft/errors.py +21 -0
  21. ggraft-1.0.0/src/ggraft/glsl/__init__.py +17 -0
  22. ggraft-1.0.0/src/ggraft/glsl/preprocessor.py +67 -0
  23. ggraft-1.0.0/src/ggraft/patching/__init__.py +15 -0
  24. ggraft-1.0.0/src/ggraft/patching/anchor.py +80 -0
  25. ggraft-1.0.0/src/ggraft/patching/engine.py +188 -0
  26. ggraft-1.0.0/src/ggraft/patching/operations.py +145 -0
  27. ggraft-1.0.0/src/ggraft/patching/patch.py +93 -0
  28. ggraft-1.0.0/src/ggraft/sources/__init__.py +3 -0
  29. ggraft-1.0.0/src/ggraft/sources/mcmeta.py +126 -0
  30. ggraft-1.0.0/src/ggraft.egg-info/PKG-INFO +164 -0
  31. ggraft-1.0.0/src/ggraft.egg-info/SOURCES.txt +46 -0
  32. ggraft-1.0.0/src/ggraft.egg-info/dependency_links.txt +1 -0
  33. ggraft-1.0.0/src/ggraft.egg-info/entry_points.txt +2 -0
  34. ggraft-1.0.0/src/ggraft.egg-info/requires.txt +1 -0
  35. ggraft-1.0.0/src/ggraft.egg-info/scm_file_list.json +42 -0
  36. ggraft-1.0.0/src/ggraft.egg-info/scm_version.json +8 -0
  37. ggraft-1.0.0/src/ggraft.egg-info/top_level.txt +1 -0
  38. ggraft-1.0.0/tests/__init__.py +0 -0
  39. ggraft-1.0.0/tests/fixtures.py +51 -0
  40. ggraft-1.0.0/tests/test_anchor.py +81 -0
  41. ggraft-1.0.0/tests/test_cli.py +212 -0
  42. ggraft-1.0.0/tests/test_config.py +83 -0
  43. ggraft-1.0.0/tests/test_engine.py +165 -0
  44. ggraft-1.0.0/tests/test_glsl.py +43 -0
  45. ggraft-1.0.0/tests/test_operations.py +108 -0
  46. ggraft-1.0.0/tests/test_patch.py +101 -0
  47. ggraft-1.0.0/tests/test_sources.py +167 -0
  48. ggraft-1.0.0/tests/test_structure.py +74 -0
@@ -0,0 +1 @@
1
+ * text=auto eol=lf
@@ -0,0 +1,60 @@
1
+ name: Build and Publish to PyPI
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ push:
6
+ tags:
7
+ - "*.*.*"
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ build_dist:
14
+ name: Build wheel and sdist
15
+ runs-on: ubuntu-latest
16
+
17
+ steps:
18
+ - name: Checkout
19
+ uses: actions/checkout@v4
20
+ with:
21
+ fetch-depth: 0
22
+
23
+ - name: Setup Python
24
+ uses: actions/setup-python@v5
25
+ with:
26
+ python-version: "3.12"
27
+
28
+ - name: Build distributions
29
+ run: |
30
+ python -m pip install --upgrade build
31
+ python -m build --wheel --sdist --outdir dist
32
+
33
+ - name: Upload distribution artifacts
34
+ uses: actions/upload-artifact@v4
35
+ with:
36
+ name: dist
37
+ path: dist/*
38
+ if-no-files-found: error
39
+
40
+ publish:
41
+ name: Publish to PyPI
42
+ needs: [build_dist]
43
+ runs-on: ubuntu-latest
44
+ environment:
45
+ name: pypi
46
+ permissions:
47
+ id-token: write
48
+ contents: read
49
+
50
+ steps:
51
+ - name: Download distribution artifacts
52
+ uses: actions/download-artifact@v4
53
+ with:
54
+ path: dist
55
+ merge-multiple: true
56
+
57
+ - name: Publish package distributions to PyPI
58
+ uses: pypa/gh-action-pypi-publish@release/v1
59
+ with:
60
+ skip-existing: true
@@ -0,0 +1,5 @@
1
+ __pycache__/
2
+ *.egg-info/
3
+ build/
4
+ dist/
5
+ .ggraft/
@@ -0,0 +1,6 @@
1
+ {
2
+ "files.exclude": {
3
+ "**/__pycache__": true,
4
+ "**/*.egg-info/": true,
5
+ }
6
+ }
ggraft-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.4
2
+ Name: ggraft
3
+ Version: 1.0.0
4
+ Summary: GLSL Graft - declarative injection patches for vanilla Minecraft shaders
5
+ Author: Neylz
6
+ License-Expression: MIT
7
+ Keywords: minecraft,glsl,shaders,resourcepack,patching
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Topic :: Software Development :: Build Tools
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: click>=8.1
13
+
14
+ # ggraft
15
+
16
+ **GLSL Graft** is a cli tool allowing declarative injection patches for vanilla Minecraft shaders.
17
+
18
+ Overriding a core shader means copying Mojang's file and changing one line. Do that across twenty shaders and every Minecraft update becomes twenty manual re-copies, with no record of what you actually changed. ggraft keeps the *edit* under version control instead of the result: you declare which line to change, and it re-derives the overrides from any version on demand.
19
+
20
+ ## Terminology
21
+
22
+ | Concept | Name |
23
+ | --- | --- |
24
+ | The vanilla GLSL going in | **base** |
25
+ | A file declaring edits | **patch** |
26
+ | Which shader it's for | **target** |
27
+ | Where in the source | **injection point** (or **anchor**) |
28
+ | One edit inside a patch | **injection** |
29
+ | What it does | **operation**: `insert`, `replace`, `wrap`, `declare` |
30
+ | Anchor didn't match | **unresolved injection** |
31
+ | Anchor matched N times | **ambiguous injection** |
32
+ | GLSL coming out | **patched shader** |
33
+
34
+ ## Install
35
+
36
+ ### From PyPI
37
+
38
+ ```bash
39
+ pip install ggraft
40
+ ```
41
+
42
+ ### From source
43
+
44
+ Needs Python 3.11+ and [Click](https://click.palletsprojects.com/).
45
+ Everything else is standard library.
46
+
47
+ ```bash
48
+ pip install -e .
49
+ ```
50
+
51
+ ## Quick start
52
+
53
+ ```bash
54
+ ggraft init 26.3 # write ggraft.toml
55
+ ggraft pull # fetch bases from misode/mcmeta
56
+ ggraft build # apply patches -> patched shaders
57
+ ```
58
+
59
+ ## Project configuration
60
+
61
+ `ggraft.toml`, found by walking up from the working directory. Paths resolve relative to it.
62
+
63
+ ```toml
64
+ [base]
65
+ version = "26.3" # Minecraft version to pull
66
+ dir = ".ggraft/base" # where bases land (gitignore this)
67
+ source = "assets/minecraft/shaders" # subtree to pull, stripped from local paths
68
+ repo = "misode/mcmeta" # optional
69
+
70
+ [patches]
71
+ dir = "patches"
72
+
73
+ [output]
74
+ dir = "assets/minecraft/shaders" # required
75
+ header = true # stamp a provenance comment
76
+ ```
77
+
78
+ Targets are relative to `base.source`: `core/terrain.vsh` reads from `.ggraft/base/core/terrain.vsh` and writes to `<output.dir>/core/terrain.vsh`.
79
+
80
+ ## Patches
81
+
82
+ ```toml
83
+ name = "iso-world"
84
+ targets = ["core/terrain.vsh", "core/rendertype_*.vsh"]
85
+ exclude = ["core/rendertype_lines.vsh"]
86
+
87
+ [[injection]]
88
+ op = "declare"
89
+ include = "foo:iso.glsl"
90
+
91
+ [[injection]]
92
+ op = "replace"
93
+ at = "gl_Position = ProjMat * ModelViewMat * vec4({expr}, 1.0);"
94
+ with = "gl_Position = iso_position(ProjMat, ModelViewMat, vec4({expr}, 1.0));"
95
+ ```
96
+
97
+ Injections apply in order, to every matching target. Patches stack in filename order.
98
+
99
+ ### Anchors
100
+
101
+ `at` is literal text, not a regex. `{name}` captures what sits in that position, `{match}` is the whole match; both are available in the replacement.
102
+
103
+ | Key | Meaning |
104
+ | --- | --- |
105
+ | `at` | the anchor pattern |
106
+ | `regex = true` | treat `at` as a regex, with named groups |
107
+ | `multiline = true` | let one `{name}` (or a regex `.`) span lines (false by default) |
108
+ | `occurrence = N` | take the Nth match (1-based) |
109
+ | `every = true` | apply to all matches |
110
+
111
+
112
+ No match is an **unresolved injection**; more than one without `occurrence` or `every` is an **ambiguous injection**. Both fail the build, naming the patch, injection index, target and line numbers.
113
+
114
+ ### Operations
115
+
116
+ | Operation | Keys | Effect |
117
+ | --- | --- | --- |
118
+ | `replace` | `with` | swap the anchored text |
119
+ | `insert` | `text`, `where` (`before`/`after`) | add text beside it |
120
+ | `wrap` | `prefix`, `suffix` | surround it, keeping it |
121
+ | `declare` | `include` or `text` | add a top-level declaration; no anchor |
122
+
123
+ `declare` inserts after the last `#include` at conditional-nesting depth zero, falling back to `#extension` then `#version`, and is idempotent. Appending after the *last* `#include` in the file would strand it inside an `#ifdef` branch that some shader variants skip.
124
+
125
+ ## Commands
126
+
127
+ | Command | Purpose |
128
+ | --- | --- |
129
+ | `ggraft pull [version]` | fetch bases; `--force` refetches, `-j` sets parallelism. Pulling another version replaces the base |
130
+ | `ggraft build` | apply patches; `--check` dry run, `-t` filters by glob |
131
+ | `ggraft targets` | show which patches hit which targets |
132
+ | `ggraft init [version]` | write a starter config; `--force` overwrites |
133
+
134
+ `-h`/`--help` works on the group and every command, `-V`/`--version` prints the version, and `-c`/`--config` points at a specific `ggraft.toml` instead of searching upwards. `pull --token` also reads `GITHUB_TOKEN`.
135
+
136
+ ### Build guarantees
137
+
138
+ - `--check` dry run without output writes.
139
+ - A target pattern matching no base file fails the build. What a patch orphaned by a Minecraft update looks like.
140
+ - A patch applying to nothing fails the build, so a mistake in `targets` or `exclude` cannot disable a patch silently.
141
+ - Stale output is pruned. Shaders ggraft wrote are recorded in `.ggraft/manifest.json`, and a target dropped from a patch has its override deleted on the next build. Only manifest entries are ever deleted; `-t` disables pruning, since a partial build cannot tell what is stale.
142
+
143
+ ## Bases
144
+
145
+ Uses [misode/mcmeta](https://github.com/misode/mcmeta).
146
+ Tree walk might be rate limited. Set `GITHUB_TOKEN` or pass `--token` to bypass the limit.
147
+
148
+ A pull downloads into a sibling `.partial` directory and swaps it in, so an interrupted pull leaves the existing base untouched and a completed one leaves nothing of it behind: `base.dir` is replaced wholesale, never merged into.
149
+
150
+ What the directory holds — version, repo and source — is recorded in `.ggraft/base.json`. Pulling a different version, repo or source therefore refetches instead of reporting the base as already present, and files from the old version cannot linger as targets for the next build. A base ggraft did not record is treated as stale and refetched too. `--force` refetches regardless.
151
+
152
+ ## Tests
153
+
154
+ ```bash
155
+ python -m unittest discover -s tests # everything
156
+ python -m unittest tests.test_cli # one module
157
+ ```
158
+
159
+ One module per layer: `test_structure`, `test_glsl`, `test_anchor`, `test_operations`, `test_engine`, `test_sources`, `test_cli`. Shared shaders, project files and a network-free mcmeta client live in `tests/fixtures.py`.
160
+
161
+ ## Planned Features
162
+ - [ ] functions injections
163
+ - [ ] python module API
164
+
ggraft-1.0.0/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # ggraft
2
+
3
+ **GLSL Graft** is a cli tool allowing declarative injection patches for vanilla Minecraft shaders.
4
+
5
+ Overriding a core shader means copying Mojang's file and changing one line. Do that across twenty shaders and every Minecraft update becomes twenty manual re-copies, with no record of what you actually changed. ggraft keeps the *edit* under version control instead of the result: you declare which line to change, and it re-derives the overrides from any version on demand.
6
+
7
+ ## Terminology
8
+
9
+ | Concept | Name |
10
+ | --- | --- |
11
+ | The vanilla GLSL going in | **base** |
12
+ | A file declaring edits | **patch** |
13
+ | Which shader it's for | **target** |
14
+ | Where in the source | **injection point** (or **anchor**) |
15
+ | One edit inside a patch | **injection** |
16
+ | What it does | **operation**: `insert`, `replace`, `wrap`, `declare` |
17
+ | Anchor didn't match | **unresolved injection** |
18
+ | Anchor matched N times | **ambiguous injection** |
19
+ | GLSL coming out | **patched shader** |
20
+
21
+ ## Install
22
+
23
+ ### From PyPI
24
+
25
+ ```bash
26
+ pip install ggraft
27
+ ```
28
+
29
+ ### From source
30
+
31
+ Needs Python 3.11+ and [Click](https://click.palletsprojects.com/).
32
+ Everything else is standard library.
33
+
34
+ ```bash
35
+ pip install -e .
36
+ ```
37
+
38
+ ## Quick start
39
+
40
+ ```bash
41
+ ggraft init 26.3 # write ggraft.toml
42
+ ggraft pull # fetch bases from misode/mcmeta
43
+ ggraft build # apply patches -> patched shaders
44
+ ```
45
+
46
+ ## Project configuration
47
+
48
+ `ggraft.toml`, found by walking up from the working directory. Paths resolve relative to it.
49
+
50
+ ```toml
51
+ [base]
52
+ version = "26.3" # Minecraft version to pull
53
+ dir = ".ggraft/base" # where bases land (gitignore this)
54
+ source = "assets/minecraft/shaders" # subtree to pull, stripped from local paths
55
+ repo = "misode/mcmeta" # optional
56
+
57
+ [patches]
58
+ dir = "patches"
59
+
60
+ [output]
61
+ dir = "assets/minecraft/shaders" # required
62
+ header = true # stamp a provenance comment
63
+ ```
64
+
65
+ Targets are relative to `base.source`: `core/terrain.vsh` reads from `.ggraft/base/core/terrain.vsh` and writes to `<output.dir>/core/terrain.vsh`.
66
+
67
+ ## Patches
68
+
69
+ ```toml
70
+ name = "iso-world"
71
+ targets = ["core/terrain.vsh", "core/rendertype_*.vsh"]
72
+ exclude = ["core/rendertype_lines.vsh"]
73
+
74
+ [[injection]]
75
+ op = "declare"
76
+ include = "foo:iso.glsl"
77
+
78
+ [[injection]]
79
+ op = "replace"
80
+ at = "gl_Position = ProjMat * ModelViewMat * vec4({expr}, 1.0);"
81
+ with = "gl_Position = iso_position(ProjMat, ModelViewMat, vec4({expr}, 1.0));"
82
+ ```
83
+
84
+ Injections apply in order, to every matching target. Patches stack in filename order.
85
+
86
+ ### Anchors
87
+
88
+ `at` is literal text, not a regex. `{name}` captures what sits in that position, `{match}` is the whole match; both are available in the replacement.
89
+
90
+ | Key | Meaning |
91
+ | --- | --- |
92
+ | `at` | the anchor pattern |
93
+ | `regex = true` | treat `at` as a regex, with named groups |
94
+ | `multiline = true` | let one `{name}` (or a regex `.`) span lines (false by default) |
95
+ | `occurrence = N` | take the Nth match (1-based) |
96
+ | `every = true` | apply to all matches |
97
+
98
+
99
+ No match is an **unresolved injection**; more than one without `occurrence` or `every` is an **ambiguous injection**. Both fail the build, naming the patch, injection index, target and line numbers.
100
+
101
+ ### Operations
102
+
103
+ | Operation | Keys | Effect |
104
+ | --- | --- | --- |
105
+ | `replace` | `with` | swap the anchored text |
106
+ | `insert` | `text`, `where` (`before`/`after`) | add text beside it |
107
+ | `wrap` | `prefix`, `suffix` | surround it, keeping it |
108
+ | `declare` | `include` or `text` | add a top-level declaration; no anchor |
109
+
110
+ `declare` inserts after the last `#include` at conditional-nesting depth zero, falling back to `#extension` then `#version`, and is idempotent. Appending after the *last* `#include` in the file would strand it inside an `#ifdef` branch that some shader variants skip.
111
+
112
+ ## Commands
113
+
114
+ | Command | Purpose |
115
+ | --- | --- |
116
+ | `ggraft pull [version]` | fetch bases; `--force` refetches, `-j` sets parallelism. Pulling another version replaces the base |
117
+ | `ggraft build` | apply patches; `--check` dry run, `-t` filters by glob |
118
+ | `ggraft targets` | show which patches hit which targets |
119
+ | `ggraft init [version]` | write a starter config; `--force` overwrites |
120
+
121
+ `-h`/`--help` works on the group and every command, `-V`/`--version` prints the version, and `-c`/`--config` points at a specific `ggraft.toml` instead of searching upwards. `pull --token` also reads `GITHUB_TOKEN`.
122
+
123
+ ### Build guarantees
124
+
125
+ - `--check` dry run without output writes.
126
+ - A target pattern matching no base file fails the build. What a patch orphaned by a Minecraft update looks like.
127
+ - A patch applying to nothing fails the build, so a mistake in `targets` or `exclude` cannot disable a patch silently.
128
+ - Stale output is pruned. Shaders ggraft wrote are recorded in `.ggraft/manifest.json`, and a target dropped from a patch has its override deleted on the next build. Only manifest entries are ever deleted; `-t` disables pruning, since a partial build cannot tell what is stale.
129
+
130
+ ## Bases
131
+
132
+ Uses [misode/mcmeta](https://github.com/misode/mcmeta).
133
+ Tree walk might be rate limited. Set `GITHUB_TOKEN` or pass `--token` to bypass the limit.
134
+
135
+ A pull downloads into a sibling `.partial` directory and swaps it in, so an interrupted pull leaves the existing base untouched and a completed one leaves nothing of it behind: `base.dir` is replaced wholesale, never merged into.
136
+
137
+ What the directory holds — version, repo and source — is recorded in `.ggraft/base.json`. Pulling a different version, repo or source therefore refetches instead of reporting the base as already present, and files from the old version cannot linger as targets for the next build. A base ggraft did not record is treated as stale and refetched too. `--force` refetches regardless.
138
+
139
+ ## Tests
140
+
141
+ ```bash
142
+ python -m unittest discover -s tests # everything
143
+ python -m unittest tests.test_cli # one module
144
+ ```
145
+
146
+ One module per layer: `test_structure`, `test_glsl`, `test_anchor`, `test_operations`, `test_engine`, `test_sources`, `test_cli`. Shared shaders, project files and a network-free mcmeta client live in `tests/fixtures.py`.
147
+
148
+ ## Planned Features
149
+ - [ ] functions injections
150
+ - [ ] python module API
151
+
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "setuptools-scm[toml]>=8", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ggraft"
7
+ description = "GLSL Graft - declarative injection patches for vanilla Minecraft shaders"
8
+ readme = "README.md"
9
+ requires-python = ">=3.11"
10
+ license = "MIT"
11
+ authors = [{ name = "Neylz" }]
12
+ keywords = ["minecraft", "glsl", "shaders", "resourcepack", "patching"]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "Topic :: Software Development :: Build Tools",
16
+ ]
17
+ dependencies = ["click>=8.1"]
18
+
19
+ dynamic = ["version"]
20
+
21
+ [project.scripts]
22
+ ggraft = "ggraft.cli:main"
23
+
24
+ [tool.setuptools.packages.find]
25
+ where = ["src"]
26
+
27
+ [tool.setuptools_scm]
28
+ tag_regex = "^(?P<version>\\d+\\.\\d+\\.\\d+)$"
29
+ version_scheme = "no-guess-dev"
30
+ local_scheme = "no-local-version"
31
+ fallback_version = "0.0.0"
ggraft-1.0.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ try:
4
+ __version__ = version("ggraft")
5
+ except PackageNotFoundError: # a source tree that was never installed
6
+ __version__ = "0.0.0"
@@ -0,0 +1,4 @@
1
+ from ggraft.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,3 @@
1
+ from ggraft.cli.main import cli, main
2
+
3
+ __all__ = ["cli", "main"]
@@ -0,0 +1,14 @@
1
+ from ggraft.cli.commands.build import build
2
+ from ggraft.cli.commands.init import init
3
+ from ggraft.cli.commands.pull import pull
4
+ from ggraft.cli.commands.targets import targets
5
+
6
+ COMMANDS = [init, pull, build, targets]
7
+
8
+ __all__ = [
9
+ "COMMANDS",
10
+ "build",
11
+ "init",
12
+ "pull",
13
+ "targets"
14
+ ]
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ import click
4
+
5
+ from ggraft.cli.commands.common import load_config
6
+ from ggraft.errors import TargetError
7
+ from ggraft.patching import engine, patch as patch_module
8
+
9
+
10
+ @click.command()
11
+ @click.option("-t", "--target", "targets", multiple=True, metavar="GLOB",
12
+ help="only build targets matching this glob (repeatable)")
13
+ @click.option("--check", is_flag=True,
14
+ help="do not write; exit non-zero if output would change")
15
+ @click.pass_obj
16
+ def build(config_path: str | None, targets: tuple[str, ...], check: bool) -> None:
17
+ """Apply patches to the base."""
18
+ cfg = load_config(config_path)
19
+ patches = patch_module.load_all(cfg.patch_dir)
20
+ plans = engine.plan(cfg, patches, only=list(targets) or None)
21
+ if not plans:
22
+ raise TargetError("no targets matched any patch")
23
+
24
+ # A filtered build cannot tell what is stale, so it never prunes.
25
+ report = engine.run(cfg, plans, write=not check, prune=not targets)
26
+ changed = [r for r in report.results if r.changed]
27
+
28
+ for result in report.results:
29
+ mark = "*" if result.changed else " "
30
+ click.echo(f" {mark} {result.target:<38} {result.injections:>2} injection(s)"
31
+ f" [{', '.join(result.patches)}]")
32
+ for target in report.pruned:
33
+ click.echo(f" - {target:<38} removed (no longer targeted)")
34
+
35
+ verb = "would patch" if check else "patched"
36
+ summary = f"{verb} {len(report.results)} shader(s), {len(changed)} changed"
37
+ if report.pruned:
38
+ summary += f", {len(report.pruned)} pruned"
39
+ click.echo("\n" + summary)
40
+
41
+ if check and changed:
42
+ raise SystemExit(1)
@@ -0,0 +1,10 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from ggraft import config as config_module
6
+ from ggraft.config import Config
7
+
8
+
9
+ def load_config(config_path: str | None) -> Config:
10
+ return config_module.load(Path(config_path).resolve() if config_path else None)
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import click
6
+
7
+ from ggraft.config import CONFIG_NAME
8
+
9
+ STARTER = '''# ggraft project configuration
10
+ [base]
11
+ version = "{version}"
12
+ dir = ".ggraft/base"
13
+ source = "assets/minecraft/shaders"
14
+
15
+ [patches]
16
+ dir = "patches"
17
+
18
+ [output]
19
+ dir = "assets/minecraft/shaders"
20
+ header = true
21
+ '''
22
+
23
+
24
+ @click.command()
25
+ @click.argument("version", required=False)
26
+ @click.option("--force", is_flag=True, help="overwrite an existing file")
27
+ def init(version: str | None, force: bool) -> None:
28
+ """Write a starter ggraft.toml.
29
+
30
+ VERSION is recorded as base.version.
31
+ """
32
+ destination = Path.cwd() / CONFIG_NAME
33
+ if destination.exists() and not force:
34
+ raise click.ClickException(
35
+ f"{CONFIG_NAME} already exists here (use --force to overwrite)"
36
+ )
37
+ destination.write_text(STARTER.format(version=version or "26.3"), encoding="utf-8")
38
+ click.echo(f"wrote {destination}")
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import click
7
+
8
+ from ggraft.cli.commands.common import load_config
9
+ from ggraft.config import Config
10
+ from ggraft.sources import McMeta
11
+
12
+
13
+ def _stamp(config: Config, version: str) -> dict[str, str]:
14
+ return {"version": version, "repo": config.repo, "source": config.source}
15
+
16
+
17
+ def _read_stamp(path: Path) -> dict[str, str] | None:
18
+ try:
19
+ data = json.loads(path.read_text(encoding="utf-8"))
20
+ except (OSError, ValueError):
21
+ return None # unknown contents are stale contents
22
+ return data if isinstance(data, dict) else None
23
+
24
+
25
+ def _write_stamp(path: Path, stamp: dict[str, str]) -> None:
26
+ path.parent.mkdir(parents=True, exist_ok=True)
27
+ path.write_text(json.dumps(stamp, indent=2) + "\n", encoding="utf-8")
28
+
29
+
30
+ @click.command()
31
+ @click.argument("version", required=False)
32
+ @click.option("--force", is_flag=True, help="refetch even if a base exists")
33
+ @click.option("--token", envvar="GITHUB_TOKEN", help="GitHub token [env: GITHUB_TOKEN]")
34
+ @click.option("-j", "--jobs", type=click.IntRange(min=1), default=8,
35
+ show_default=True, help="parallel downloads")
36
+ @click.pass_obj
37
+ def pull(config_path: str | None, version: str | None, force: bool,
38
+ token: str | None, jobs: int) -> None:
39
+ """Fetch vanilla bases from mcmeta.
40
+
41
+ VERSION defaults to base.version in ggraft.toml.
42
+ """
43
+ cfg = load_config(config_path)
44
+ version = version or cfg.require_version()
45
+ client = McMeta(cfg.repo, token=token)
46
+ tag = client.tag_for(version)
47
+
48
+ wanted = _stamp(cfg, version)
49
+ held = _read_stamp(cfg.base_state)
50
+ present = cfg.base_dir.is_dir() and any(cfg.base_dir.iterdir())
51
+
52
+ if present and held == wanted and not force:
53
+ click.echo(f"base already present at {cfg.base_dir} (use --force to refetch)")
54
+ return
55
+ if present and held != wanted:
56
+ was = held.get("version") if held else None
57
+ click.echo(f"{cfg.base_dir} holds {was or 'an unrecorded base'}; replacing it")
58
+
59
+ click.echo(f"listing {cfg.source} at {tag} ...")
60
+ blobs = client.list_blobs(tag, cfg.source)
61
+ click.echo(f"fetching {len(blobs)} file(s) into {cfg.base_dir} ...")
62
+
63
+ client.download(tag, cfg.source, blobs, cfg.base_dir, workers=jobs)
64
+ _write_stamp(cfg.base_state, wanted)
65
+ click.echo(f"pulled {len(blobs)} file(s) from {cfg.repo}@{tag}")
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ import click
4
+
5
+ from ggraft.cli.commands.common import load_config
6
+ from ggraft.patching import engine, patch as patch_module
7
+
8
+
9
+ @click.command()
10
+ @click.option("-t", "--target", "filters", multiple=True, metavar="GLOB",
11
+ help="filter by glob (repeatable)")
12
+ @click.pass_obj
13
+ def targets(config_path: str | None, filters: tuple[str, ...]) -> None:
14
+ """Show which patches hit which targets."""
15
+ cfg = load_config(config_path)
16
+ patches = patch_module.load_all(cfg.patch_dir)
17
+
18
+ click.echo(f"{len(patches)} patch(es) in {cfg.patch_dir}:")
19
+ for item in patches:
20
+ click.echo(f" {item.name:<24} {len(item.injections)} injection(s)"
21
+ f" <- {item.path.name}")
22
+ click.echo()
23
+ for item in engine.plan(cfg, patches, only=list(filters) or None):
24
+ click.echo(f" {item.target:<38} {', '.join(item.patch_names)}")