Skip to content

Commit ac45cd9

Browse files
flet pack CLI to create app bundles with custom icons and metadata (#770)
* Trying to wrap PyInstaller * Version package args * Copy bin dir * Patch icon and version info * certifi==2022.12.7 * Try removing certifi pin * Update main exec version info * Move Windows utils into a separate module * Update icon on macOS * Patching plist and signing app bundle * Remove icon name field * Rename Flet.app to a custom bundle name * Update CFBundleIdentifier * --onefile is always forced * Update pack.py * Update win_utils.py * Fix --name arg description
1 parent ff35962 commit ac45cd9

File tree

12 files changed

+504
-96
lines changed

12 files changed

+504
-96
lines changed

.appveyor.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,6 @@ for:
398398
install:
399399
- python --version
400400
- cd sdk/python
401-
- pip install certifi==2022.9.24
402401
- pip install pdm
403402
- pdm install
404403

@@ -417,7 +416,6 @@ for:
417416
install:
418417
- python --version
419418
- cd sdk/python
420-
- pip install certifi==2022.9.24
421419
- pip install --upgrade setuptools wheel twine pdm
422420
- pdm install
423421

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
temp_bin_dir = None
Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import os
22

3-
# package entire "bin" folder
4-
bin_path = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, "bin"))
3+
import flet.__pyinstaller.config as hook_config
4+
from flet.__pyinstaller.utils import get_flet_bin_path
55

6-
# package "bin/fletd" only
7-
if os.getenv("PACKAGE_FLETD_ONLY"):
8-
bin_path = os.path.join(bin_path, "fletd*")
6+
bin_path = hook_config.temp_bin_dir
7+
if not bin_path:
8+
bin_path = get_flet_bin_path()
99

