Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7d69700
Improve documentation image builds
yawkat Jul 18, 2026
64b283d
Install OpenSCAD EGL runtime in CI
yawkat Jul 18, 2026
fcd48ae
Install AppImage OpenGL runtime in CI
yawkat Jul 18, 2026
eb058e8
Generate puzzle paths before documentation
yawkat Jul 18, 2026
3b8f92f
Upload documentation image diffs
yawkat Jul 18, 2026
6d9d8ea
Regenerate images with pinned OpenSCAD
yawkat Jul 18, 2026
0050e61
Use softpipe for documentation renders
yawkat Jul 18, 2026
cc36a7e
Regenerate images with softpipe
yawkat Jul 18, 2026
63081c0
Normalize isolated render noise
yawkat Jul 18, 2026
bda8ff5
Use OpenSCAD deterministic output mode
yawkat Jul 18, 2026
4530d0c
Regenerate deterministic documentation images
yawkat Jul 18, 2026
d7ff57e
Avoid raster tie in jig documentation image
yawkat Jul 18, 2026
97f6389
Render documentation with deterministic CGAL backend
yawkat Jul 18, 2026
4e93104
Use CGAL for colored jig documentation renders
yawkat Jul 18, 2026
76ea9a8
Make jig documentation renders predictable
yawkat Jul 18, 2026
df0f555
Pin local documentation renderer
yawkat Jul 19, 2026
f60bb73
Canonicalize documentation PNG output
yawkat Jul 19, 2026
9b8497a
Serialize duplicate documentation outputs
yawkat Jul 19, 2026
f526a4c
Give click latch renders unique outputs
yawkat Jul 19, 2026
25a6f4e
Always canonicalize documentation PNGs
yawkat Jul 19, 2026
8a15371
Parallelize documentation canonicalization
yawkat Jul 19, 2026
3432b71
Canonicalize images in worker processes
yawkat Jul 19, 2026
ab7a6d7
Use libraries for canonical PNG encoding
yawkat Jul 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Documentation

on:
push:
branches:
- main
pull_request:
branches:
- main
workflow_dispatch: {}

jobs:
images:
runs-on: ubuntu-latest
env:
GALLIUM_DRIVER: softpipe
LIBGL_ALWAYS_SOFTWARE: "1"
MESA_LOADER_DRIVER_OVERRIDE: softpipe
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
submodules: true

- name: Setup Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.13"

- name: Setup uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2

- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y just libegl1 libgl1-mesa-dri libopengl0
uv sync --frozen

- name: Generate documentation images
run: just docs

- name: Check documentation images
run: |
# Make newly generated, untracked images visible to git diff as well.
git add --intent-to-add docs/images
git diff --exit-code -- docs/images

- name: Upload changed documentation images
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: documentation-images
path: docs/images
retention-days: 1
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ Pull the pusher back into its original position to release the baseplate. You ca
<!-- These images are generated for regression detection purposes. -->
<!-- openscad -o docs/images/jig-main.png --camera=0,0,0,40,0,10,200 -D part='"jig"' mag_insert_jig.scad -->
<!-- openscad -o docs/images/jig-pusher.png --camera=0,0,0,40,0,10,200 -D part='"pusher"' mag_insert_jig.scad -->
<!-- openscad -o docs/images/jig-main-below.png --camera=0,0,0,140,0,10,200 -D part='"jig"' mag_insert_jig.scad -->
<!-- openscad -o docs/images/jig-main-below.png --camera=0,0,0,175,0,10,200 -D part='"jig"' mag_insert_jig.scad -->
<!-- openscad -o docs/images/jig-pusher-below.png --camera=0,0,0,140,0,10,200 -D part='"pusher"' mag_insert_jig.scad -->

### Release slot
Expand Down Expand Up @@ -331,15 +331,15 @@ The depth of the tab can be configured with `clickgroove_depth`. Changing this v

The `clickgroove_strength` property controls the thickness of the latch itself. This is measured from the very bottom of the latch which, if you look at the gridfinity specification, has a chamfer of 0.7mm, so the strength needs to be higher than this to get any reasonable latch height. Here's an example with `clickgroove_strength=2.5` (and `clickgroove_wall_strength=0`, or else there would not be enough space):

