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
37 changes: 34 additions & 3 deletions .claude/skills/forge-ci/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,10 @@ wire CI + local to the fix so recipes validate BEFORE the PR:
builds also need `device_info_plus`/`connectivity_plus` pins, but keep those OUT of
the committed template — add post-staging; CI's Xcode doesn't need them.)
- The on-device iOS/APK test in CI still uses PUBLISHED sp, so it can't pass until the
release — dispatch wheel builds with `mobile_test_pythons=""` and verify the recipe
LOCALLY (git or path sp override + local flet-cli + `--python-version 3.12`; see the
local-recipe-testing skill). CI goes green only after the sp/python-build release.
release — dispatch wheel builds with `mobile_test_pythons=none` (see the footgun below)
and verify the recipe LOCALLY (git or path sp override + local flet-cli +
`--python-version 3.12`; see the local-recipe-testing skill). CI goes green only after
the sp/python-build release.

## Watching a run without babysitting

Expand All @@ -237,3 +238,33 @@ runs take 30min–3h) and print the conclusion + failed-job list when it
completes. One watcher per run; kill stale watchers when you supersede a run,
and remember a killed multi-line script may still fire its remaining lines —
put dispatches *before* long build loops in scripts, or in separate commands.

## Deploying (publishing wheels to pypi.flet.dev)

Publishing is gated in `build-wheels-version.yml`'s **Publish wheels** step. It fires when
either (a) `inputs.publish == true` (an explicit deploy dispatch), OR (b) the automatic path
— a **push to `main`** with an **empty `python_build_run_id`**. `publish_to_pypi dist/*.whl`
is **per matrix-job and 409-tolerant**: each (python × platform) job publishes its own wheels
at the end, already-published (version+build) wheels 409-skip, and one job failing never
blocks another's publish.

