diff --git a/.env.example b/.env.example index fa2d0e3..3ced2d4 100644 --- a/.env.example +++ b/.env.example @@ -28,5 +28,4 @@ SYNOLOGY_PHOTOS_CERT_SHA256= # Build-only mirror hosts; official defaults work outside mainland China. # For mainland NAS builds: source ./deploy/build-cn.env before build-verify.sh. -DEBIAN_MIRROR=deb.debian.org -DEBIAN_SECURITY_MIRROR=deb.debian.org +ALPINE_MIRROR=dl-cdn.alpinelinux.org diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 5f81ade..ad202d1 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -15,8 +15,7 @@ jobs: DOCKER_PIP_FALLBACK_INDEX_URL: ${{ vars.DOCKER_PIP_FALLBACK_INDEX_URL }} CI_PIP_INDEX_URL: ${{ vars.CI_PIP_INDEX_URL }} CI_PIP_FALLBACK_INDEX_URL: ${{ vars.CI_PIP_FALLBACK_INDEX_URL }} - DEBIAN_MIRROR: ${{ vars.DEBIAN_MIRROR }} - DEBIAN_SECURITY_MIRROR: ${{ vars.DEBIAN_SECURITY_MIRROR }} + ALPINE_MIRROR: ${{ vars.ALPINE_MIRROR }} steps: - uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - uses: https://github.com/actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 diff --git a/.gitignore b/.gitignore index 5e24a17..99c5835 100644 --- a/.gitignore +++ b/.gitignore @@ -16,8 +16,6 @@ dist/ .mypy_cache/ -build-cache/debs/* -!build-cache/debs/.gitkeep build-cache/wheels/* !build-cache/wheels/.gitkeep diff --git a/Dockerfile b/Dockerfile index c2a46a2..f619bf4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,36 +1,24 @@ -ARG PYTHON_BASE=python:3.14.5-slim-bookworm@sha256:ec58d916f9e24a6035cab2bdf07f6206c4cc092a16613c60597534711332d9d6 +ARG PYTHON_BASE=python:3.14.8-alpine3.24@sha256:f6a589d43c42b9e7f7dc67a12d37132491f362859a5d750607710cc56da3bc72 FROM ${PYTHON_BASE} AS prepared -ARG DEBIAN_MIRROR=deb.debian.org -ARG DEBIAN_SECURITY_MIRROR=deb.debian.org +ARG ALPINE_MIRROR=dl-cdn.alpinelinux.org ARG TARGETARCH ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 TZ=Asia/Shanghai \ DATA_DIR=/data CONFIG_DIR=/config WORKDIR /app -# CJK fonts intentionally retained for readable Chinese rendering. -RUN --mount=type=cache,id=frameflow-bookworm-apt-${TARGETARCH},target=/var/cache/apt,sharing=locked \ - --mount=type=cache,id=frameflow-bookworm-lists-${TARGETARCH},target=/var/lib/apt/lists,sharing=locked \ - --mount=type=bind,source=build-cache/debs,target=/build-debs,readonly \ - rm -f /etc/apt/apt.conf.d/docker-clean \ - && printf 'Binary::apt::APT::Keep-Downloaded-Packages "true";\n' > /etc/apt/apt.conf.d/keep-cache \ - && sed -i "s|http://deb.debian.org/debian-security|https://${DEBIAN_SECURITY_MIRROR}/debian-security|g; s|http://deb.debian.org/debian|https://${DEBIAN_MIRROR}/debian|g" /etc/apt/sources.list.d/debian.sources \ - && apt-get -o Acquire::Retries=3 -o Acquire::https::Timeout=60 update \ - && apt-get -y --no-install-recommends upgrade \ - && if [ -f /build-debs/fonts-noto-cjk.deb ]; then \ - font_version=$(apt-cache policy fonts-noto-cjk | awk '$1=="Candidate:" {print $2}'); \ - font_sha=$(apt-cache show "fonts-noto-cjk=$font_version" | awk '$1=="SHA256:" {print $2;exit}'); \ - test -n "$font_sha"; \ - echo "$font_sha /build-debs/fonts-noto-cjk.deb" | sha256sum -c -; \ - apt-get install -y --no-install-recommends tzdata; \ - dpkg -i /build-debs/fonts-noto-cjk.deb; \ - else apt-get install -y --no-install-recommends fonts-noto-cjk tzdata; fi \ - && groupadd --gid 10333 frameflow && useradd --uid 10333 --gid 10333 --no-create-home frameflow \ +# Preserve the existing FONT_PATH contract for deployments and Chinese rendering. +RUN sed -i "s|https://dl-cdn.alpinelinux.org/alpine|https://${ALPINE_MIRROR}/alpine|g" /etc/apk/repositories \ + && apk upgrade --no-cache \ + && apk add --no-cache font-noto-cjk tzdata \ + && mkdir -p /usr/share/fonts/opentype/noto \ + && ln -s /usr/share/fonts/noto/NotoSansCJK-Regular.ttc /usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc \ + && addgroup -g 10333 frameflow && adduser -D -H -u 10333 -G frameflow frameflow \ && mkdir -p /data/images /data/generated /data/cache /config \ && chown -R 10333:10333 /data /config COPY requirements.lock ./ COPY scripts/ci-install.py /build-tools/install.py ARG DOCKER_PIP_INDEX_URL=https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple ARG DOCKER_PIP_FALLBACK_INDEX_URL=https://pypi.org/simple -RUN --mount=type=cache,id=frameflow-pip-py314-${TARGETARCH},target=/root/.cache/pip,sharing=locked \ +RUN --mount=type=cache,id=frameflow-pip-py314-musl-${TARGETARCH},target=/root/.cache/pip,sharing=locked \ --mount=type=bind,source=build-cache/wheels,target=/build-wheels,readonly \ if [ -f /build-wheels/SHA256SUMS ]; then \ (cd /build-wheels && sha256sum -c SHA256SUMS) \ @@ -55,6 +43,7 @@ WORKDIR /app ARG BUILD_COMMIT=development ARG BUILD_TIME=unknown ENV BUILD_COMMIT=$BUILD_COMMIT BUILD_TIME=$BUILD_TIME +LABEL org.opencontainers.image.title="PIGWay Frame Flow" org.opencontainers.image.version="0.1.21" org.opencontainers.image.revision=$BUILD_COMMIT USER 10333:10333 EXPOSE 18787 HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ diff --git a/NOTICE.md b/NOTICE.md index 73e7735..6b3b65e 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -21,7 +21,8 @@ for endpoint coverage. Their source code was not incorporated into runtime. FastAPI, Starlette, Uvicorn, Requests, croniter, PyYAML, Pydantic and their transitive dependencies retain their respective upstream licenses. Pillow uses its upstream -HPND license. Debian Noto CJK fonts retain their font package license. +HPND license. Noto CJK fonts use SIL OFL 1.1; the license is included in +`vendor/noto-cjk-OFL-1.1.txt`, with copyright notices retained in font metadata. Development dependencies (pytest, Ruff, mypy, pip-audit, HTTPX2, Playwright) retain their upstream licenses and are not shipped in the runtime image. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 9761765..dce0721 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,9 +1,9 @@ -# pigway-frame-flow v0.1.20 +# pigway-frame-flow v0.1.21 -统一开源许可证、贡献和安全反馈入口;完善当前版本的文档导航、源码分发和校验和。插件 ZIP 携带许可证。运行功能边界保持项目首页所述。 +基础映像改为固定摘要的官方 Python 3.14 Alpine 3.24,并应用系统安全更新,移除 Debian 基础系统遗留告警。保留原字体路径、中文字体、时区和第三方许可。 -本轮 585 项 Python 测试通过。源码语法、文档入口、打包和校验和检查通过;生产运行状态以实际部署验收为准。 +发布脚本统一从完整 Dockerfile 构建,避免复用旧运行映像继续携带漏洞;版本标签读取项目元数据。移除旧 Debian 离线字体缓存和运行映像覆盖构建方式,同步构建参数与文档。 -Ruff、格式、mypy、25 项锁定依赖的漏洞检查通过;AMD64 Docker 构建和两轮隔离启动/重启只读冒烟验证通过。 +585 项测试、Ruff、格式和 mypy 通过。AMD64 容器启动/重启、只读接口及中文实际字形和 PNG 渲染验证通过。Trivy 0.76.0 扫描系统包和全部 25 项 Python 运行依赖,各严重级别均未检出已知漏洞;结论限于 2026-10-18 漏洞库快照。 -Docker 构建增加发行版安全更新,清除可修复系统包告警;仍有上游未修复包级告警,分析及默认部署边界见 [安全说明](SECURITY.md),不宣称映像零漏洞。 +生产升级须通过现有挂载身份校验和部署流程完成,发布源码不等同于生产升级。 diff --git a/SECURITY.md b/SECURITY.md index 42fb671..1ae5fd0 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -14,12 +14,8 @@ A useful private report includes the affected version, deployment mode, required permissions, impact, and minimal steps using mock data. Do not test on another person's deployment or write to real frames without explicit authorization. -## 当前容器基础系统扫描 +## 当前容器映像检查 -2026-10-18 使用 Trivy 0.76.0 扫描当前映像。在构建阶段应用 Debian 安全更新后,可修复的 HIGH/CRITICAL 项已清除;仍有 63 条发行版包级告警,合并为 16 个 CVE,当前 Bookworm 无可用修复版本。不能将 Python 依赖检查通过描述为整个映像零漏洞。 +v0.1.21 使用官方 Python Alpine 3.24 基础映像并固定摘要,构建时应用 APK 安全更新。2026-10-18 使用 Trivy 0.76.0 扫描构建的 Linux AMD64 映像,涵盖操作系统和 Python 包、全部严重级别,未检出已知漏洞;未配置漏洞忽略规则。该结果是当日漏洞库快照,不是未来无漏洞的保证,也不代表旧运行容器已升级。 -- [CVE-2023-45853](https://security-tracker.debian.org/tracker/CVE-2023-45853) 针对 MiniZip;Debian 明确 Bookworm 的 zlib 未构建相应代码,本映像也未安装 MiniZip。 -- [CVE-2025-7458](https://security-tracker.debian.org/tracker/CVE-2025-7458) 需要攻击者执行特制 SQL;Frame Flow 不接受任意 SQL 或上传外部 SQLite 数据库。FTS5 相关 [CVE-2026-11822](https://security-tracker.debian.org/tracker/CVE-2026-11822)、[CVE-2026-11824](https://security-tracker.debian.org/tracker/CVE-2026-11824) 的匹配查询也未被应用使用。此结论不适用于自行替换数据库或修改服务 SQL 的部署。 -- 其他告警涉及挂载/命名空间工具、systemd-homed、ACL、ncurses、Perl/LZH CLI 及 DTLS;应用未调用这些路径。默认 UID/GID 10333、只读根目录、清空 capabilities、禁提权和独立数据卷仍是必要边界。 - -本轮未发现这些遗留项在默认应用路径中的可达利用入口,不将该判断扩展为基础系统已修复。后续基础映像更新仍须复扫;不得为消除扫描数字而混装其他 Debian 发行版包或删除许可声明。 +基础映像更新后须重新构建并复扫。默认非 root、只读根目录、清空 capabilities 和 no-new-privileges 的部署限制继续保留。中文字体、第三方许可证和依赖元数据均保留;不以删除包清单掩盖告警。 diff --git a/SHA256SUMS b/SHA256SUMS index 10c2f33..3fc5680 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -1,48 +1,46 @@ c2792b2c75880ff09822804632b1b3b6dd6190b38841b8c3567bb1b7e7b59a8c .dockerignore -fe4b8e57c14d6f4df261de0ec274f33153def558227a47f38c3f91d5cfc77f80 .env.example +d20031a8e861a0f3e19b7750ddb53bc84c480c469c44a70ecdf8261ec05431c0 .env.example 0c7f4585d62a8d35bfc22e0bebdf7a479a179122b2b092bfe434e3038cddd3d5 .gitattributes b45d35d737f119a70bf40cb1f9626ff80772ed7913cb35f6e9b870d6ada01a15 .github/workflows/community.yml -c93afa220096b0a3a27115e74617db9dbe97e98c6a3735a0abfb528cdca7e668 .github/workflows/verify.yml -342242379194ca16021542722407a42858f22685293d16162d0e6fe004cbd62c .gitignore +d3ad09bdc5dc60c41a774e1cad43f3e3f8ae5c851303ae6ec2aac030392a2939 .github/workflows/verify.yml +3931cc314019760a582e0914e2d9c0f3b862a004d0a30d374900edcad5f599e5 .gitignore 4b4ed4926a350cbeb71436541b4566e4ba7d2494ac95236b88827a9f5dd171c2 CONTRIBUTING.md -6588694aee24d5e662061be78027a049f6726d50dd99ef00a60fba05051ef299 Dockerfile +660c76fccccae8643633d052d0d6733ecf278f2a3016de29e6850b312a365413 Dockerfile 6eb7728e6e73f9568ca6fd5ad6383d13ffc638f2870fefa2d86f72f39685c0fc LICENSE -ef0d1ecd6427325fc92324032fbf7b880624a68629acbaf430cd8be80c73b698 NOTICE.md +250a2bcec248b074786d90e0329874628d70d5a0fe3b4fba47c8c57d4f84dcce NOTICE.md 26ad0d9063c0ee843f0f532604b8fd66e8b39fd72d129595ed078677c0d6d13c README.md ee00cb267b38485f1e574d610f0a57b51d4ae575731a18e57d06a7c43dc0a8b0 README.zh-CN.md -3c0ab38958a3bdb17ee95b5642c2ee691affc3da44252a5c4a3048ebf7860fe4 RELEASE_NOTES.md -d1233db9c8da6d391028e1e515813cd45ce8d8f97e48087daa325a61789987bc SECURITY.md -e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 build-cache/debs/.gitkeep +304bec983ac8242107b1ec4154522f451a5d33b6a77ed26669926cb3e161f863 RELEASE_NOTES.md +6a0c85c8d52a2d4a3b779dce306a93c4efc4dfe3ab3b23e4a6a3bb7c55079910 SECURITY.md e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 build-cache/wheels/.gitkeep e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 config/.gitkeep -a897bf4a9f95f3e7b96b286a4bf7d124bb4a50fd47a16c6a3d9fbb70bce47f15 deploy/Dockerfile.runtime-update -4cc0b0813f527ac0ca311993f791005de5c8e89e6810ecbf5cf058e58e2fe572 deploy/build-cn.env +dff739157875c8a70235c4994df868afc6824e0d3861b2716ac28014bb38144f deploy/build-cn.env 3683a3cfb63f08cc9e6144162117ab68c607d38bdeaae0e82cb418e812a450f2 deploy/compose.ubuntu-photos-api.yml d68d727e01c592c098fde71a900bcdd234a9ca6610d0799019a6f585983b4494 deploy/devices.yml.example fd53125a5359dcae1d81e95b8447aac755270f9aeb354d3e1140d7fccb57c42e deploy/docker-compose.photos.yml 4464545a9d91bbc9fff9cd9941db63f44fa61120caca426687d1ed06635cc3e6 deploy/organized-photos-source.json -582dae3db4a08192d714e81e5e8234b170b4ac4231faf251b2c26eeec54f0942 docker-compose.yml +f78d6c3d40e6c5246601bb99c2b46eef91e883a8f83841d85c925394708af0c0 docker-compose.yml 30d3e705870ebb012ce122bd7d8f95192d86664c4d2bb45a3169c86988fbaf72 docs/AUTOMATIC_COLLAGE.md -6b602b686db5b25060f91cfecf05a840a09ee5b2a6d5099636cb3f47880a4a4d docs/BUILDING.md -c14983d5c65775ad80244fcbb77fe5c0c4a413cc27945b7184bbc923a53d38a8 docs/EXTENSIONS.md +5f727c9588504a18009ea892f3204fce9520b03754a56dbc9f59e3505ce8a1b2 docs/BUILDING.md +d9f07164c75c757e85f4ace2b55deaf82b8b904216c5222be6c0e3ab7b28cb36 docs/EXTENSIONS.md 1d8c5f79eff3115b1cd13d8f63fff1332b80d1fe1bec6b0f2713b5f80b0cbf67 docs/KOKONNA_RECOVERY.md -3b4d24d633dea8474a6a7b8868c5afdd37a5205df32356d3967cf838bcafdba9 docs/NAS.md +0fe1896a53cef4cfd2e380a0b979eceda493733082a9b56c3d7e89e7f62a65b3 docs/NAS.md 3280bddcbe2d2721bb7efeb0eb2ec03d9d5ef867c69f3e70066b69f1ad5926e2 docs/PHOTO_SOURCES.md edf44436fd044561539c48480c481e66c1a38164f51ba5226cde04c756f7bacb docs/PHOTO_SOURCE_CONNECTIONS.md 4b00d08eb5a2425b205af13b87f9be04dd6870a734b42463a3e075658039dac9 docs/PUBLICATION.md 5fe4641e75e0746929f71256c6ff11116c777cca8d4288ccc2d7aee91bd7a6ac docs/README.md 5eb19e7e4ed1ceddbdf388fd55697e40f1b7ecd9a4fbdf8ab485132cc684dcb2 docs/REAL_DEVICE_TEST.md bfc38b889fa966031e195c3131db1d4a713e0dc35c644e3047c7ed94f2354700 docs/RESEARCH.md -6772ddfd79b28bb52ce469e40188f8781f66275bdf4b84861f81c32a87aea6eb docs/SYNOLOGY_PHOTOS_API.md +fac36f986e1a7a331d3669aa8e2013e6c0ea7a99c16a61ba3ff573e836aa87e1 docs/SYNOLOGY_PHOTOS_API.md ac0481196e593ff9cca2e5133ff41fb3614268e6a10728a0deca3ad3bfb80b34 docs/integrations/TICKTAR.md -757a4d87aec789054dc5268d314cd218f6d7cab49082cfacef55f9860d2362b0 docs/openapi.json +b191be41aec1dab92ff0d2956d32ec6a325b49786ce9051e5299fe20e2b6b62d docs/openapi.json 54bf7061e0626900ce35e19e81efc5c32c8fd6a0438bdad68f494e1ce81616d4 docs/research/KOKONNA_ACCOUNT_CAPABILITIES.md a07cbe71571c6e2d5a1b94897bd63321c74675c05d15438c4964eef39db0fd76 docs/research/KOKONNA_CONTENT_SUBSCRIPTIONS.md ec53d587b855be84283c50f8fb45be62a4c1caba78b28d436a8260fd171943a2 docs/research/KOKONNA_LOCAL_CONTROL.md 2a68fe95ba4afc24aec6abd3e6ab24a360eb6bc3005ab0fefd696190915d76c1 docs/research/KOKONNA_WIDGET_CATALOG.json e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 pigway_frame_flow/__init__.py e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 pigway_frame_flow/api/__init__.py -632e8be140d1c206388b84e6e08039fc42d6449e83ea23b58d7a57c3404a8747 pigway_frame_flow/api/app.py +06e2e5156383120271ec989b89236bd1baa2d7ba3d6e51b3018de2b23bd39d91 pigway_frame_flow/api/app.py e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 pigway_frame_flow/core/__init__.py fbadf6beaf25b9670dcbf125ec9d192188cf9e076ee4d4a0b5decdaf3834417c pigway_frame_flow/core/accounts.py 818e8fb29d22c13159a6cb22bcbc6b9e211a8a1301588126eb192ac4ed71441b pigway_frame_flow/core/auth.py @@ -84,7 +82,7 @@ a518965aa4db9abeb60865c315229c3053c8e9edd26d80f24dc0af0528bad9e6 pigway_frame_f 2a68fe95ba4afc24aec6abd3e6ab24a360eb6bc3005ab0fefd696190915d76c1 pigway_frame_flow/integrations/kokonna_official/widget_catalog.json aea594fb4e4c9a6e2094d4d7973eb627ac02fb632811b2d1a558a0ee1cd12e77 pigway_frame_flow/integrations/kokonna_official/widgets.py 1fe53f4bfb407b8db774fe730131ecf1213e16d752a3506c4c56b46365ea14e8 pigway_frame_flow/main.py -fb41e20b181815a62e213a1d179f6479b155586fc7000c0a103737ddc16b1c83 pigway_frame_flow/models.py +0a7a96f84c3c1248eadf80ae412df36c71d308771f9f53512f3fc3c9bc6d5976 pigway_frame_flow/models.py e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 pigway_frame_flow/providers/__init__.py b7a6cb8eb40d8dedfbce18777f8fbd433a174f14bcdadf498bd834132aa568fd pigway_frame_flow/providers/cloud_drives.py 58b070f0c814d639a4a912d079479d751d459816d59f2199d0be03e4d573ec3b pigway_frame_flow/providers/external.py @@ -122,12 +120,12 @@ c43faa235848bec6370d7a3d8a7b3475cbd8eb4a6e494518dfaf160ca8f77bfa pigway_frame_f d7d55b06ead173b4c23a277fa39800cadfc18f1163c500e8617cfc1f4ee0dea9 pigway_frame_flow/web/extensions.js fe19e508ad60fab0ab4ca61f5d966f8340143e89f1572de00e78b3f1fed8384b pigway_frame_flow/web/features.js 3c4e3be6321d40547d45724fc3c54af090ebbbc2a41dec717966596577c72c5d pigway_frame_flow/web/i18n.js -0ff0e5c80f6cfb3dc71a63813e01fd8cde5355b0f135a4fbb2781d43383a569f pigway_frame_flow/web/index.html +d4518595bc39adfd88d908f98c8c14c02ffbc67a4289003e792b2ceade2838dd pigway_frame_flow/web/index.html 0609e796b5ca8346405b1bb6e65ac9f88ee163f7a93024d96d54d4e6ab96ba41 pigway_frame_flow/web/style.css cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30 pigway_frame_flow/web/swagger-LICENSE 050bc415ee7048dcd881682678f720264e7da5e373f7461d7c58c755305255f7 pigway_frame_flow/web/swagger-ui-bundle.js 1ac324f7dcd27e4b9386b4bd6421271ec147e922a22c05ba24b11515e9aa6321 pigway_frame_flow/web/swagger-ui.css -a363adb54463d0345cd4e8d0ebe6cac758856e8c0e1d4da49468ddee1ca1aec6 pyproject.toml +da96be4eeb3b57f6805ca766919e3899968179f1e7e6a5cb25dbabfd13bc1569 pyproject.toml d34ac82e4c3feacd38546aa2f0a4491ab17ddbf3ad8f489375783166d78923f0 requirements.lock ae264ad19391bb2515def85963f39458cfa75a8611ff54e5329106c35bd8cc6e scripts/build-verify.sh 43e8345ac5f29c2a5d1fa751c0e7c963b00b5391817393541e72c52919f0f2b4 scripts/check-compose-remote.py @@ -138,9 +136,9 @@ a11e660a0343f2631c1a3dd0aa28471db827fb1f43d1af4217f51209aa3afe32 scripts/create cdabf30144ea089ae8e64228f6762951f53d4fe8c191673fb5676331d249473d scripts/migrate-photos-api.py 80813607835c19d321085fac4cabb17d865d98ed62a09b7f45195ac48bec94a8 scripts/package-nas.py b0b877cfbb2bdf830e2e41097c2057538bb9d0b61a3c39952fe505d910cd5c29 scripts/persistence-check.py -3213ef2c17dbcf7423b04440794b842c691dc65616d56c1e1a85dd286ee34ada scripts/production-deploy.py +b7247c5590fe17ae461311f2fbdbeda0daa8a34c0804cef22a314c7ff8f4c389 scripts/production-deploy.py 11627ecbd8a3be01aa079bdf6166b4368e670f5ad4a165b5971754b8187bbc2c scripts/run_local.py -0fe19e94ffa4e9ff56d4f4a079b19f74991b35f0e18679ac793e8de38ecc3833 scripts/server-release.py +792dafe1ffb2be6a50aba549e642aa95357469ac1ec94a04bdc93a0f419d5f63 scripts/server-release.py 1fc16c43485a60a05012322cb1e4a219309dbd7e11f002f510602beea743ce30 scripts/smoke-test.py 1236d3b2d27559cbe73a27dcf56248ef3c60404c6fb21453caa7225570d4ac45 scripts/ui-audit-regressions.cjs 57a79a534a71442f5e3a70ed9ff96f426007d9f6dd0bb9b1f5d79eac773d2254 scripts/ui-auto-collage.cjs @@ -224,5 +222,6 @@ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 vendor/__init_ 4cae8606769fdb590c427bc1716a871017bf22d91aabf8a52513bf126cfb847f vendor/guooscar_kokonna.py 63c7a2f1a6adf98e7fb83a83ae2d6a2dddcecd7bce4d7de25dd6f26bd496019f vendor/guooscar_protocol.py e5f5b7a5955f2438da131859a08c9d78cda3718ada8a9365a74cbbb9ac5e7853 vendor/metadata.json +6a73f9541c2de74158c0e7cf6b0a58ef774f5a780bf191f2d7ec9cc53efe2bf2 vendor/noto-cjk-OFL-1.1.txt e8cf7d54ea46c19aba793983889b7f7425e1ebfcaaccec764a7db091646e203c vendor/paho-mqtt-EDL-1.0.txt 57938f47d5f05f7de119b17fcecd4509c5d13644c8d5f43db30e45684d09d07e vendor/swagger-metadata.json diff --git a/build-cache/debs/.gitkeep b/build-cache/debs/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/deploy/Dockerfile.runtime-update b/deploy/Dockerfile.runtime-update deleted file mode 100644 index d852625..0000000 --- a/deploy/Dockerfile.runtime-update +++ /dev/null @@ -1,33 +0,0 @@ -# Reuse a reviewed image only when the locked Python dependencies are identical. -# For a fresh installation use the repository's main Dockerfile instead. -ARG BASE_IMAGE -FROM ${BASE_IMAGE} AS prepared -# Cleanup runs only in the disposable preparation stage. -USER 0 -COPY requirements.lock /app/requirements.expected.lock -RUN python -c "from pathlib import Path; assert Path('/app/requirements.lock').read_bytes() == Path('/app/requirements.expected.lock').read_bytes(), 'Dependency lock changed; use the full Dockerfile'" -COPY pigway_frame_flow /app/pigway_frame_flow -COPY vendor /app/vendor -COPY LICENSE NOTICE.md /app/ -# Publish only the cleaned filesystem, without retaining build-stage layers. -RUN rm -rf /usr/local/lib/python*/site-packages/pip \ - /usr/local/lib/python*/site-packages/pip-*.dist-info \ - /usr/local/lib/python*/ensurepip \ - && rm -f /usr/local/bin/pip /usr/local/bin/pip[0-9]* \ - && find / -name .DS_Store -type f -delete - -FROM scratch -COPY --from=prepared / / -ENV PATH=/usr/local/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin LANG=C.UTF-8 -ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 TZ=Asia/Shanghai \ - DATA_DIR=/data CONFIG_DIR=/config -WORKDIR /app -USER 10333:10333 -EXPOSE 18787 -HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ - CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:18787/health',timeout=3)" -CMD ["python", "-m", "uvicorn", "pigway_frame_flow.main:app", "--host", "0.0.0.0", "--port", "18787", "--workers", "1", "--no-access-log"] -ARG BUILD_COMMIT -ARG BUILD_TIME -ENV BUILD_COMMIT=$BUILD_COMMIT BUILD_TIME=$BUILD_TIME -LABEL org.opencontainers.image.title="PIGWay Frame Flow" org.opencontainers.image.version="0.1.19" org.opencontainers.image.revision=$BUILD_COMMIT diff --git a/deploy/build-cn.env b/deploy/build-cn.env index 9c81363..ec4b204 100644 --- a/deploy/build-cn.env +++ b/deploy/build-cn.env @@ -1,5 +1,3 @@ -# Shared mainland-China build profile; source this file before Compose builds. -# Nonempty environment / Gitea repository variables override these defaults. -DEBIAN_MIRROR=${DEBIAN_MIRROR:-mirrors.tuna.tsinghua.edu.cn} -DEBIAN_SECURITY_MIRROR=${DEBIAN_SECURITY_MIRROR:-mirrors.tuna.tsinghua.edu.cn} -export DEBIAN_MIRROR DEBIAN_SECURITY_MIRROR +# Shared mainland-China build profile; source before Compose builds. +ALPINE_MIRROR=${ALPINE_MIRROR:-mirrors.tuna.tsinghua.edu.cn} +export ALPINE_MIRROR diff --git a/docker-compose.yml b/docker-compose.yml index 6b988d3..26a82a4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,8 +7,7 @@ services: build: context: . args: - DEBIAN_MIRROR: ${DEBIAN_MIRROR:-deb.debian.org} - DEBIAN_SECURITY_MIRROR: ${DEBIAN_SECURITY_MIRROR:-deb.debian.org} + ALPINE_MIRROR: ${ALPINE_MIRROR:-dl-cdn.alpinelinux.org} DOCKER_PIP_INDEX_URL: ${DOCKER_PIP_INDEX_URL:-https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple} DOCKER_PIP_FALLBACK_INDEX_URL: ${DOCKER_PIP_FALLBACK_INDEX_URL:-https://pypi.org/simple} BUILD_COMMIT: ${BUILD_COMMIT:-development} diff --git a/docs/BUILDING.md b/docs/BUILDING.md index 6d9a8e3..9d3f3b5 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -2,50 +2,30 @@ ### Build networking and cache -The Dockerfile retains the pinned Python base, locked Python dependencies, -`fonts-noto-cjk`, Debian repository signature checks, and offline artifact -SHA256 verification. Production uses `scripts/build-verify.sh`; CI uses `scripts/ci-compose-verify.py`. -Both build the same Dockerfile with BuildKit and retain `docker compose build --pull`. +The runtime uses an official Python 3.14 Alpine 3.24 image pinned by digest, +locked Python dependencies, and `font-noto-cjk` for Chinese rendering. APK +repository signatures and offline wheel SHA256 checks remain enabled. +Production uses `scripts/build-verify.sh`; CI uses `scripts/ci-compose-verify.py`. +Both build the same Dockerfile with BuildKit and `docker compose build --pull`. -APT mirror hosts are configurable through `DEBIAN_MIRROR` and -`DEBIAN_SECURITY_MIRROR` (hosts only, without scheme or path). Defaults are -`deb.debian.org`; HTTPS is used for both Debian and security repositories. -For mainland-China NAS builds, use the shared profile: +`ALPINE_MIRROR` accepts a mirror host (without scheme or path), defaulting to +`dl-cdn.alpinelinux.org`. For mainland-China builds: ```sh . ./deploy/build-cn.env ./scripts/build-verify.sh ``` -CI loads this same profile, using Tsinghua TUNA for both repositories unless -Gitea repository variables `DEBIAN_MIRROR` / `DEBIAN_SECURITY_MIRROR` override -it. Production may also set the two hosts directly in `.env` when calling the -build script without the profile. An alternate mainland mirror is -`mirrors.ustc.edu.cn`; availability and throughput depend on the Runner network. -No fallback disables signature verification or removes packages. +CI uses this profile; the Gitea `ALPINE_MIRROR` repository variable can override +it. APK upgrades available packages during a fresh dependency-layer build. +Use `docker compose build --pull --no-cache` to refresh OS packages explicitly. -Layer order stays: pinned base → APT/font dependencies → `requirements.lock` -→ pip dependencies → application/vendor source → build metadata. -A source-only change therefore reuses the APT and pip layers, provided the -same builder, base digest, mirror arguments, and optional offline artifacts -are used. Changing the lockfile rebuilds pip and later layers, not APT. -Changing the mirror or APT instructions rebuilds APT and later layers. - -Previously an APT layer miss redownloaded indexes/packages, and pip used -`--no-cache-dir`. Now locked BuildKit mounts retain APT indexes, downloaded -`.deb` files, and pip downloads/wheels outside the final image. Debian's -`docker-clean` hook is disabled so it cannot discard cached packages. -Re-executed APT steps still run `apt-get update` and validate repository -signatures; cached packages and pip artifacts can avoid repeat large downloads. -The initial build after this Dockerfile change will miss dependency layers; -subsequent builds benefit. No measured speedup is claimed before CI completes. - -Caches belong to the Docker builder, not the ephemeral CI workspace. Keep the -NAS Runner's builder storage across jobs; removing the builder, pruning caches, -or using a fresh Docker daemon loses the benefit. Cache mounts are disposable: -an empty cache still builds correctly. `--pull` remains enabled; a pinned base -digest check alone does not invalidate dependency layers. To deliberately -refresh installed APT versions, use `docker compose build --pull --no-cache`. +Layer order is pinned base → APK/fonts → requirements → Python dependencies +→ application source → metadata. BuildKit caches pip downloads separately for +musl and architecture. Optional `build-cache/wheels/` must contain compatible +musllinux wheels and their SHA256 manifest; empty-cache builds work normally. +Do not reuse Debian/manylinux wheels on Alpine. Caches are disposable builder +resources and are not runtime mounts or dependencies. ### CI with a host Docker socket @@ -94,8 +74,8 @@ certificate environment settings, and proxy settings remain in effect. These `CI_PIP_*` settings apply only to the Job's pip install. They are not passed into Docker build arguments or persisted into the production image. The Dockerfile keeps its own cached/offline dependency installation and logs -the Docker pip index host for online installs. Debian mirror variables affect -APT only and are loaded later in the Docker verification step. +the Docker pip index host for online installs. The Alpine mirror variable affects +APK only and are loaded later in the Docker verification step. If dependency installation fails, the always() cleanup step still executes. It skips the Python/PyYAML cleanup script when no CI Compose manifest exists, diff --git a/docs/EXTENSIONS.md b/docs/EXTENSIONS.md index f5034fd..3d334a6 100644 --- a/docs/EXTENSIONS.md +++ b/docs/EXTENSIONS.md @@ -81,7 +81,7 @@ A failed mirror installation retries the same locked requirements against the fallback, without extra indexes, version downgrades, or disabling build isolation. Logs show index hosts and redact URL credentials/query parameters. -APT/font layers remain cached when Python dependencies change. The Python layer +APK/font layers remain cached when Python dependencies change. The Python layer changes only when the lock, installer, base image, or mirror arguments change; application source changes reuse it. BuildKit pip cache survives layer misses. An explicitly supplied, checksum-verified offline wheel directory still takes diff --git a/docs/NAS.md b/docs/NAS.md index 2989ab0..37f7afc 100644 --- a/docs/NAS.md +++ b/docs/NAS.md @@ -128,23 +128,19 @@ all uncompressed rootfs diff IDs before loading. With the verified local tag loa ```sh sudo env DOCKER_BUILDKIT=1 /usr/local/bin/docker compose build \ - --build-arg PYTHON_BASE=python:3.14.5-slim-bookworm + --build-arg PYTHON_BASE=python:3.14.8-alpine3.24 ``` This per-build override does not change global registry or network configuration. -A slow Debian CDN can be overridden only for this build, retaining APT GPG +A slow Alpine CDN can be overridden for this build, retaining APK signature verification and changing no NAS system settings: ```sh -sudo /usr/local/bin/docker compose build --build-arg DEBIAN_MIRROR=mirrors.aliyun.com +sudo /usr/local/bin/docker compose build --build-arg ALPINE_MIRROR=mirrors.tuna.tsinghua.edu.cn ``` -Optional `build-cache/debs/fonts-noto-cjk.deb` avoids a slow build-network download. -Dockerfile verifies it against SHA-256 from the GPG-authenticated APT index before use. -The cache is not a runtime mount or committed dependency, and empty-cache builds work. - -Optional `build-cache/wheels/` can hold all 24 Linux AMD64/Python 3.14 wheels +Optional `build-cache/wheels/` can hold all locked Linux AMD64/Python 3.14 musllinux wheels and `SHA256SUMS`, verified against official PyPI release metadata. Builds verify the manifest and use pip `--no-index`; incomplete caches fail explicitly. Build commit/time are set after dependency installation to preserve build cache. diff --git a/docs/SYNOLOGY_PHOTOS_API.md b/docs/SYNOLOGY_PHOTOS_API.md index 136d549..be2b089 100644 --- a/docs/SYNOLOGY_PHOTOS_API.md +++ b/docs/SYNOLOGY_PHOTOS_API.md @@ -35,7 +35,7 @@ FRAME_FLOW_IMAGE='<已验证的镜像标签>' docker compose -f deploy/compose.ubuntu-photos-api.yml config --quiet ``` -完整构建使用根目录 Dockerfile。若使用 `deploy/Dockerfile.runtime-update`,必须指定已审核的基础镜像,构建步骤会检查依赖锁与基础镜像相同;不相同时必须完整构建。本次依赖未新增,已比对现有 0.1.8 基础镜像的锁文件。 +发布统一使用根目录 Dockerfile 完整构建,包含固定基础映像、系统安全更新和锁定依赖,避免继承旧映像中的系统漏洞。 ## 旧来源转换与恢复 diff --git a/docs/openapi.json b/docs/openapi.json index 2b80f35..b19b288 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "PIGWay Frame Flow API", - "version": "0.1.19" + "version": "0.1.21" }, "paths": { "/api/v1/devices/{device_id}/widgets/selection": { @@ -15227,7 +15227,7 @@ "version": { "type": "string", "title": "Version", - "default": "0.1.19" + "default": "0.1.21" } }, "type": "object", @@ -17490,7 +17490,7 @@ "version": { "type": "string", "title": "Version", - "default": "0.1.19" + "default": "0.1.21" }, "api_version": { "type": "string", diff --git a/pigway_frame_flow/api/app.py b/pigway_frame_flow/api/app.py index d19476f..1cbb669 100644 --- a/pigway_frame_flow/api/app.py +++ b/pigway_frame_flow/api/app.py @@ -126,7 +126,7 @@ def create_app(settings=None, start_worker=True): app = FastAPI( title="PIGWay Frame Flow API", docs_url=None, - version="0.1.20", + version="0.1.21", lifespan=lifespan, responses=error_responses, ) @@ -1040,7 +1040,7 @@ def create_app(settings=None, start_worker=True): supports_upload=True, ), ], - version="0.1.20", + version="0.1.21", mock=settings.mock, write_enabled=settings.write_enabled, api_key_configured=bool(settings.api_key) diff --git a/pigway_frame_flow/models.py b/pigway_frame_flow/models.py index 96f4b33..eaf447e 100644 --- a/pigway_frame_flow/models.py +++ b/pigway_frame_flow/models.py @@ -366,7 +366,7 @@ class SettingsView(BaseModel): class Health(BaseModel): status: str - version: str = "0.1.20" + version: str = "0.1.21" class Readiness(BaseModel): @@ -397,7 +397,7 @@ class RestoreRequest(StrictModel): class Version(BaseModel): product: str = "PIGWay Frame Flow" - version: str = "0.1.20" + version: str = "0.1.21" api_version: str = "v1" commit: str = "development" build_time: str = "unknown" diff --git a/pigway_frame_flow/web/index.html b/pigway_frame_flow/web/index.html index 5085d1b..dec19e0 100644 --- a/pigway_frame_flow/web/index.html +++ b/pigway_frame_flow/web/index.html @@ -1,3 +1,3 @@ PIGWay Frame Flow -
PIGWay Frame Flow