<!-- openscad -o docs/images/click1-strength-2.5.png --camera=0,0,0,40,0,10,100 -D plate_size='[42, 42]' -D click=true -D click_style=1 -D clickgroove_strength=2.5 -D clickgroove_wall_strength=0 -->
<img src="docs/images/click1-strength-2.5.png" alt="Click latch with clickgroove_strength=2.5" />
<!-- openscad -o docs/images/click2-strength-2.5.png --camera=0,0,0,40,0,10,100 -D plate_size='[42, 42]' -D click=true -D click_style=1 -D clickgroove_strength=2.5 -D clickgroove_wall_strength=0 -->
<img src="docs/images/click2-strength-2.5.png" alt="Click latch with clickgroove_strength=2.5" />

#### Latch Wall Strength

The `clickgroove_wall_strength` property controls the thickness of the wall behind the latch. This wall serves two purposes: It adds rigidity to the baseplate, and it prevents the click latch from bending too far. Note that the wall is measured per cell, so if you have two neighbouring cells, the actual wall thickness will be double this value. An example with `clickgroove_wall_strength=2` (and reduced `clickgroove_strength`):

<!-- openscad -o docs/images/click1-wall-strength-2.png --camera=0,0,0,40,0,10,100 -D plate_size='[84, 42]' -D click=true -D click_style=1 -D clickgroove_strength=0.8 -D clickgroove_wall_strength=2 -->
<img src="docs/images/click1-wall-strength-2.png" alt="Click latch with clickgroove_wall_strength=2" />
<!-- openscad -o docs/images/click2-wall-strength-2.png --camera=0,0,0,40,0,10,100 -D plate_size='[84, 42]' -D click=true -D click_style=1 -D clickgroove_strength=0.8 -D clickgroove_wall_strength=2 -->
<img src="docs/images/click2-wall-strength-2.png" alt="Click latch with clickgroove_wall_strength=2" />

### Arc Style Latch