- **Deploy via dispatch:** run on **origin (`flet-dev/mobile-forge`)** — that's where
`GEMFURY_TOKEN` lives; a `publish=true` run on a fork builds but the publish step has no
token. `gh workflow run build-wheels.yml -R flet-dev/mobile-forge --ref <branch> -f
packages=… -f publish=true -f mobile_test_pythons=none -f python_build_run_id=<run w/
dc76612> -f serious_python_ref=main`. (The `publish` boolean input is read from the
dispatched **ref**, so it works even before it's on `main`.)
- **Bump before republishing an already-published version.** A recipe published at
`<ver>-<build>` will 409-skip on re-publish, so a forge/loader fix won't reach pypi.flet.dev
until the **build number is bumped** (pymongo/onnxruntime 1→2 for the fix_wheel fixes).
- **Only ONE genuine build-order coupling** across a deploy set is `host_build` (build-time
static-link) deps — e.g. `jq` needs `flet-libjq`; put it in `prebuild_recipes` (it's built
into dist/ AND published). Runtime deps (opaque→pysodium, ncnn→opencv) don't need ordering
when tests are off. Scan with `grep -A6 host_build: recipes/*/meta.yaml`.
- **Verify against pypi.flet.dev, not job conclusions.** A *cancelled* run's "success" jobs
are unreliable (early-cancel can leave gaps), and a per-job artifact-upload flake
(`Failed to CreateArtifact: … ENOTFOUND`) fails the job → skips its Publish. Check the
actual wheels — **with the filename normalized `-`→`_`** in the distribution name
(`opencv_python-…`, `scikit_learn-…`, `flet_libjq-…`; the project *URL* keeps the hyphen).
Allow a few minutes for Gemfury indexing after a Publish step reports `[success]`. Account
for single-platform recipes (`platforms: [ios]`, e.g. pyobjus → no android wheels expected).
Original file line number Diff line number Diff line change
Expand Up @@ -1295,8 +1295,16 @@ if _sub is not None and getattr(_sub, "origin", None):
return
# else fall through to the stock loader (desktop / non-0.86)
```
Pair with **`extract_packages: [cv2]`** in meta.yaml (the extra-submodule scan does
`os.listdir()` on the package dir, so cv2 must ship extracted, not zipped). On
The recipe also declares **`extract_packages: [cv2]`**, but that is **defensive, not
required for core cv2**: this loader loads the native `cv2` directly (no `config.py`
read, no dir scan), and the only path that touches the package dir — cv2's OPTIONAL
extra-submodule scan (`__collect_extra_submodules` → `os.listdir`, for `cv2.gapi`
etc.) — is wrapped in `try/except` and **fails silently** when zipped. So `import cv2`
+ image ops (`imdecode`, …) work fine from `sitepackages.zip` WITHOUT `extract_packages`
(verified: a consumer app with no EP classified on-device). Add `extract_packages: [cv2]`
only if you use one of those extra submodules. Consumer apps must set it themselves in
`[tool.flet.android] extract_packages` — the recipe's meta value only reaches the
recipe-tester (via `stage_recipe.sh`), not `pip install` consumers. On
desktop `find_spec('cv2.cv2')` is None → stock loader runs unchanged. General tell:
any package whose loader **re-imports its own native under a specific top-level
name** — reproduce the exact name via a fresh `ExtensionFileLoader(name, origin)`,
Expand Down
14 changes: 9 additions & 5 deletions .github/workflows/build-wheels-version.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ on:
description: |
Python versions (comma-separated) whose recipe mobile tests should actually run.
Versions in `python_versions` that aren't in this list still build wheels but skip the
APK/iOS-simulator test stage. Use "ALL" to run tests for every entry in `python_versions`:
APK/iOS-simulator test stage. Use "ALL" to run tests for every entry in `python_versions`.
Empty (or any value matching no python, e.g. "none") disables all mobile tests:
required: false
type: string
default: "3.12"
Expand Down Expand Up @@ -104,7 +105,8 @@ on:
description: |
Python versions (comma-separated) whose recipe mobile tests should actually run.
Versions in `python_versions` that aren't in this list still build wheels but skip the
APK/iOS-simulator test stage. Use "ALL" to run tests for every entry in `python_versions`:
APK/iOS-simulator test stage. Use "ALL" to run tests for every entry in `python_versions`.
Empty (or any value matching no python, e.g. "none") disables all mobile tests:
required: false
default: "3.12"
python_build_run_id:
Expand Down Expand Up @@ -429,7 +431,9 @@ jobs:
fi

# Gate on the caller-provided mobile_test_pythons list: only Python
# versions in that list actually run the APK/iOS-sim test step.
# versions in that list actually run the APK/iOS-sim test step. An empty
# list (or a non-matching sentinel like "none") matches no python, so
# every leg skips — i.e. mobile_test_pythons="" disables all mobile tests.
if [[ "$MOBILE_TEST_PYTHONS" != "ALL" ]]; then
allowed=""
for v in $(echo "$MOBILE_TEST_PYTHONS" | tr ',' ' '); do
Expand Down Expand Up @@ -523,7 +527,7 @@ jobs:

cd tests/recipe-tester
PIP_FIND_LINKS="$GITHUB_WORKSPACE/dist-test" \
uvx --prerelease allow --with 'flet-cli>=0.86.0.dev0' --with 'flet>=0.86.0.dev0' flet build apk --arch x86_64 -vv --yes --python-version "$(echo "$INPUT_PYTHON_VERSION" | cut -d. -f1-2)"
uvx --prerelease allow --with 'flet-cli' --with 'flet' flet build apk --arch x86_64 -vv --yes --python-version "$(echo "$INPUT_PYTHON_VERSION" | cut -d. -f1-2)"

- name: Test on Android emulator (API 28, x86_64)
if: matrix.platform == 'android' && steps.detect-tests.outputs.has_tests == 'true'
Expand Down Expand Up @@ -586,7 +590,7 @@ jobs:

cd tests/recipe-tester
PIP_FIND_LINKS="$GITHUB_WORKSPACE/dist-test" \
uvx --prerelease allow --with 'flet-cli>=0.86.0.dev0' --with 'flet>=0.86.0.dev0' flet build ios-simulator -vv --yes --python-version "$(echo "$INPUT_PYTHON_VERSION" | cut -d. -f1-2)"
uvx --prerelease allow --with 'flet-cli' --with 'flet' flet build ios-simulator -vv --yes --python-version "$(echo "$INPUT_PYTHON_VERSION" | cut -d. -f1-2)"

- name: Test on iOS Simulator
if: matrix.platform == 'ios' && steps.detect-tests.outputs.has_tests == 'true'
Expand Down
13 changes: 11 additions & 2 deletions .github/workflows/build-wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ on:
description: |
Python versions (comma-separated) whose recipe mobile tests should actually run.
Versions in `python_versions` that aren't in this list still build wheels but skip the
APK/iOS-simulator test stage. Use "ALL" to run tests for every entry in `python_versions`:
APK/iOS-simulator test stage. Use "ALL" to run tests for every entry in `python_versions`.
Pass an empty string to build wheels WITHOUT any on-device tests (build-only deploy):
required: false
default: "3.12"
python_build_run_id:
Expand Down Expand Up @@ -199,7 +200,15 @@ jobs:
archs: ${{ inputs.archs || 'android,iOS' }}
packages: ${{ needs.detect.outputs.packages }}
prebuild_recipes: ${{ inputs.prebuild_recipes || '' }}
mobile_test_pythons: ${{ inputs.mobile_test_pythons || '3.12' }}
# A non-empty value is honored verbatim (list of test legs, or "ALL"). push/PR
# carry no inputs, so those default to the 3.12 on-device test. On
# workflow_dispatch/workflow_call an EXPLICIT empty string disables all mobile
# tests — mapped to the `none` sentinel so it survives the reusable-workflow
# boundary and never matches a python in the child's gate (`none` works too).
mobile_test_pythons: >-
${{ inputs.mobile_test_pythons != '' && inputs.mobile_test_pythons
|| ((github.event_name == 'push' || github.event_name == 'pull_request') && '3.12')
|| 'none' }}
python_build_run_id: ${{ inputs.python_build_run_id || '' }}
# No `|| default` here: an explicitly-passed empty string must survive so a
# caller can disable the override and test against the published sp.
Expand Down
17 changes: 17 additions & 0 deletions recipes/pyclipper/meta.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package:
name: pyclipper
version: "1.4.0"

build:
number: 1
script_env:
_PYTHON_SYSCONFIGDATA_NAME: '{sysconfigdata_name}'

# {% if sdk == 'android' %}
requirements:
host:
# The extension is C++ (Cython language="c++" over the vendored Clipper
# sources) -> libc++_shared.so at runtime on Android (srsly/murmurhash
# wiring).
- flet-libcpp-shared >=27.2.12479018
# {% endif %}
51 changes: 51 additions & 0 deletions recipes/pyclipper/tests/test_pyclipper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
def test_intersection():
"""pyclipper is a Cython C++ extension over the vendored Clipper library.
Clip a triangle against a square — the shape rapidocr uses it for
(text-box postprocessing) is exactly this kind of polygon math."""
import pyclipper

subj = [[180, 200], [260, 200], [260, 150], [180, 150]] # square
clip = [[190, 210], [240, 210], [240, 130], [190, 130]] # square

pc = pyclipper.Pyclipper()
pc.AddPath(clip, pyclipper.PT_CLIP, True)
pc.AddPath(subj, pyclipper.PT_SUBJECT, True)
solution = pc.Execute(pyclipper.CT_INTERSECTION)

assert len(solution) == 1
# intersection of the two squares is the 50x50 box (190..240, 150..200)
assert sorted(map(tuple, solution[0])) == [
(190, 150),
(190, 200),
(240, 150),
(240, 200),
]


def test_offset():
"""PyclipperOffset (polygon dilation) — rapidocr's unclip step."""
import pyclipper

pco = pyclipper.PyclipperOffset()
pco.AddPath(
[[0, 0], [100, 0], [100, 100], [0, 100]],
pyclipper.JT_MITER,
pyclipper.ET_CLOSEDPOLYGON,
)
solution = pco.Execute(10)

assert len(solution) == 1
xs = [p[0] for p in solution[0]]
ys = [p[1] for p in solution[0]]
assert min(xs) == -10 and max(xs) == 110
assert min(ys) == -10 and max(ys) == 110


def test_scale_helpers():
"""The float<->int fixed-point helpers used by every real caller."""
import pyclipper

path = [(0.5, 0.25), (1.5, 2.75)]
scaled = pyclipper.scale_to_clipper(path)
back = pyclipper.scale_from_clipper(scaled)
assert [tuple(p) for p in back] == [(0.5, 0.25), (1.5, 2.75)]
4 changes: 2 additions & 2 deletions tests/recipe-tester/pyproject.toml.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description = "Generic in-app pytest runner for mobile-forge recipe wheels."
requires-python = ">=3.10"

dependencies = [
"flet>=0.86.0.dev0",
"flet >=0.86.0",
"pytest",
# `stage_recipe.sh` rewrites the line below to pin the recipe under test (e.g. `"numpy==2.2.2"`),
# and replaces the token line after it with any test-only deps declared in
Expand All @@ -16,7 +16,7 @@ dependencies = [

[dependency-groups]
dev = [
"flet[all]>=0.86.0.dev0",
"flet[all] >=0.86.0",
]

[tool.flet]
Expand Down
Loading