Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,12 @@
# The licence is a legal document. It is stored exactly as received.
# ---------------------------------------------------------------------------
/LICENSE -text

# ---------------------------------------------------------------------------
# The copy of the OpenGL binding under third_party is a published module with
# one patch, and a guard holds every file of it to the published bytes plus
# that patch. The generated bindings are 2.3 MB and 0.8 MB of code nobody
# wrote by hand, so they are marked generated for the pull request view. The
# patched file, procaddr.go, is not - it is the one worth reading.
# ---------------------------------------------------------------------------
third_party/go-gl-gl/**/package.go linguist-generated=true
5 changes: 5 additions & 0 deletions .github/mesa-dist-win
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
version=26.2.0
archive=mesa3d-26.2.0-release-msvc.7z
sha256=dcb2719ef346dab5b609fcb193a5f13cfc4b0502e3f4de1ad43d349477402f47
file=x64/libgallium_wgl.dll 1a2e49cd5fdb1a857d98117ab04240d723b57da5dffe6d07f5386f42014557c1
file=x64/opengl32.dll 33b217ed7947b48684baa987914475898a2b4d7d64cce96b078216c67a633582
12 changes: 11 additions & 1 deletion .github/scripts/dependency_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,17 @@
# looked at. A name here is a decision with a reason, not a way to make a red
# build green - and it names one package, never a whole ecosystem.
EXCEPTIONS = {
# (empty on purpose - add "name": "why this is fine" when it happens)
# The OpenGL binding, carried as a copy in this repository since
# 2026-09-17 and named by go.mod through a replace directive. GitHub's
# graph reports the directory as a new dependency with no licence, because
# a directory is not a package on any registry. The licence is the copied
# module's own MIT, in the LICENSE file that travels with the directory,
# and the bytes are the published module plus one patch - which is what
# TestTheOpenGLBindingIsThePinnedModulePlusExactlyThePatch holds, by
# downloading the pinned version and comparing. The reason for the copy is
# in third_party/go-gl-gl/PATCH.md. This names one directory, and a second
# copy of something else needs a line of its own here.
"./third_party/go-gl-gl": "a copy of github.com/go-gl/gl, MIT, held to the published module plus one patch by a guard",
}

# GitHub reports license: null for every action, measured on this repository on
Expand Down
65 changes: 65 additions & 0 deletions .github/scripts/fetch_software_renderer.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
# Puts the software renderer beside the window binary, for the Windows
# archive: the two files of Mesa's llvmpipe that the window loads when the
# graphics driver offers no OpenGL 2.1.
#
# Usage: fetch_software_renderer.sh <directory the window binary is in>
#
# The release of pal1000/mesa-dist-win to take them from, the SHA-256 of that
# release's archive, and the two files with their own sums, come from
# .github/mesa-dist-win and nowhere else. The same version and sums stand in
# internal/legal/companions.go, which is what the notices and the bill of
# materials are rendered from, and a guard holds the two files equal - so a
# bump here without the review there is refused before it is built.
#
# The archive's sum is checked BEFORE anything is unpacked. A download that
# does not match is an archive nobody reviewed, and unpacking it first would
# put two files nobody reviewed next to a program that loads them by path.
#
# Exactly two files, taken from one directory of the archive, into one
# directory beside the program. The names are the ones the window looks for -
# see internal/gui/software.go - and the guard above holds these to the
# registry as well, so a renamed file cannot ship under the old name.
set -euo pipefail

into="${1:?usage: fetch_software_renderer.sh <directory>}"
pin=".github/mesa-dist-win"

version="$(grep '^version=' "${pin}" | cut -d= -f2-)"
archive="$(grep '^archive=' "${pin}" | cut -d= -f2-)"
sha256="$(grep '^sha256=' "${pin}" | cut -d= -f2-)"
for value in "${version}" "${archive}" "${sha256}"; do
if [ -z "${value}" ]; then
echo "fetch_software_renderer: ${pin} does not name the version, the archive and the sum" >&2
exit 1
fi
done

# Beside the target rather than under /tmp: on one machine /tmp was a
# directory this user could write and not read back, and a download that
# cannot be summed is a download that cannot be trusted.
mkdir -p "${into}"
fetched="$(mktemp -d "${into}/.software-renderer.XXXXXX")"
curl --silent --show-error --fail --location --retry 3 \
--output "${fetched}/${archive}" \
"https://github.com/pal1000/mesa-dist-win/releases/download/${version}/${archive}"
(cd "${fetched}" && echo "${sha256} ${archive}" | sha256sum -c -)

# The files, each with its own sum, from the same pin. The archive's sum
# already covers them, and this is the half a person can check against the
# notices without downloading seventy megabytes: the notices name these two
# sums, and so does the registry the notices are rendered from.
mkdir -p "${into}/opengl"
grep '^file=' "${pin}" | cut -d= -f2- | while read -r inside sum; do
7z e -bso0 -o"${into}/opengl" "${fetched}/${archive}" "${inside}"
(cd "${into}/opengl" && echo "${sum} $(basename "${inside}")" | sha256sum -c -)
done
rm -rf "${fetched}"