Expand Down
Binary file modified docs/images/align-center.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/align-east.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/align-west.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/bottom-chamfer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/click1-distance-0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/click1-distance-5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/click1-height-0.5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/click1-inner-length-20.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/click1-outer-length-20.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/click1-steepness-0.1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/click1-steepness-5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/click1-strength-2.5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/click1-wall-strength-0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/click1-wall-strength-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/click1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/click2-bin.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/click2-bottom-chamfer-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/click2-bottom-chamfer-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/click2-bottom-chamfer-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/click2-bottom-chamfer-4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/click2-depth.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/click2-gap-length.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/click2-strength-2.5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/click2-tab-length.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/click2-wall-strength-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/click2.png
Binary file modified docs/images/closeup.png
Binary file modified docs/images/corner-radius-1.png
Binary file modified docs/images/corner-radius.png
Binary file modified docs/images/custom-cell.png
Binary file modified docs/images/edge-adjustment-cut.png
Binary file modified docs/images/edge-adjustment-default.png
Binary file modified docs/images/edge-adjustment-pad.png
Binary file modified docs/images/edge-adjustment-shift.png
Binary file modified docs/images/edge-puzzle-clickgroove.png
Binary file modified docs/images/edge-puzzle-full-height.png
Binary file modified docs/images/edge-puzzle-multi.png
Binary file modified docs/images/edge-puzzle-unconnected.png
Binary file modified docs/images/edge-puzzle.png
Binary file modified docs/images/filler-dynamic-expand-always.png
Binary file modified docs/images/filler-dynamic-expand.png
Binary file modified docs/images/filler-dynamic.png
Binary file modified docs/images/filler-half.png
Binary file modified docs/images/filler-none.png
Binary file modified docs/images/filler-third.png
Binary file modified docs/images/hscrews.png
Binary file modified docs/images/intersection-puzzle-loose.png
Binary file modified docs/images/intersection-puzzle-tight.png
Binary file modified docs/images/intersection-puzzle.png
Binary file modified docs/images/irregular-base-shape.png
Binary file modified docs/images/irregular-no-override.png
Binary file modified docs/images/irregular-override.png
Binary file modified docs/images/jig-main-below.png
Binary file modified docs/images/jig-main.png
Binary file modified docs/images/jig-pusher-below.png
Binary file modified docs/images/jig-pusher.png
Binary file modified docs/images/magnet-border-1.png
Binary file modified docs/images/magnets-glue-in-bottom.png
Binary file modified docs/images/magnets-glue-in.png
Binary file modified docs/images/magnets-none.png
Binary file modified docs/images/magnets-press-fit-below.png
Binary file modified docs/images/magnets-press-fit.png
Binary file modified docs/images/magnets-solid.png
Binary file modified docs/images/numbering-squeeze.png
Binary file modified docs/images/numbering.png
Binary file modified docs/images/override-empty.png
Binary file modified docs/images/override-normal.png
Binary file modified docs/images/override-solid.png
Binary file modified docs/images/segment-x-ideal.png
Binary file modified docs/images/segment-x-incremental-override.png
Binary file modified docs/images/segment-x-incremental.png
Binary file modified docs/images/segment-y-override.png
Binary file modified docs/images/segment-y.png
Binary file modified docs/images/separate-edge.png
Binary file modified docs/images/solid_base.png
Binary file modified docs/images/stacked-print-duplicate.png
Binary file modified docs/images/stacked-print-gap-exaggerated.png
Binary file modified docs/images/stacked-print-gap-normal.png
Binary file modified docs/images/stacked-print.png
Binary file modified docs/images/thumb-screw-base-magnet.png
Binary file modified docs/images/thumb-screw-base.png
Binary file modified docs/images/thumb-screw-magnet.png
Binary file modified docs/images/top-chamfer.png
Binary file modified docs/images/top-slice.png
Binary file modified docs/images/vscrews-counterbore.png
Binary file modified docs/images/vscrews-counterboth.png
Binary file modified docs/images/vscrews-countersink.png
Binary file modified docs/images/vscrews-diameter.png
Binary file modified docs/images/vscrews-else.png
Binary file modified docs/images/vscrews-most.png
Binary file modified docs/images/vscrews-plate-corners-mod.png
Binary file modified docs/images/vscrews-plate-corners.png
Binary file modified docs/images/vscrews-plate-edges.png
Binary file modified docs/images/vscrews-segment-corners.png
Binary file modified docs/images/vscrews-segment-edges.png
Binary file modified docs/images/vscrews.png
Binary file modified docs/images/wall-bottom.png
Binary file modified docs/images/wall-top.png
Binary file modified docs/images/whole.png
51 changes: 3 additions & 48 deletions justfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
paths:
mkdir -p paths
uv run extract_paths.py puzzle.svg paths/puzzle.scad
uv run scripts/extract_paths.py puzzle.svg paths/puzzle.scad

test:
openscad -o /dev/null --export-format=stl test.scad
Expand All @@ -14,53 +14,8 @@ banner name:

banners: (banner "banner-generator-yawkat") (banner "banner-generator-perplexinglabs")

docs:
#!/usr/bin/env -S uv run --script
import re
import shlex
import subprocess
import os
import asyncio

openscad_pattern = re.compile(r"^\s*<!--\s*openscad (.+)\s*-->\s*$")
concurrency = asyncio.Semaphore(8)

async def run(cmd, output):
max_attempts = 5
for attempt in range(1, max_attempts + 1):
async with concurrency:
print("Running: " + shlex.join(cmd))
proc = await asyncio.create_subprocess_exec(*cmd)
await proc.wait()
assert proc.returncode == 0
if os.path.getsize(output) != 7763:
return
# render failure, retry
print(f"Render failure for `{shlex.join(cmd)}` ({attempt}/{max_attempts}), retrying")
raise RuntimeError(f"Render failure after {max_attempts} attempts: {shlex.join(cmd)}")