+
PIGWay Frame Flow

diff --git a/pyproject.toml b/pyproject.toml index f6df8f6..d1bde71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pigway-frame-flow" -version = "0.1.20" +version = "0.1.21" readme = "README.md" license = {file = "LICENSE"} requires-python = ">=3.14,<3.15" diff --git a/scripts/production-deploy.py b/scripts/production-deploy.py index 0c934bf..7c94bdb 100644 --- a/scripts/production-deploy.py +++ b/scripts/production-deploy.py @@ -375,9 +375,7 @@ class Deploy: "DOCKER_PIP_FALLBACK_INDEX_URL" ) or "https://pypi.org/simple", - "DEBIAN_MIRROR": self.env.get("DEBIAN_MIRROR") or "deb.debian.org", - "DEBIAN_SECURITY_MIRROR": self.env.get("DEBIAN_SECURITY_MIRROR") - or "deb.debian.org", + "ALPINE_MIRROR": self.env.get("ALPINE_MIRROR") or "dl-cdn.alpinelinux.org", "BUILD_COMMIT": commit, "BUILD_TIME": now, }.items(): diff --git a/scripts/server-release.py b/scripts/server-release.py index 16d1d61..21061cf 100644 --- a/scripts/server-release.py +++ b/scripts/server-release.py @@ -10,6 +10,7 @@ import shutil import sqlite3 import subprocess import time +import tomllib import urllib.request from pathlib import Path @@ -139,9 +140,6 @@ def release(lock): before = inspect() old_env = dict(s.split("=", 1) for s in before["Config"]["Env"]) verify_runtime(old_env["BUILD_COMMIT"]) - assert run("docker", "exec", CONTAINER, "cat", "/app/requirements.lock") == ( - (ctx / "requirements.lock").read_text().strip() - ), "Dependencies differ; full build required" stamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%dT%H%M%SZ") backup = RECOVERY / ("release-" + stamp + "-" + commit[:12]) backup.mkdir(mode=0o700) @@ -153,29 +151,26 @@ def release(lock): with sqlite3.connect(backup / "frameflow.before.db") as dst: src.backup(dst) assert dst.execute("pragma quick_check").fetchone()[0] == "ok" - base = "pigway-frame-flow:release-base-" + commit[:12] - run("docker", "image", "tag", before["Image"], base) - image = "pigway-frame-flow:0.1.19-" + commit[:12] - try: - run( - "docker", - "build", - "--pull=false", - "--network=none", - "-f", - str(ctx / "deploy/Dockerfile.runtime-update"), - "--build-arg", - "BASE_IMAGE=" + base, - "--build-arg", - "BUILD_COMMIT=" + commit, - "--build-arg", - "BUILD_TIME=" + stamp, - "-t", - image, - str(ctx), - ) - finally: - run("docker", "image", "rm", base) + version = tomllib.loads((ctx / "pyproject.toml").read_text())["project"]["version"] + assert re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", version) + image = "pigway-frame-flow:" + version + "-" + commit[:12] + # A source-only overlay would retain vulnerable packages from the old image. + run( + "docker", + "build", + "--pull", + "-f", + str(ctx / "Dockerfile"), + "--build-arg", + "BUILD_COMMIT=" + commit, + "--build-arg", + "BUILD_TIME=" + stamp, + "--build-arg", + "ALPINE_MIRROR=" + os.getenv("ALPINE_MIRROR", "dl-cdn.alpinelinux.org"), + "-t", + image, + str(ctx), + ) compose = json.loads((APP / "compose.json").read_text()) service = compose["services"][CONTAINER] service["image"] = image diff --git a/vendor/noto-cjk-OFL-1.1.txt b/vendor/noto-cjk-OFL-1.1.txt new file mode 100644 index 0000000..d952d62 --- /dev/null +++ b/vendor/noto-cjk-OFL-1.1.txt @@ -0,0 +1,92 @@ +This Font Software is licensed under the SIL Open Font License, +Version 1.1. + +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font +creation efforts of academic and linguistic communities, and to +provide a free and open framework in which fonts may be shared and +improved in partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply to +any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software +components as distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, +deleting, or substituting -- in part or in whole -- any of the +components of the Original Version, by changing formats or by porting +the Font Software to a new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, +modify, redistribute, and sell modified and unmodified copies of the +Font Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, in +Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the +corresponding Copyright Holder. This restriction only applies to the +primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created using +the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE.