`flet build` app packaging: incomplete default exclusions, and no way to see what was dropped
Summary
flet build copies the app directory into the package with a single default exclusion — build — so a project whose app root is the project root ships its virtualenv, .git, editor directories, stray files and previous build output to end users. Separately, what is dropped happens across four phases with two different matchers, and none of it is reported or documented, so a user cannot find out what Flet actually removed from their app.
Two halves: a concrete defect (part 1) and a design question for discussion (part 2).
Part 1 — the defect
build_base.py:2619:
exclude_list = ["build"]
# + user's --exclude / [tool.flet.app.exclude] / [tool.flet.<platform>.app.exclude]
# + "assets" when target_platform == "web"That is the entire filter. Nothing excludes .venv, venv, .git, .flet, .idea, .vscode, .mypy_cache, .ruff_cache or .pytest_cache.
Measured — web
A minimal app (one main.py, two small assets, flet + flet-lottie) with a .venv at the project root — the layout uv init and python -m venv .venv produce:
build/web/assets/app/app.zip |
39,963,366 bytes |
| uncompressed | 95.17 MB |
.venv share |
2985 of 3384 entries (92.63 MB) |
inside .venv |
1793 .pyc, 0 .py |
The last row matters: 1793 .pyc and zero .py means compileall walked the entire virtualenv before it was packaged. The venv is compiled and shipped.
Measured — macOS (same project, flet build macos)
.app bundle |
310 MB |
| app payload inside the bundle | 184 MB |
.venv |
119 MB |
| a stray previous build output | 65 MB |
The full packaged payload was .flet .keep .venv assets build-macos.log build.log main.pyc pyproject.toml quiet_app.pyc runweb.log runweb2.log runweb3.log server-mode-client t.log v.log v1.log. Everything the app actually needs — main.pyc, pyproject.toml, assets/ — is a rounding error against that. Note .flet/ in there: the dev-storage directory flet run creates, i.e. the development database and cache, shipped to end users.
Scope
Every platform, and the delivery model differs per platform without changing the outcome. Per the 0.86 guide App files ship unpacked in a read-only bundle:
- macOS / iOS / Windows / Linux — app files ship unpacked inside the bundle, next to the stdlib and site-packages. No
app.zip, no first-launch extraction. Confirmed above:.venvsits unpacked atContents/Resources/serious_python_darwin_*.bundle/Contents/Resources/app/.venv. - Android — the app ships as a stored (uncompressed)
app.zipasset inside the APK and is unpacked once, version-keyed, to the app's files directory on the first launch after an install or update. So on Android the bytes are paid for twice: in the download and again on device. - Web — unchanged;
app.zipis fetched by the browser before the app starts.
All 8 flet build targets go through package_python_app(), as do flet debug and flet test. The only platform-conditional line in the exclusion list is assets on web.
Who is affected
flet create is immune: its template sets [tool.flet.app] path = "src", so a project-root .venv sits above the packaged directory.
It hits projects whose app root is the project root — hand-rolled layouts, and 536 of 538 example pyproject.toml files in this repo. No CI covers it.
Prior reports, both closed without changing the default:
- #5983 — 2.8 GB / 17,755 files, down to 261 MB / 82 files once the venv was excluded
- #6135 — 189 MB APK whose 127 MB
app.zipcontained.venv,.venv mobile,.venv desktopand.idea
flet publish already avoids this by a different route: filter_tar drops every top-level dot-entry (publish.py:299-309), so .venv never reaches its output. It has the mirror-image hole instead — it never drops build/. The two packagers have complementary blind spots.
The docs currently recommend the workaround rather than shipping the default (website/docs/publish/index.md:1444, and the canonical example is flet build <target_platform> --exclude .git .venv).
Proposed fix
Detect virtual environments structurally rather than by name: scan the app root for directories containing pyvenv.cfg (PEP 405 — written by python -m venv, uv venv and virtualenv alike; verified for all three, with a decoy package named env correctly left alone). Add a small name list for entries that are tool-reserved and have no marker: .git, .flet, .idea, .vscode, .mypy_cache, .ruff_cache, .pytest_cache.
Merge with the user's list using dict.fromkeys(DEFAULTS + user), mirroring ANDROID_DEFAULT_EXTRACT_PACKAGES in the same file.
Two constraints worth recording, both verified:
- Nested paths do work —
copyDirectorypassesrootDirunchanged through the recursion, so--exclude src/.venvmatches at depth. - The separator does not. Dart's
path.relativereturnssrc\.venvon Windows, so any multi-segment value Flet emits must beos.sep-joined.
Opt-out
Today user configuration can only extend exclude_list; there is no way to un-exclude. Since this change makes Flet drop more by default, it should ship with --no-default-excludes (and a pyproject.toml equivalent) so anyone we guess wrong about is not forced to pin an older Flet.
Part 2 — what is actually dropped, and why you cannot see it
build is not the whole story, but the rest is not expressible as exclusions either. Four phases, two matchers:
| phase | mechanism | matcher | default | applies to |
|---|---|---|---|---|
| copy | --exclude |
exact relative path | build (+ assets on web) |
app dir |
| compile | --compile-app |
— | ON | app dir — every .py deleted, replaced by .pyc |
| cleanup app | --cleanup-app |
globs | OFF | app dir |
| cleanup packages | --cleanup-packages |
globs | ON | site-packages |
The glob set (serious_python junkFiles) is **.c, **.h, **.cpp, **.hpp, **.typed, **.pyi, **.pxd, **.pyx, **.a, **.pdb, __pycache__, **/__pycache__ — plus **.exe, **.dll, bin, **/bin on mobile. Defaults are at build_base.py:2664-2677.
Should the glob set be folded into the default exclusion list?
Proposed answer: no — but the need behind the question is real.
Against folding:
--excludecannot express globs. It isexcludeList.contains(relativePath);**.pyiis unrepresentable.- They run in different phases. Exclusions happen before the copy; cleanup after copy and compile. Moving them changes build cost and semantics, not just presentation.
- They mostly apply to different trees. The one that is ON by default (
cleanup-packages) does not touch the app at all — it prunes dependencies. - It would couple Flet's defaults to a serious_python internal constant that can change on any version bump.
A nuance worth stating, because it reframes the question: for the app directory, the only thing dropped by default beyond build/assets is .py (turned into .pyc). cleanup-app is off, so a user's own .pyi, .c and bin/ all ship. The hidden pruning is in site-packages.
Proposed instead: report, and document
- One line at default verbosity naming the default exclusions that actually matched something on disk. "We silently deleted your virtualenv" is exactly the kind of thing that should not be silent — especially as a new default.
- The full picture at
-v, including the cleanup globs and which tree each applied to. - Document the four phases.
website/docs/publish/index.md:1444currently says only thatbuildis always excluded, and never mentions the cleanup phase. - Possibly a packaging manifest /
--dry-run: "show me what would go into the package, and what was dropped and why". That answers the question durably and reflects the user's actual project, which a static docs list never will. Floated for discussion rather than proposed.
Open questions for the team
.env— exclude, or ship and warn? It is the classic secret file, but python-dotenv apps read it at runtime, so silently dropping it turns a size bug into "worked locally, broke when built". Leaning warn.- Is
cleanup-appOFF whilecleanup-packagesis ON deliberate? It is asymmetric and undocumented, and it is why a user's own.pyi/.cship. - How loud should the report be? One line always, or only under
-v? - Should
flet publishgain the same defaults, andflet buildgain publish's dot-entry rule, so the two packagers stop having complementary blind spots?
Notes
- Already-built artifacts are unaffected by a fix; anyone who has shipped a bundle containing
.venvor.fletmay want to check what went out. - A changelog entry should call out the behaviour change explicitly, since apps that (unwisely) relied on something dot-prefixed being packaged would change behaviour on upgrade.
Source: flet-dev/flet