async def main():
tasks = []
written = []
for line in open("README.md"):
match = openscad_pattern.match(line)
if match:
cmd = ["openscad", "--hardwarnings", "--projection=ortho", "--colorscheme=Starnight", "--render", "--imgsize=2500,1000", *shlex.split(match.group(1))]
# use gridflock.scad if no other file specified
for c in cmd:
if ".scad" in c:
break
else:
cmd.append("gridflock.scad")
output = cmd[cmd.index("-o") + 1]
tasks.append(run(cmd, output))
written.append(output)
for f in os.listdir("docs/images"):
if os.path.join("docs/images", f) not in written:
os.unlink(os.path.join("docs/images", f))
await asyncio.gather(*tasks)

asyncio.run(main())
docs: paths
uv run scripts/generate_docs.py

overlay-png name:
inkscape -w 1600 -h 1200 docs/{{name}}.svg -o build/{{name}}.png
Expand Down
2 changes: 1 addition & 1 deletion mag_insert_jig.scad
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ if (part == "jig") {
jig_with_pusher();
}
} else {
jig_with_pusher();
color([1.0, 0.2, 0.2, jig_alpha]) jig_with_pusher();
}
} else if (part == "pusher") {
pusher_combined();
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@ description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"pypng>=0.20220715.0",
"svgelements>=1.9.6",
"zopfli>=0.4.3",
]
40 changes: 40 additions & 0 deletions scripts/canonicalize_png.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import os
import sys
from pathlib import Path

import png
from zopfli.zlib import compress


class _ZopfliCompressor:
def __init__(self) -> None:
self.data = bytearray()

def compress(self, data: bytes) -> bytes:
self.data.extend(data)
return b""

def flush(self) -> bytes:
return compress(bytes(self.data), numiterations=1)


def canonicalize(path: Path | str) -> None:
path = Path(path)
temporary_path = path.with_name(f".{path.name}.canonical.png")
original_compressobj = png.zlib.compressobj
try:
width, height, rows, _ = png.Reader(filename=path).asRGB8()
# PyPNG always uses PNG filter 0. Substitute its streaming zlib object
# with Zopfli so compression is deterministic across zlib versions.
png.zlib.compressobj = lambda *_args, **_kwargs: _ZopfliCompressor()
with temporary_path.open("wb") as output:
png.Writer(width, height, greyscale=False, alpha=False).write(output, rows)
os.replace(temporary_path, path)
finally:
png.zlib.compressobj = original_compressobj
temporary_path.unlink(missing_ok=True)


if __name__ == "__main__":
for argument in sys.argv[1:]:
canonicalize(Path(argument))
File renamed without changes.
102 changes: 102 additions & 0 deletions scripts/generate_docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import asyncio
import os
import re
import shlex
from concurrent.futures import ProcessPoolExecutor
from pathlib import Path

from canonicalize_png import canonicalize
from pinned_openscad import openscad_path


OPENSCAD_PATTERN = re.compile(r"^\s*<!--\s*openscad (.+)\s*-->\s*$")
CONCURRENCY = asyncio.Semaphore(8)
CANONICALIZE_POOL = ProcessPoolExecutor(max_workers=8)
OPENSCAD_ENVIRONMENT = os.environ | {
"GALLIUM_DRIVER": "softpipe",
"LIBGL_ALWAYS_SOFTWARE": "1",
"MESA_LOADER_DRIVER_OVERRIDE": "softpipe",
}


async def render(command: list[str], output: str) -> None:
output_path = Path(output)
temporary_output = output_path.with_name(f".{output_path.name}.rendering.png")
render_command = command.copy()
render_command[render_command.index("-o") + 1] = str(temporary_output)
max_attempts = 5
try:
for attempt in range(1, max_attempts + 1):
async with CONCURRENCY:
print("Running: " + shlex.join(command))
process = await asyncio.create_subprocess_exec(
*render_command,
env=OPENSCAD_ENVIRONMENT,
)
await process.wait()
if process.returncode != 0:
raise RuntimeError(
f"OpenSCAD exited with {process.returncode}: {shlex.join(command)}"
)
if temporary_output.stat().st_size != 7763:
loop = asyncio.get_running_loop()
await loop.run_in_executor(
CANONICALIZE_POOL, canonicalize, temporary_output
)
os.replace(temporary_output, output_path)
return
print(
f"Render failure for `{shlex.join(command)}` "
f"({attempt}/{max_attempts}), retrying"
)
raise RuntimeError(
f"Render failure after {max_attempts} attempts: {shlex.join(command)}"
)
finally:
temporary_output.unlink(missing_ok=True)


