onescad 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.
Files changed (123) hide show
  1. onescad-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +32 -0
  2. onescad-0.1.0/.github/ISSUE_TEMPLATE/config.yml +1 -0
  3. onescad-0.1.0/.github/ISSUE_TEMPLATE/feature_request.yml +18 -0
  4. onescad-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +6 -0
  5. onescad-0.1.0/.github/dependabot.yml +16 -0
  6. onescad-0.1.0/.github/workflows/check.yml +62 -0
  7. onescad-0.1.0/.github/workflows/publish.yml +31 -0
  8. onescad-0.1.0/.gitignore +10 -0
  9. onescad-0.1.0/.gitmodules +3 -0
  10. onescad-0.1.0/CHANGELOG.md +7 -0
  11. onescad-0.1.0/CONTRIBUTING.md +54 -0
  12. onescad-0.1.0/LICENSE +21 -0
  13. onescad-0.1.0/PKG-INFO +113 -0
  14. onescad-0.1.0/README.md +104 -0
  15. onescad-0.1.0/SECURITY.md +14 -0
  16. onescad-0.1.0/pyproject.toml +46 -0
  17. onescad-0.1.0/scripts/check-release.sh +82 -0
  18. onescad-0.1.0/scripts/release.sh +290 -0
  19. onescad-0.1.0/src/onescad/__init__.py +1 -0
  20. onescad-0.1.0/src/onescad/cli.py +133 -0
  21. onescad-0.1.0/src/onescad/customizer.py +249 -0
  22. onescad-0.1.0/src/onescad/emit.py +293 -0
  23. onescad-0.1.0/src/onescad/errors.py +5 -0
  24. onescad-0.1.0/src/onescad/flatten.py +164 -0
  25. onescad-0.1.0/src/onescad/header.py +109 -0
  26. onescad-0.1.0/src/onescad/lexer.py +102 -0
  27. onescad-0.1.0/src/onescad/licenses.py +50 -0
  28. onescad-0.1.0/src/onescad/loader.py +168 -0
  29. onescad-0.1.0/src/onescad/parser.py +409 -0
  30. onescad-0.1.0/src/onescad/paths.py +28 -0
  31. onescad-0.1.0/src/onescad/presets.py +38 -0
  32. onescad-0.1.0/src/onescad/rename.py +174 -0
  33. onescad-0.1.0/src/onescad/resolver.py +366 -0
  34. onescad-0.1.0/src/onescad/shake.py +59 -0
  35. onescad-0.1.0/src/onescad/syntax.py +225 -0
  36. onescad-0.1.0/src/onescad/verify.py +105 -0
  37. onescad-0.1.0/tests/fixtures/bosl2_std/expected.scad +19013 -0
  38. onescad-0.1.0/tests/fixtures/bosl2_std/libs +1 -0
  39. onescad-0.1.0/tests/fixtures/bosl2_std/main.scad +4 -0
  40. onescad-0.1.0/tests/fixtures/call_capture/expected.scad +16 -0
  41. onescad-0.1.0/tests/fixtures/call_capture/main.scad +6 -0
  42. onescad-0.1.0/tests/fixtures/customizer_edges/expected.scad +40 -0
  43. onescad-0.1.0/tests/fixtures/customizer_edges/main.scad +27 -0
  44. onescad-0.1.0/tests/fixtures/customizer_reassign_after_cutoff/expected.scad +10 -0
  45. onescad-0.1.0/tests/fixtures/customizer_reassign_after_cutoff/main.scad +4 -0
  46. onescad-0.1.0/tests/fixtures/cycle/a.scad +1 -0
  47. onescad-0.1.0/tests/fixtures/cycle/error.txt +1 -0
  48. onescad-0.1.0/tests/fixtures/cycle/main.scad +1 -0
  49. onescad-0.1.0/tests/fixtures/default_args/expected.scad +21 -0
  50. onescad-0.1.0/tests/fixtures/default_args/lib.scad +2 -0
  51. onescad-0.1.0/tests/fixtures/default_args/main.scad +6 -0
  52. onescad-0.1.0/tests/fixtures/dollar_in_used/error.txt +1 -0
  53. onescad-0.1.0/tests/fixtures/dollar_in_used/lib.scad +2 -0
  54. onescad-0.1.0/tests/fixtures/dollar_in_used/main.scad +2 -0
  55. onescad-0.1.0/tests/fixtures/echo_function_literal/a.scad +1 -0
  56. onescad-0.1.0/tests/fixtures/echo_function_literal/b.scad +2 -0
  57. onescad-0.1.0/tests/fixtures/echo_function_literal/expected.scad +16 -0
  58. onescad-0.1.0/tests/fixtures/echo_function_literal/main.scad +3 -0
  59. onescad-0.1.0/tests/fixtures/external_import/expected.scad +8 -0
  60. onescad-0.1.0/tests/fixtures/external_import/main.scad +1 -0
  61. onescad-0.1.0/tests/fixtures/external_import/noverify +1 -0
  62. onescad-0.1.0/tests/fixtures/external_import/warning.txt +1 -0
  63. onescad-0.1.0/tests/fixtures/header_blank_line/expected.scad +12 -0
  64. onescad-0.1.0/tests/fixtures/header_blank_line/main.scad +3 -0
  65. onescad-0.1.0/tests/fixtures/hoist_conflict/error.txt +1 -0
  66. onescad-0.1.0/tests/fixtures/hoist_conflict/main.scad +3 -0
  67. onescad-0.1.0/tests/fixtures/include_in_module/body.scad +2 -0
  68. onescad-0.1.0/tests/fixtures/include_in_module/expected.scad +13 -0
  69. onescad-0.1.0/tests/fixtures/include_in_module/main.scad +5 -0
  70. onescad-0.1.0/tests/fixtures/include_repeat/defs.scad +3 -0
  71. onescad-0.1.0/tests/fixtures/include_repeat/expected.scad +16 -0
  72. onescad-0.1.0/tests/fixtures/include_repeat/main.scad +3 -0
  73. onescad-0.1.0/tests/fixtures/license/expected.scad +18 -0
  74. onescad-0.1.0/tests/fixtures/license/main.scad +3 -0
  75. onescad-0.1.0/tests/fixtures/license/vendor/LICENSE +3 -0
  76. onescad-0.1.0/tests/fixtures/license/vendor/lib.scad +2 -0
  77. onescad-0.1.0/tests/fixtures/local_lookup/expected.scad +30 -0
  78. onescad-0.1.0/tests/fixtures/local_lookup/main.scad +18 -0
  79. onescad-0.1.0/tests/fixtures/missing_file/error.txt +1 -0
  80. onescad-0.1.0/tests/fixtures/missing_file/main.scad +1 -0
  81. onescad-0.1.0/tests/fixtures/presets/expected.scad +11 -0
  82. onescad-0.1.0/tests/fixtures/presets/main.json +1 -0
  83. onescad-0.1.0/tests/fixtures/presets/main.scad +2 -0
  84. onescad-0.1.0/tests/fixtures/reassign/expected.scad +20 -0
  85. onescad-0.1.0/tests/fixtures/reassign/main.scad +11 -0
  86. onescad-0.1.0/tests/fixtures/shadow_builtin/a.scad +1 -0
  87. onescad-0.1.0/tests/fixtures/shadow_builtin/b.scad +4 -0
  88. onescad-0.1.0/tests/fixtures/shadow_builtin/expected.scad +18 -0
  89. onescad-0.1.0/tests/fixtures/shadow_builtin/main.scad +3 -0
  90. onescad-0.1.0/tests/fixtures/shake/expected.scad +12 -0
  91. onescad-0.1.0/tests/fixtures/shake/lib.scad +5 -0
  92. onescad-0.1.0/tests/fixtures/shake/main.scad +2 -0
  93. onescad-0.1.0/tests/fixtures/use_basic/expected.scad +21 -0
  94. onescad-0.1.0/tests/fixtures/use_basic/lib.scad +3 -0
  95. onescad-0.1.0/tests/fixtures/use_basic/main.scad +7 -0
  96. onescad-0.1.0/tests/fixtures/use_basic/mid.scad +2 -0
  97. onescad-0.1.0/tests/fixtures/use_in_module/error.txt +1 -0
  98. onescad-0.1.0/tests/fixtures/use_in_module/main.scad +1 -0
  99. onescad-0.1.0/tests/fixtures/use_last_wins/a.scad +2 -0
  100. onescad-0.1.0/tests/fixtures/use_last_wins/b.scad +1 -0
  101. onescad-0.1.0/tests/fixtures/use_last_wins/expected.scad +12 -0
  102. onescad-0.1.0/tests/fixtures/use_last_wins/main.scad +4 -0
  103. onescad-0.1.0/tests/fixtures/use_placement/a.scad +2 -0
  104. onescad-0.1.0/tests/fixtures/use_placement/b.scad +1 -0
  105. onescad-0.1.0/tests/fixtures/use_placement/expected.scad +16 -0
  106. onescad-0.1.0/tests/fixtures/use_placement/main.scad +6 -0
  107. onescad-0.1.0/tests/helpers.py +59 -0
  108. onescad-0.1.0/tests/test_cli.py +178 -0
  109. onescad-0.1.0/tests/test_customizer.py +588 -0
  110. onescad-0.1.0/tests/test_emit.py +198 -0
  111. onescad-0.1.0/tests/test_flatten.py +106 -0
  112. onescad-0.1.0/tests/test_header.py +145 -0
  113. onescad-0.1.0/tests/test_lexer.py +111 -0
  114. onescad-0.1.0/tests/test_loader.py +240 -0
  115. onescad-0.1.0/tests/test_oracle.py +105 -0
  116. onescad-0.1.0/tests/test_parser.py +247 -0
  117. onescad-0.1.0/tests/test_presets.py +52 -0
  118. onescad-0.1.0/tests/test_rename.py +95 -0
  119. onescad-0.1.0/tests/test_resolver.py +268 -0
  120. onescad-0.1.0/tests/test_shake.py +87 -0
  121. onescad-0.1.0/tests/test_verify.py +60 -0
  122. onescad-0.1.0/tests/test_version.py +9 -0
  123. onescad-0.1.0/uv.lock +570 -0