10-
datas = [(bin_path, "flet/bin")]
10+
if bin_path:
11+
# package "bin/fletd" only
12+
if os.getenv("PACKAGE_FLETD_ONLY"):
13+
bin_path = os.path.join(bin_path, "fletd*")
14+
15+
datas = [(bin_path, "flet/bin")]
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import os
2+
import plistlib
3+
import shutil
4+
import subprocess
5+
import tarfile
6+
from pathlib import Path
7+
8+
from PyInstaller.building.icon import normalize_icon_type
9+
10+
from flet.utils import safe_tar_extractall
11+
12+
13+
def unpack_app_bundle(tar_path):
14+
bin_dir = str(Path(tar_path).parent)
15+
16+
with tarfile.open(tar_path, "r:gz") as tar_arch:
17+
safe_tar_extractall(tar_arch, bin_dir)
18+
os.remove(tar_path)
19+
20+
return os.path.join(bin_dir, "Flet.app")
21+
22+
23+
def update_flet_view_icon(app_path, icon_path):
24+
print("Updating Flet View icon", app_path, icon_path)
25+
26+
icon_file = "AppIcon.icns"
27+
28+
# normalize icon
29+
normalized_icon_path = normalize_icon_type(
30+
icon_path, ("icns",), "icns", os.getcwd()
31+
)
32+
33+
# patch icon
34+
print("Copying icons from", normalized_icon_path)
35+
shutil.copy(
36+
normalized_icon_path,
37+
os.path.join(app_path, "Contents", "Resources", icon_file),
38+
)
39+
40+
# update icon file name
41+
pl = __load_info_plist(app_path)
42+
pl["CFBundleIconFile"] = icon_file
43+
del pl["CFBundleIconName"]
44+
__save_info_plist(app_path, pl)
45+
46+
47+
def update_flet_view_version_info(
48+
app_path,
49+
bundle_id,
50+
product_name,
51+
product_version,
52+
copyright,
53+
):
54+
print("Updating Flet View plist", app_path)
55+
56+
pl = __load_info_plist(app_path)
57+
58+
if bundle_id:
59+
pl["CFBundleIdentifier"] = bundle_id
60+
if product_name:
61+
pl["CFBundleName"] = product_name
62+
pl["CFBundleDisplayName"] = product_name
63+
64+
# rename app bundle
65+
new_app_path = os.path.join(Path(app_path).parent, f"{product_name}.app")
66+
os.rename(app_path, new_app_path)
67+
app_path = new_app_path
68+
if product_version:
69+
pl["CFBundleShortVersionString"] = product_version
70+
if copyright:
71+
pl["NSHumanReadableCopyright"] = copyright
72+
73+
__save_info_plist(app_path, pl)
74+
75+
return app_path
76+
77+
78+
def assemble_app_bundle(app_path, tar_path):
79+
80+
# sign app bundle
81+
print(f"Signing file {app_path}")
82+
cmd_args = [
83+
"codesign",
84+
"-s",
85+
"-",
86+
"--force",
87+
"--all-architectures",
88+
"--timestamp",
89+
"--deep",
90+
app_path,
91+
]
92+
p = subprocess.run(
93+
cmd_args,
94+
stdout=subprocess.PIPE,
95+
stderr=subprocess.STDOUT,
96+
universal_newlines=True,
97+
)
98+
if p.returncode:
99+
raise SystemError(
100+
f"codesign command ({cmd_args}) failed with error code {p.returncode}!\noutput: {p.stdout}"
101+
)
102+
103+
# pack tar
104+
with tarfile.open(tar_path, "w:gz") as tar:
105+
tar.add(app_path, arcname=os.path.basename(app_path))
106+
107+
# cleanup
108+
shutil.rmtree(app_path, ignore_errors=True)
109+
110+
111+
def __load_info_plist(app_path):
112+
with open(__get_plist_path(app_path), "rb") as fp:
113+
return plistlib.load(fp)
114+
115+
116+
def __save_info_plist(app_path, pl):
117+
with open(__get_plist_path(app_path), "wb") as fp:
118+
plistlib.dump(pl, fp)
119+
120+
121+
def __get_plist_path(app_path):
122+
return os.path.join(app_path, "Contents", "Info.plist")
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import os
2+
import shutil
3+
import tempfile
4+
import uuid
5+
from pathlib import Path
6+
7+
8+
def get_flet_bin_path():
9+
bin_path = os.path.abspath(
10+
os.path.join(os.path.dirname(__file__), os.pardir, "bin")
11+
)
12+
if not os.path.exists(bin_path):
13+
return None
14+
return bin_path
15+
16+
17+
def copy_flet_bin():
18+
bin_path = get_flet_bin_path()
19+
if not bin_path:
20+
return None
21+
22+
# create temp bin dir
23+
temp_bin_dir = Path(tempfile.gettempdir()).joinpath(str(uuid.uuid4()))
24+
shutil.copytree(bin_path, str(temp_bin_dir))
25+
return str(temp_bin_dir)
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import os
2+
import tempfile
3+
import uuid
4+
from pathlib import Path
5+
6+
import packaging.version as version
7+
import pefile
8+
from PyInstaller.building.icon import normalize_icon_type
9+
from PyInstaller.compat import win32api
10+
from PyInstaller.utils.win32.icon import IconFile, normalize_icon_type
11+
from PyInstaller.utils.win32.versioninfo import decode
12+
13+
14+
def update_flet_view_icon(exe_path, icon_path):
15+
print("Updating Flet View icon", exe_path, icon_path)
16+
17+
RT_ICON = 3
18+
RT_GROUP_ICON = 14
19+
20+
normalized_icon_path = normalize_icon_type(
21+
icon_path, ("exe", "ico"), "ico", os.getcwd()
22+
)
23+
icon = IconFile(normalized_icon_path)
24+
print("Copying icons from", normalized_icon_path)
25+
26+
hdst = win32api.BeginUpdateResource(exe_path, 0)
27+
28+
iconid = 1
29+
# Each step in the following enumerate() will instantiate an IconFile object, as a result of deferred execution
30+
# of the map() above.
31+
i = 101
32+
data = icon.grp_icon_dir()
33+
data = data + icon.grp_icondir_entries(iconid)
34+
win32api.UpdateResource(hdst, RT_GROUP_ICON, i, data, 1033)
35+
print("Writing RT_GROUP_ICON %d resource with %d bytes", i, len(data))
36+
for data in icon.images:
37+
win32api.UpdateResource(hdst, RT_ICON, iconid, data, 1033)
38+
print("Writing RT_ICON %d resource with %d bytes", iconid, len(data))
39+
iconid = iconid + 1
40+
41+
win32api.EndUpdateResource(hdst, 0)
42+
43+
44+
def update_flet_view_version_info(
45+
exe_path,
46+
product_name,
47+
file_description,
48+
product_version,
49+
file_version,
50+
company_name,
51+
copyright,
52+
):
53+
print("Updating Flet View version info", exe_path)
54+
55+
# load versioninfo from exe
56+
vs = decode(exe_path)
57+
58+
# update file version
59+
if file_version:
60+
pv = version.parse(file_version)
61+
filevers = (pv.major, pv.minor, pv.micro, 0)
62+
vs.ffi.fileVersionMS = (filevers[0] << 16) | (filevers[1] & 0xFFFF)
63+
vs.ffi.fileVersionLS = (filevers[2] << 16) | (filevers[3] & 0xFFFF)
64+
65+
# update string props
66+
for k in vs.kids[0].kids[0].kids:
67+
if k.name == "ProductName":
68+
k.val = product_name if product_name else ""
69+
elif k.name == "FileDescription":
70+
k.val = file_description if file_description else ""
71+
if k.name == "ProductVersion":
72+
k.val = product_version if product_version else ""
73+
if k.name == "FileVersion" and file_version:
74+
k.val = file_version if file_version else ""
75+
if k.name == "CompanyName":
76+
k.val = company_name if company_name else ""
77+
if k.name == "LegalCopyright":
78+
k.val = copyright if copyright else ""
79+
80+
version_info_path = str(Path(tempfile.gettempdir()).joinpath(str(uuid.uuid4())))
81+
with open(version_info_path, "w") as f:
82+
f.write(str(vs))
83+
84+
# Remember overlay
85+
pe = pefile.PE(exe_path, fast_load=True)
86+
overlay_before = pe.get_overlay()
87+
pe.close()
88+
89+
hdst = win32api.BeginUpdateResource(exe_path, 0)
90+
win32api.UpdateResource(
91+
hdst, pefile.RESOURCE_TYPE["RT_VERSION"], 1, vs.toRaw(), 1033
92+
)
93+
win32api.EndUpdateResource(hdst, 0)
94+
95+
if overlay_before:
96+
# Check if the overlay is still present
97+
pe = pefile.PE(exe_path, fast_load=True)
98+
overlay_after = pe.get_overlay()
99+
pe.close()
100+
101+
# If the update removed the overlay data, re-append it
102+
if not overlay_after:
103+
with open(exe_path, "ab") as exef:
104+
exef.write(overlay_before)
105+
106+
return version_info_path

sdk/python/flet/cli/cli.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import argparse
22
import sys
3-
import flet.version
3+
4+
import flet.cli.commands.pack
45
import flet.cli.commands.run
5-
import flet.cli.commands.build
6+
import flet.version
7+
68

79
# Source https://stackoverflow.com/a/26379693
810
def set_default_subparser(self, name, args=None, positional_args=0):
@@ -60,7 +62,7 @@ def main():
6062
# sp.default = "run"
6163

6264
flet.cli.commands.run.Command.register_to(sp, "run")
63-
flet.cli.commands.build.Command.register_to(sp, "build")
65+
flet.cli.commands.pack.Command.register_to(sp, "pack")
6466
parser.set_default_subparser("run", positional_args=1)
6567

6668
# print usage if called without args

sdk/python/flet/cli/commands/build.py

Lines changed: 0 additions & 23 deletions
This file was deleted.

0 commit comments

Comments
 (0)