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.
- ggraft-1.0.0/.gitattributes +1 -0
- ggraft-1.0.0/.github/workflows/cd.yml +60 -0
- ggraft-1.0.0/.gitignore +5 -0
- ggraft-1.0.0/.vscode/settings.json +6 -0
- ggraft-1.0.0/PKG-INFO +164 -0
- ggraft-1.0.0/README.md +151 -0
- ggraft-1.0.0/pyproject.toml +31 -0
- ggraft-1.0.0/setup.cfg +4 -0
- ggraft-1.0.0/src/ggraft/__init__.py +6 -0
- ggraft-1.0.0/src/ggraft/__main__.py +4 -0
- ggraft-1.0.0/src/ggraft/cli/__init__.py +3 -0
- ggraft-1.0.0/src/ggraft/cli/commands/__init__.py +14 -0
- ggraft-1.0.0/src/ggraft/cli/commands/build.py +42 -0
- ggraft-1.0.0/src/ggraft/cli/commands/common.py +10 -0
- ggraft-1.0.0/src/ggraft/cli/commands/init.py +38 -0
- ggraft-1.0.0/src/ggraft/cli/commands/pull.py +65 -0
- ggraft-1.0.0/src/ggraft/cli/commands/targets.py +24 -0
- ggraft-1.0.0/src/ggraft/cli/main.py +61 -0
- ggraft-1.0.0/src/ggraft/config.py +87 -0
- ggraft-1.0.0/src/ggraft/errors.py +21 -0
- ggraft-1.0.0/src/ggraft/glsl/__init__.py +17 -0
- ggraft-1.0.0/src/ggraft/glsl/preprocessor.py +67 -0
- ggraft-1.0.0/src/ggraft/patching/__init__.py +15 -0
- ggraft-1.0.0/src/ggraft/patching/anchor.py +80 -0
- ggraft-1.0.0/src/ggraft/patching/engine.py +188 -0
- ggraft-1.0.0/src/ggraft/patching/operations.py +145 -0
- ggraft-1.0.0/src/ggraft/patching/patch.py +93 -0
- ggraft-1.0.0/src/ggraft/sources/__init__.py +3 -0
- ggraft-1.0.0/src/ggraft/sources/mcmeta.py +126 -0
- ggraft-1.0.0/src/ggraft.egg-info/PKG-INFO +164 -0
- ggraft-1.0.0/src/ggraft.egg-info/SOURCES.txt +46 -0
- ggraft-1.0.0/src/ggraft.egg-info/dependency_links.txt +1 -0
- ggraft-1.0.0/src/ggraft.egg-info/entry_points.txt +2 -0
- ggraft-1.0.0/src/ggraft.egg-info/requires.txt +1 -0
- ggraft-1.0.0/src/ggraft.egg-info/scm_file_list.json +42 -0
- ggraft-1.0.0/src/ggraft.egg-info/scm_version.json +8 -0
- ggraft-1.0.0/src/ggraft.egg-info/top_level.txt +1 -0
- ggraft-1.0.0/tests/__init__.py +0 -0
- ggraft-1.0.0/tests/fixtures.py +51 -0
- ggraft-1.0.0/tests/test_anchor.py +81 -0
- ggraft-1.0.0/tests/test_cli.py +212 -0
- ggraft-1.0.0/tests/test_config.py +83 -0
- ggraft-1.0.0/tests/test_engine.py +165 -0
- ggraft-1.0.0/tests/test_glsl.py +43 -0
- ggraft-1.0.0/tests/test_operations.py +108 -0
- ggraft-1.0.0/tests/test_patch.py +101 -0
- ggraft-1.0.0/tests/test_sources.py +167 -0
- 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
|
ggraft-1.0.0/.gitignore
ADDED
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,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)}")
|