@@ -0,0 +1,32 @@
1
+ name: Bug report
2
+ description: onescad fails on a model or produces a bundle that renders differently.
3
+ labels: [bug]
4
+ body:
5
+ - type: textarea
6
+ id: what-happened
7
+ attributes:
8
+ label: What happened
9
+ description: The command you ran, what you expected, and what you got instead.
10
+ validations:
11
+ required: true
12
+ - type: input
13
+ id: version
14
+ attributes:
15
+ label: onescad version
16
+ description: Output of `onescad --version`.
17
+ validations:
18
+ required: true
19
+ - type: input
20
+ id: openscad
21
+ attributes:
22
+ label: OpenSCAD version
23
+ description: Output of `openscad --version`, if the bug involves rendering or `--verify`.
24
+ validations:
25
+ required: false
26
+ - type: textarea
27
+ id: repro
28
+ attributes:
29
+ label: Minimal reproduction
30
+ description: A small .scad model that triggers the bug, if you can share one.
31
+ validations:
32
+ required: false
@@ -0,0 +1 @@
1
+ blank_issues_enabled: false
@@ -0,0 +1,18 @@
1
+ name: Feature request
2
+ description: Propose a change to onescad's behavior or CLI.
3
+ labels: [enhancement]
4
+ body:
5
+ - type: textarea
6
+ id: problem
7
+ attributes:
8
+ label: Problem
9
+ description: What you are trying to do that onescad does not support today.
10
+ validations:
11
+ required: true
12
+ - type: textarea
13
+ id: proposal
14
+ attributes:
15
+ label: Proposed change
16
+ description: What you would like to see, and why it fits bundling one model into one file.
17
+ validations:
18
+ required: true
@@ -0,0 +1,6 @@
1
+ ## Checklist
2
+
3
+ - [ ] `uv run ruff check` and `uv run ruff format --check` pass
4
+ - [ ] `uv run mypy` and `uv run pytest` pass
5
+ - [ ] A fixture or test covers the behavior this PR changes
6
+ - [ ] `uv run pytest -m oracle --no-cov` passes if the bundle's output could change
@@ -0,0 +1,16 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: github-actions
4
+ directory: /
5
+ schedule:
6
+ interval: weekly
7
+
8
+ - package-ecosystem: uv
9
+ directory: /
10
+ schedule:
11
+ interval: weekly
12
+
13
+ - package-ecosystem: gitsubmodule
14
+ directory: /
15
+ schedule:
16
+ interval: weekly
@@ -0,0 +1,62 @@
1
+ name: check
2
+ on:
3
+ push:
4
+ pull_request:
5
+ permissions:
6
+ contents: read
7
+ jobs:
8
+ check:
9
+ strategy:
10
+ matrix:
11
+ os: [ubuntu-latest, macos-latest]
12
+ python-version: ['3.11', '3.14']
13
+ runs-on: ${{ matrix.os }}
14
+ steps:
15
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
16
+ with:
17
+ persist-credentials: false
18
+ submodules: true
19
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
20
+ - run: uv sync --locked --python ${{ matrix.python-version }}
21
+ - run: uv run ruff check
22
+ - run: uv run ruff format --check
23
+ - run: uv run mypy
24
+ - run: uv run pytest
25
+
26
+ oracle:
27
+ runs-on: ubuntu-latest
28
+ env:
29
+ OPENSCAD_VERSION: '2026.09.23'
30
+ OPENSCAD_SHA256: 033762f4e0b0de7a2c6cf2b5a12febb29d60ba62ede227d57b1c8b675a94710a
31
+ APPIMAGE_EXTRACT_AND_RUN: '1'
32
+ QT_QPA_PLATFORM: offscreen
33
+ steps:
34
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
35
+ with:
36
+ persist-credentials: false
37
+ submodules: true
38
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
39
+ - name: Install OpenSCAD
40
+ run: |
41
+ sudo apt-get update
42
+ sudo apt-get install -y --no-install-recommends libegl1 libopengl0 libgl1 libglu1-mesa libxkbcommon0 libfontconfig1 libdbus-1-3
43
+ curl -fsSL -o openscad "https://files.openscad.org/snapshots/OpenSCAD-${OPENSCAD_VERSION}-x86_64.AppImage"
44
+ echo "${OPENSCAD_SHA256} openscad" | sha256sum -c -
45
+ chmod +x openscad
46
+ mkdir -p "$HOME/.local/bin"
47
+ mv openscad "$HOME/.local/bin/openscad"
48
+ echo "$HOME/.local/bin" >> "$GITHUB_PATH"
49
+ - run: openscad --version
50
+ - run: uv sync --locked
51
+ - run: uv run pytest -m oracle --no-cov
52
+
53
+ release-check:
54
+ runs-on: ubuntu-latest
55
+ steps:
56
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
57
+ with:
58
+ persist-credentials: false
59
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
60
+ - run: uv build
61
+ - run: uvx twine check dist/*
62
+ - run: uvx zizmor==1.30.1 .github/workflows
@@ -0,0 +1,31 @@
1
+ name: publish
2
+ on:
3
+ release:
4
+ types: [published]
5
+ permissions:
6
+ contents: read
7
+ jobs:
8
+ verify:
9
+ runs-on: ubuntu-latest
10
+ env:
11
+ RELEASE_TAG: ${{ github.event.release.tag_name }}
12
+ RELEASE_NAME: ${{ github.event.release.name }}
13
+ steps:
14
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
15
+ with:
16
+ persist-credentials: false
17
+ - run: sh scripts/check-release.sh "$RELEASE_TAG" "$RELEASE_NAME"
18
+ publish:
19
+ needs: verify
20
+ runs-on: ubuntu-latest
21
+ permissions:
22
+ id-token: write
23
+ steps:
24
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
25
+ with:
26
+ persist-credentials: false
27
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
28
+ with:
29
+ enable-cache: false
30
+ - run: uv build
31
+ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .coverage
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
@@ -0,0 +1,3 @@
1
+ [submodule "tests/corpus/BOSL2"]
2
+ path = tests/corpus/BOSL2
3
+ url = https://github.com/BelfrySCAD/BOSL2
@@ -0,0 +1,7 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ First release. `onescad INPUT -o OUTPUT [-L DIR]... [--verify]` bundles an OpenSCAD model and
6
+ everything it includes or uses into one `.scad` file that keeps the Customizer working, copies a
7
+ neighboring presets file, and embeds each contributing license.
@@ -0,0 +1,54 @@
1
+ # Contributing
2
+
3
+ ## Setup and checks
4
+
5
+ ```sh
6
+ git submodule update --init
7
+ uv sync --locked
8
+ uv run ruff check
9
+ uv run ruff format --check
10
+ uv run mypy
11
+ uv run pytest
12
+ ```
13
+
14
+ `tests/corpus/BOSL2` is a submodule pinned to the BOSL2 revision the parser tests run against.
15
+ `pytest` fails under 90% coverage.
16
+
17
+ The oracle tests render the source and the bundle with a real `openscad` and skip without one.
18
+ CI runs them against a pinned OpenSCAD 2026.09.23 AppImage. Locally:
19
+
20
+ ```sh
21
+ uv run pytest -m oracle --no-cov
22
+ ```
23
+
24
+ Each fixture under `tests/fixtures/<case>/` holds a `main.scad` and a golden `expected.scad`,
25
+ or an `error.txt` for a case that must fail. Regenerate goldens with
26
+ `ONESCAD_UPDATE_GOLDEN=1`. A defect found in review is fixed with a fixture or test for its
27
+ class, not only its instance.
28
+
29
+ CI also runs `uv build`, `twine check`, and `zizmor` over the workflows.
30
+
31
+ ## Releasing
32
+
33
+ ```sh
34
+ sh scripts/release.sh [--dry-run] <version>
35
+ ```
36
+
37
+ `<version>` is bare (`0.1.0`, not `v0.1.0`). Run it on a clean, synced `main` with `gh` and `uv`
38
+ installed. The script runs the same checks CI runs, opens a `release-<version>` PR that sets the
39
+ version in `pyproject.toml`, `src/onescad/__init__.py`, and `uv.lock`, waits for its checks, and
40
+ squash-merges it. GitHub signs the squash commit, so the release shows as Verified. It then
41
+ tags the merged commit and creates the GitHub release from the matching `CHANGELOG.md` section;
42
+ add a `## <version>` heading there first. If the bump is already on `main`, it skips the PR and
43
+ only tags and releases, so a run that stopped after the merge can be repeated. `--dry-run` runs
44
+ every check and prints each mutating command instead of running it.
45
+
46
+ The release event triggers `publish.yml`, which re-runs `scripts/check-release.sh` (the tag, its
47
+ title, and the `CHANGELOG.md` heading must all agree) before building and publishing to PyPI
48
+ through a trusted publisher.
49
+
50
+ ## Scope
51
+
52
+ onescad is a single-maintainer project with no runtime dependencies. It bundles one model into
53
+ one file and checks the result. PRs that add a runtime dependency or reach past that are
54
+ declined. Bug reports and fixes are welcome.
onescad-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kamil Jiwa
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.
onescad-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.5
2
+ Name: onescad
3
+ Version: 0.1.0
4
+ Summary: Bundle an OpenSCAD model into one customizer-ready file
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+
10
+ # onescad
11
+
12
+ Bundle an OpenSCAD model into one self-contained, Customizer-ready `.scad` file.
13
+
14
+ ```sh
15
+ uvx onescad model.scad -o dist/model.scad
16
+ ```
17
+
18
+ The bundle renders identically to the source, keeps the Customizer's parameters, groups, and
19
+ widgets, and carries the license text of every file it takes code from. Python 3.11 or newer;
20
+ no runtime dependencies.
21
+
22
+ ## Usage
23
+
24
+ ```
25
+ onescad INPUT -o OUTPUT [-L DIR]... [--verify] [--version]
26
+ ```
27
+
28
+ | Option | Meaning |
29
+ | --- | --- |
30
+ | `INPUT` | The model's `.scad` file. |
31
+ | `-o`, `--output` | The bundle to write. Parent directories are created. |
32
+ | `-L`, `--library DIR` | A directory to search for `include` and `use`; repeatable. |
33
+ | `--verify` | Render the source and the bundle with `openscad` and compare them. |
34
+ | `--version` | Print the version. |
35
+
36
+ Warnings print to stderr as `onescad: warning: <message>`. Errors print as `onescad: <message>`.
37
+
38
+ | Exit code | Meaning |
39
+ | --- | --- |
40
+ | 0 | The bundle was written (and verified, with `--verify`). |
41
+ | 1 | The bundle could not be built or verified. |
42
+ | 2 | Usage error. |
43
+
44
+ ## Search path
45
+
46
+ `include` and `use` are resolved in this order: the including file's directory, each `-L`
47
+ directory, then each `OPENSCADPATH` entry. OpenSCAD also searches the user and built-in library
48
+ directories; onescad does not, so a bundle is the same on every machine. Put those libraries on
49
+ `-L` or `OPENSCADPATH`. A missing file is an error that lists the directories searched.
50
+
51
+ ## What gets bundled
52
+
53
+ The output has these parts, in order:
54
+
55
+ 1. A header of `//` comments:
56
+ - the onescad version, marked as generated;
57
+ - the source path relative to its git root, the remote as an https URL, and the commit, with
58
+ `+dirty` when tracked files have uncommitted changes (omitted outside a git work tree with a
59
+ commit);
60
+ - the main file's leading comment;
61
+ - the fonts named by `font=` strings, the files read by `import()` or `surface()`, and a note
62
+ naming the presets file, when they apply;
63
+ - for each license file covering code that was kept, its directory and its text verbatim. The
64
+ license file is the nearest `LICENSE*`, `LICENCE*`, or `COPYING*` in an ancestor directory of
65
+ each source file.
66
+ 2. The Customizer parameters, hoisted to the top.
67
+ 3. A `/* [Hidden] */` marker, so nothing after it appears in the Customizer.
68
+ 4. Each used file's definitions, dependencies first, each preceded by a `// onescad: <path>` marker
69
+ and the file's leading comment.
70
+ 5. The main file, with its included files spliced in.
71
+
72
+ The output has no timestamps, so the same input gives byte-identical output.
73
+
74
+ Definitions that the model never reaches are dropped. A definition is renamed (`name__1`) when
75
+ keeping its name would capture a different reference, as when a library wraps a builtin it later
76
+ overrides.
77
+
78
+ ## Presets
79
+
80
+ If `model.json` sits next to `model.scad`, onescad copies it verbatim to the output's name with a
81
+ `.json` extension (`dist/model.json` for `-o dist/model.scad`). OpenSCAD loads presets only from a
82
+ `.json` beside the `.scad`, so keep the two together. A warning names each preset key that is not a
83
+ Customizer parameter of the bundle.
84
+
85
+ ## Verification
86
+
87
+ `--verify` needs `openscad` on `PATH`. It compares the source with the bundle on:
88
+
89
+ - the CSG of the default parameters and of every preset;
90
+ - the `ECHO` lines, after undoing renames;
91
+ - the Customizer parameter set (`-o x.param`, ignoring `title`).
92
+
93
+ The bundle renders with `OPENSCADPATH` unset and an empty `HOME`, which proves it is
94
+ self-contained. `text()` fonts are not compared. Verifying needs an OpenSCAD that can export
95
+ `param` (the 2026.09 snapshots do; 2021.01 is untested).
96
+
97
+ ## Limitations
98
+
99
+ - The Customizer is emulated, not run. A model whose parameter assignment reads a name before
100
+ that name's first assignment cannot be hoisted; onescad stops with an error.
101
+ - A top-level `$` variable in a used file, conflicting definitions that cannot be renamed, and a
102
+ rename that would split a call's possible targets are errors.
103
+ - A top-level variable in a used file is evaluated once in the bundle, not on every call as
104
+ OpenSCAD does; onescad warns.
105
+ - `import()` and `surface()` still read their files at render time; onescad warns and lists them
106
+ in the header. Relative paths then resolve against wherever the bundle is rendered.
107
+ - Fonts are not embedded; the header lists those that `font=` names.
108
+ - A model's license text appears in the header only when a license file covers its source.
109
+ onescad warns about each source file with none.
110
+
111
+ ## License
112
+
113
+ MIT. See `LICENSE`. Bundles carry the licenses of their inputs; check them before redistributing.
@@ -0,0 +1,104 @@
1
+ # onescad
2
+
3
+ Bundle an OpenSCAD model into one self-contained, Customizer-ready `.scad` file.
4
+
5
+ ```sh
6
+ uvx onescad model.scad -o dist/model.scad
7
+ ```
8
+
9
+ The bundle renders identically to the source, keeps the Customizer's parameters, groups, and
10
+ widgets, and carries the license text of every file it takes code from. Python 3.11 or newer;
11
+ no runtime dependencies.
12
+
13
+ ## Usage
14
+
15
+ ```
16
+ onescad INPUT -o OUTPUT [-L DIR]... [--verify] [--version]
17
+ ```
18
+
19
+ | Option | Meaning |
20
+ | --- | --- |
21
+ | `INPUT` | The model's `.scad` file. |
22
+ | `-o`, `--output` | The bundle to write. Parent directories are created. |
23
+ | `-L`, `--library DIR` | A directory to search for `include` and `use`; repeatable. |
24
+ | `--verify` | Render the source and the bundle with `openscad` and compare them. |
25
+ | `--version` | Print the version. |
26
+
27
+ Warnings print to stderr as `onescad: warning: <message>`. Errors print as `onescad: <message>`.
28
+
29
+ | Exit code | Meaning |
30
+ | --- | --- |
31
+ | 0 | The bundle was written (and verified, with `--verify`). |
32
+ | 1 | The bundle could not be built or verified. |
33
+ | 2 | Usage error. |
34
+
35
+ ## Search path
36
+
37
+ `include` and `use` are resolved in this order: the including file's directory, each `-L`
38
+ directory, then each `OPENSCADPATH` entry. OpenSCAD also searches the user and built-in library
39
+ directories; onescad does not, so a bundle is the same on every machine. Put those libraries on
40
+ `-L` or `OPENSCADPATH`. A missing file is an error that lists the directories searched.
41
+
42
+ ## What gets bundled
43
+
44
+ The output has these parts, in order:
45
+
46
+ 1. A header of `//` comments:
47
+ - the onescad version, marked as generated;
48
+ - the source path relative to its git root, the remote as an https URL, and the commit, with
49
+ `+dirty` when tracked files have uncommitted changes (omitted outside a git work tree with a
50
+ commit);
51
+ - the main file's leading comment;
52
+ - the fonts named by `font=` strings, the files read by `import()` or `surface()`, and a note
53
+ naming the presets file, when they apply;
54
+ - for each license file covering code that was kept, its directory and its text verbatim. The
55
+ license file is the nearest `LICENSE*`, `LICENCE*`, or `COPYING*` in an ancestor directory of
56
+ each source file.
57
+ 2. The Customizer parameters, hoisted to the top.
58
+ 3. A `/* [Hidden] */` marker, so nothing after it appears in the Customizer.
59
+ 4. Each used file's definitions, dependencies first, each preceded by a `// onescad: <path>` marker
60
+ and the file's leading comment.
61
+ 5. The main file, with its included files spliced in.
62
+
63
+ The output has no timestamps, so the same input gives byte-identical output.
64
+
65
+ Definitions that the model never reaches are dropped. A definition is renamed (`name__1`) when
66
+ keeping its name would capture a different reference, as when a library wraps a builtin it later
67
+ overrides.
68
+
69
+ ## Presets
70
+
71
+ If `model.json` sits next to `model.scad`, onescad copies it verbatim to the output's name with a
72
+ `.json` extension (`dist/model.json` for `-o dist/model.scad`). OpenSCAD loads presets only from a
73
+ `.json` beside the `.scad`, so keep the two together. A warning names each preset key that is not a
74
+ Customizer parameter of the bundle.
75
+
76
+ ## Verification
77
+
78
+ `--verify` needs `openscad` on `PATH`. It compares the source with the bundle on:
79
+
80
+ - the CSG of the default parameters and of every preset;
81
+ - the `ECHO` lines, after undoing renames;
82
+ - the Customizer parameter set (`-o x.param`, ignoring `title`).
83
+
84
+ The bundle renders with `OPENSCADPATH` unset and an empty `HOME`, which proves it is
85
+ self-contained. `text()` fonts are not compared. Verifying needs an OpenSCAD that can export
86
+ `param` (the 2026.09 snapshots do; 2021.01 is untested).
87
+
88
+ ## Limitations
89
+
90
+ - The Customizer is emulated, not run. A model whose parameter assignment reads a name before
91
+ that name's first assignment cannot be hoisted; onescad stops with an error.
92
+ - A top-level `$` variable in a used file, conflicting definitions that cannot be renamed, and a
93
+ rename that would split a call's possible targets are errors.
94
+ - A top-level variable in a used file is evaluated once in the bundle, not on every call as
95
+ OpenSCAD does; onescad warns.
96
+ - `import()` and `surface()` still read their files at render time; onescad warns and lists them
97
+ in the header. Relative paths then resolve against wherever the bundle is rendered.
98
+ - Fonts are not embedded; the header lists those that `font=` names.
99
+ - A model's license text appears in the header only when a license file covers its source.
100
+ onescad warns about each source file with none.
101
+
102
+ ## License
103
+
104
+ MIT. See `LICENSE`. Bundles carry the licenses of their inputs; check them before redistributing.
@@ -0,0 +1,14 @@
1
+ # Security
2
+
3
+ onescad reads the `.scad` files that a model includes or uses, the license files beside them,
4
+ and a neighboring presets `.json`. It runs `git` to read the source's remote and commit, and,
5
+ with `--verify`, `openscad`. It writes the bundle and a copy of the presets file. It makes no
6
+ network requests and sends no telemetry. A bundle embeds the remote URL of the repository it
7
+ was built from.
8
+
9
+ ## Reporting a vulnerability
10
+
11
+ Please use
12
+ [GitHub's private vulnerability reporting](https://github.com/kjiwa/onescad/security/advisories/new)
13
+ for this repository rather than opening a public issue. If that option isn't available yet,
14
+ email kamil.jiwa@gmail.com instead.
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "onescad"
7
+ version = "0.1.0"
8
+ description = "Bundle an OpenSCAD model into one customizer-ready file"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.11"
13
+ dependencies = []
14
+
15
+ [project.scripts]
16
+ onescad = "onescad.cli:main"
17
+
18
+ [dependency-groups]
19
+ dev = ["pytest", "pytest-cov", "mypy", "ruff"]
20
+
21
+ [tool.hatch.build.targets.wheel]
22
+ packages = ["src/onescad"]
23
+
24
+ [tool.ruff]
25
+ line-length = 100
26
+ src = ["src", "tests"]
27
+ extend-exclude = ["tests/corpus"]
28
+
29
+ [tool.ruff.lint]
30
+ select = ["E", "F", "I", "UP", "B", "S", "SIM", "RUF"]
31
+
32
+ [tool.ruff.lint.per-file-ignores]
33
+ "tests/**" = ["S101"]
34
+
35
+ [tool.mypy]
36
+ strict = true
37
+ files = ["src", "tests"]
38
+ exclude = ["tests/corpus/"]
39
+
40
+ [tool.pytest.ini_options]
41
+ testpaths = ["tests"]
42
+ addopts = "--cov=onescad --cov-report=term-missing"
43
+ markers = ["oracle: compares against a real openscad run"]
44
+
45
+ [tool.coverage.report]
46
+ fail_under = 90
@@ -0,0 +1,82 @@
1
+ #!/bin/sh
2
+ # Check that a release's tag and title match the version in pyproject.toml
3
+ # and that CHANGELOG.md has a matching heading, so a hand-cut release
4
+ # cannot drift the way v0.1.7 did.
5
+ #
6
+ # Usage: sh scripts/check-release.sh <tag> <title>
7
+ # sh scripts/check-release.sh --changelog <tag>
8
+ #
9
+ # --changelog checks only the CHANGELOG heading, for use before the version
10
+ # bump has landed in pyproject.toml.
11
+ set -eu
12
+
13
+ _check_tag_shape() {
14
+ _check_tag_shape_tag=$1
15
+ if ! echo "$_check_tag_shape_tag" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
16
+ echo "check-release: tag '$_check_tag_shape_tag' does not match vX.Y.Z" >&2
17
+ return 1
18
+ fi
19
+ }
20
+
21
+ _check_tag_matches_version() {
22
+ _check_tag_matches_version_tag=$1
23
+ _check_tag_matches_version_version=$(sed -n 's/^version = "\(.*\)"$/\1/p' "$PYPROJECT" | head -n1)
24
+ if [ -z "$_check_tag_matches_version_version" ]; then
25
+ echo "check-release: could not read version from $PYPROJECT" >&2
26
+ return 1
27
+ fi
28
+ if [ "$_check_tag_matches_version_tag" != "v$_check_tag_matches_version_version" ]; then
29
+ echo "check-release: tag '$_check_tag_matches_version_tag' does not match pyproject.toml version '$_check_tag_matches_version_version'" >&2
30
+ return 1
31
+ fi
32
+ }
33
+
34
+ _check_title_matches_tag() {
35
+ _check_title_matches_tag_tag=$1
36
+ _check_title_matches_tag_title=$2
37
+ if [ "$_check_title_matches_tag_title" != "$_check_title_matches_tag_tag" ]; then
38
+ echo "check-release: title '$_check_title_matches_tag_title' does not match tag '$_check_title_matches_tag_tag'" >&2
39
+ return 1
40
+ fi
41
+ }
42
+
43
+ _check_changelog_heading() {
44
+ _check_changelog_heading_tag=$1
45
+ _check_changelog_heading_version=${_check_changelog_heading_tag#v}
46
+ if ! grep -Fxq "## $_check_changelog_heading_version" "$CHANGELOG"; then
47
+ echo "check-release: CHANGELOG.md has no '## $_check_changelog_heading_version' heading" >&2
48
+ return 1
49
+ fi
50
+ }
51
+
52
+ main() {
53
+ REPO_ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
54
+ readonly REPO_ROOT
55
+ readonly PYPROJECT="$REPO_ROOT/pyproject.toml"
56
+ readonly CHANGELOG="$REPO_ROOT/CHANGELOG.md"
57
+
58
+ if [ $# -eq 2 ] && [ "$1" = "--changelog" ]; then
59
+ _check_changelog_heading "$2"
60
+ exit $?
61
+ fi
62
+ if [ $# -ne 2 ]; then
63
+ echo "usage: sh scripts/check-release.sh <tag> <title>" >&2
64
+ echo " sh scripts/check-release.sh --changelog <tag>" >&2
65
+ exit 2
66
+ fi
67
+
68
+ _main_tag=$1
69
+ _main_title=$2
70
+ _main_status=0
71
+
72
+ _check_tag_shape "$_main_tag" || _main_status=1
73
+ if [ "$_main_status" -eq 0 ]; then
74
+ _check_tag_matches_version "$_main_tag" || _main_status=1
75
+ fi
76
+ _check_title_matches_tag "$_main_tag" "$_main_title" || _main_status=1
77
+ _check_changelog_heading "$_main_tag" || _main_status=1
78
+
79
+ exit "$_main_status"
80
+ }
81
+
82
+ main "$@"