# Two files and no more, or the archive is not what the notices describe.
count="$(find "${into}/opengl" -type f | wc -l | tr -d ' ')"
if [ "${count}" != "2" ]; then
echo "fetch_software_renderer: expected two files under opengl and found ${count}" >&2
exit 1
fi
ls -l "${into}/opengl"
87 changes: 67 additions & 20 deletions .github/scripts/sign_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,15 @@ def run(argv, **kw):
return subprocess.run(argv, check=True, **kw)


def powershell(script):
def powershell(script, env=None):
"""Run a PowerShell script. Values the script needs go in env, never in
the script's text: a file name with a quote in it would end the string
and the rest would run as PowerShell. -LiteralPath does not help, because
the injected quote is parsed before the parameter is."""
out = subprocess.run(
["powershell", "-NoProfile", "-NonInteractive", "-Command", script],
capture_output=True, text=True)
capture_output=True, text=True,
env=None if env is None else {**os.environ, **env})
if out.returncode != 0:
raise SystemExit("sign_release: powershell failed:\n%s" % out.stderr.strip())
# PowerShell errors are NON TERMINATING by default, so a script can print
Expand Down Expand Up @@ -243,15 +248,21 @@ def signing_thumbprint(pin):


def certificate_of(path):
"""The sha256 of the certificate that actually signed a file."""
"""The sha256 of the certificate that actually signed a file.

The path reaches PowerShell as an environment variable and never as part
of the script. Until 2026-09-17 it was interpolated into the text, which
was harmless while the only file was our own tfg-gui.exe and stopped
being harmless the day the archive gained files named by somebody else
- an outside review of the pull request named it."""
script = (
"$s = Get-AuthenticodeSignature -LiteralPath '%s'; "
"$s = Get-AuthenticodeSignature -LiteralPath $env:TFG_SIGNED_FILE; "
"if ($s.Status -ne 'Valid') { Write-Error ('signature status: ' + $s.Status); exit 1 }; "
"$h = [System.Security.Cryptography.SHA256]::Create()"
".ComputeHash($s.SignerCertificate.RawData); "
"(($h | ForEach-Object { $_.ToString('x2') }) -join '')" % path
"(($h | ForEach-Object { $_.ToString('x2') }) -join '')"
)
return powershell(script).strip()
return powershell(script, env={"TFG_SIGNED_FILE": path}).strip()


def sha256_of(path):
Expand Down Expand Up @@ -333,40 +344,76 @@ def windows_archives(directory):
return found


def files_under(work):
"""Every file under work, as a path relative to it with forward slashes.

Recursive, and that is the whole point of it. Until 2026-09-17 the
archive was put back together from os.listdir, which names a directory
and none of its contents, and zipfile writes a directory entry for a
directory and nothing more - so a subdirectory came out of the signing
EMPTY, with no error and a valid archive. Nothing shipped in a
subdirectory until the software renderer did, so nothing had noticed.
"""
found = []
for base, _dirs, names in os.walk(work):
for name in names:
full = os.path.join(base, name)
found.append(os.path.relpath(full, work).replace(os.sep, "/"))
return sorted(found)


def sign_archive(path, thumbprint, pin, signtool, dry_run):
"""Sign the program inside one archive and put the archive back together."""
"""Sign everything Authenticode can sign inside one archive - the program
and any library beside it - and put the archive back together whole."""
work = path + ".unpacked"
if os.path.isdir(work):
shutil.rmtree(work)
os.makedirs(work)
with zipfile.ZipFile(path) as archive:
archive.extractall(work)
programs = [n for n in sorted(os.listdir(work)) if n.endswith(".exe")]
inside = files_under(work)
programs = [n for n in inside if n.endswith(".exe")]
if len(programs) != 1:
raise SystemExit("sign_release: %s holds %d programs, expected one"
% (os.path.basename(path), len(programs)))
program = os.path.join(work, programs[0])

command = [signtool, "sign", "/sha1", thumbprint, "/fd", "sha256",
"/tr", TIMESTAMP_URL, "/td", "sha256", "/v", program]
if dry_run:
print(" DRY RUN, would run: %s" % " ".join(command))
else:
# The libraries beside the program too: the software renderer in the
# window's archive. Their bytes are somebody else's and reviewed as such
# (internal/legal/companions.go), and the signature says this release
# vouches for exactly these bytes next to its program - a library beside
# a signed program is otherwise the one file a tamperer would swap.
signed = programs + [n for n in inside if n.endswith(".dll")]
Comment thread
coderabbitai[bot] marked this conversation as resolved.

for name in signed:
target = os.path.join(work, name)
command = [signtool, "sign", "/sha1", thumbprint, "/fd", "sha256",
"/tr", TIMESTAMP_URL, "/td", "sha256", "/v", target]
if dry_run:
print(" DRY RUN, would run: %s" % " ".join(command))
continue
run(command)
run([signtool, "verify", "/pa", "/v", program])
actual = certificate_of(program)
run([signtool, "verify", "/pa", "/v", target])
actual = certificate_of(target)
if actual != pin:
raise SystemExit(
"sign_release: %s was signed by a DIFFERENT certificate\n"
" expected %s\n got %s\nNothing has been uploaded."
% (programs[0], pin, actual))
% (name, pin, actual))