async def render_group(commands: list[list[str]], output: str) -> None:
# Some README examples intentionally use the same output for legacy and
# renamed options. Run those sequentially so they cannot race over the
# shared temporary and final paths.
for command in commands:
await render(command, output)


async def main() -> None:
openscad = str(openscad_path())
commands_by_output: dict[str, list[list[str]]] = {}
with Path("README.md").open() as readme:
for line in readme:
match = OPENSCAD_PATTERN.match(line)
if match:
command = [
openscad,
"--enable",
"predictible-output",
"--hardwarnings",
"--projection=ortho",
"--colorscheme=Starnight",
"--render",
"--imgsize=2500,1000",
*shlex.split(match.group(1)),
]
if not any(".scad" in argument for argument in command):
command.append("gridflock.scad")
output = command[command.index("-o") + 1]
commands_by_output.setdefault(output, []).append(command)

for image in Path("docs/images").iterdir():
if str(image) not in commands_by_output:
image.unlink()
await asyncio.gather(
*(
render_group(commands, output)
for output, commands in commands_by_output.items()
)
)


if __name__ == "__main__":
asyncio.run(main())
75 changes: 75 additions & 0 deletions scripts/pinned_openscad.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import hashlib
import os
import platform
import shutil
import stat
import subprocess
import sys
import tempfile
import urllib.request
from pathlib import Path


OPENSCAD_VERSION = "2026.07.16"
OPENSCAD_URL = (
f"https://files.openscad.org/snapshots/"
f"OpenSCAD-{OPENSCAD_VERSION}-x86_64.AppImage"
)
OPENSCAD_SHA256 = "85000c7839cf96ca824511d9da38091683e3c6f71a390205ef4dd07e62ed97b4"
BUILD_DIRECTORY = Path(__file__).resolve().parent.parent / "build"


def _download(destination: Path) -> None:
digest = hashlib.sha256()
print(f"Downloading OpenSCAD {OPENSCAD_VERSION} AppImage", file=sys.stderr)
with urllib.request.urlopen(OPENSCAD_URL) as response, destination.open("wb") as output:
while chunk := response.read(1024 * 1024):
digest.update(chunk)
output.write(chunk)
actual_sha256 = digest.hexdigest()
if actual_sha256 != OPENSCAD_SHA256:
raise RuntimeError(
f"OpenSCAD AppImage checksum mismatch: expected {OPENSCAD_SHA256}, "
f"got {actual_sha256}"
)
destination.chmod(destination.stat().st_mode | stat.S_IXUSR)


def openscad_path() -> Path:
if platform.system() != "Linux" or platform.machine() not in {"x86_64", "AMD64"}:
raise RuntimeError("The pinned OpenSCAD AppImage requires x86_64 Linux")

cache_parent = BUILD_DIRECTORY
cache_directory = cache_parent / f"openscad-{OPENSCAD_VERSION}-{OPENSCAD_SHA256[:12]}"
executable = cache_directory / "AppRun"
if executable.is_file():
return executable

cache_parent.mkdir(parents=True, exist_ok=True)
temporary_directory = Path(tempfile.mkdtemp(prefix="openscad-", dir=cache_parent))
try:
appimage = temporary_directory / "OpenSCAD.AppImage"
_download(appimage)
subprocess.run(
[appimage, "--appimage-extract"],
cwd=temporary_directory,
check=True,
stdout=subprocess.DEVNULL,
)
extracted = temporary_directory / "squashfs-root"
try:
extracted.rename(cache_directory)
except FileExistsError:
# Another concurrent invocation populated the same immutable cache.
pass
finally:
shutil.rmtree(temporary_directory, ignore_errors=True)

if not executable.is_file():
raise RuntimeError("OpenSCAD AppImage extraction did not produce AppRun")
return executable


if __name__ == "__main__":
executable = openscad_path()
os.execv(executable, [str(executable), *sys.argv[1:]])
Loading
Loading