os.remove(path)
with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as archive:
for name in sorted(os.listdir(work)):
for name in files_under(work):
archive.write(os.path.join(work, name), name)
# What went in is what comes out: every file, in every directory. A
# repack that lost a file would be the quietest defect this script could
# have, so it is counted rather than trusted.
with zipfile.ZipFile(path) as archive:
repacked = sorted(n for n in archive.namelist() if not n.endswith("/"))
if repacked != inside:
raise SystemExit("sign_release: %s was repacked with %d file(s) and held %d\nNothing has been uploaded."
% (os.path.basename(path), len(repacked), len(inside)))
shutil.rmtree(work)
print(" %s: %s signed and repacked" % (os.path.basename(path), programs[0]))
print(" %s: %s signed and repacked, %d file(s)"
% (os.path.basename(path), ", ".join(signed), len(repacked)))


def macos_archives(directory):
Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,17 @@ jobs:
# updated the list and not the prose. The list is checked by the job
# below and the prose is checked by nobody.
#
# Unchanged on 2026-09-17, when github.com/go-gl/gl gained a replace
# directive pointing at a copy under third_party, and that is worth
# saying because it looks like the kind of change this list exists to
# notice. go list still reports the module under its own path, so
# the list below is the same - what moved is where the bytes come
# from, and two guards hold that instead: one keeps the copy equal to
# the published version plus exactly the one patch its PATCH.md
# describes, and one reads the built window binary's import table.
# go.sum no longer carries the module's sum, because a replaced
# module has none there. The sum lives in that PATCH.md now.
#
# The second question protects what ships to most people. The
# command line binary links exactly four external modules and the
# toolkit is not among them, so a build for a server carries no
Expand Down
16 changes: 16 additions & 0 deletions .github/workflows/dev-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,22 @@ jobs:
"${work}" "tfg-gui" "com.donislawdev.tfg-gui" "dev-${short}"
fi

# The software renderer, beside the program, Windows only: two files
# of Mesa's llvmpipe the window loads when the graphics driver offers
# no OpenGL 2.1 - a virtual machine without 3D acceleration, a remote
# desktop, a server. Measured on 2026-09-17 on such a guest: without
# them the window refuses, with them it opens. The version and the
# sums come from .github/mesa-dist-win, the script checks the sums
# before it unpacks anything, and a guard holds that file to the
# registry the notices are rendered from. Linux has Mesa in the
# system and macOS has never offered the toolkit less than it needs,
# so nothing of the kind ships there. Here as well as in the
# release, by the owner's decision: a build from a branch has to be the
# build a guest without a driver can be handed.
if [ "$os" = "windows" ]; then
.github/scripts/fetch_software_renderer.sh "${work}"
fi

cp LICENSE THIRD-PARTY-NOTICES.md README.md "${work}/"
.github/scripts/unofficial_note.sh "${work}/UNOFFICIAL-BUILD.txt" "${short}"

Expand Down
22 changes: 22 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,20 @@ jobs:
"${work}" "tfg-gui" "com.donislawdev.tfg-gui" "${version}"
fi

# The software renderer, beside the program, Windows only: two files
# of Mesa's llvmpipe the window loads when the graphics driver offers
# no OpenGL 2.1 - a virtual machine without 3D acceleration, a remote
# desktop, a server. Measured on 2026-09-17 on such a guest: without
# them the window refuses, with them it opens. The version and the
# sums come from .github/mesa-dist-win, the script checks the sums
# before it unpacks anything, and a guard holds that file to the
# registry the notices are rendered from. Linux has Mesa in the
# system and macOS has never offered the toolkit less than it needs,
# so nothing of the kind ships there.
if [ "$os" = "windows" ]; then
.github/scripts/fetch_software_renderer.sh "${work}"
fi

cp LICENSE THIRD-PARTY-NOTICES.md README.md "${work}/"

base="tfg-gui_${version}_${label}_${arch}"
Expand Down Expand Up @@ -445,6 +459,14 @@ jobs:
echo "- \`tfg_*\` is the command line. It carries no graphics toolkit and no network stack."
echo "- \`tfg-gui_*\` is the desktop window. Same engine, same features."
echo
echo "The Windows window archive also carries a software OpenGL renderer, Mesa llvmpipe,"
echo "in \`opengl\` next to the program. The window loads it only after its first attempt"
echo "at a window failed - typically a graphics driver with no OpenGL 2.1: a virtual"
echo "machine without 3D acceleration, a remote desktop, a server - and says so on its"
echo "About screen. On a machine with a driver it is not touched unless you ask for it"
echo "with \`--software-gl\`. Both files are signed like the program. Keep the folder next"
echo "to the program."
echo
echo "Match the file to your system and architecture. Check what you downloaded against"
echo "\`verify-SHA256SUMS.txt\`, at the bottom of this list."
echo
Expand Down
Loading
Loading