pyship 0.1.6__py3-none-any.whl → 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyship/__init__.py +2 -1
- pyship/__main__.py +0 -1
- pyship/_version_.py +1 -1
- pyship/app_info.py +12 -27
- pyship/clip.py +18 -104
- pyship/cloud.py +2 -1
- pyship/constants.py +7 -0
- pyship/create_launcher.py +48 -81
- pyship/download.py +18 -2
- pyship/launcher/__init__.py +1 -1
- pyship/launcher/launcher_standalone.py +309 -0
- pyship/launcher/metadata.py +5 -0
- pyship/launcher_stub.py +198 -0
- pyship/nsis.py +12 -5
- pyship/pyship.py +11 -9
- pyship/pyship_custom_print.py +0 -1
- pyship/pyship_get_icon.py +3 -1
- pyship/uv_util.py +157 -0
- pyship-0.3.1.dist-info/METADATA +81 -0
- pyship-0.3.1.dist-info/RECORD +34 -0
- {pyship-0.1.6.dist-info → pyship-0.3.1.dist-info}/WHEEL +1 -1
- pyship/atomic_zip.py +0 -41
- pyship/get-pip.py +0 -22713
- pyship/launcher/launcher.py +0 -224
- pyship/patch/__init__.py +0 -0
- pyship/patch/pyship_patch.py +0 -33
- pyship-0.1.6.dist-info/METADATA +0 -47
- pyship-0.1.6.dist-info/RECORD +0 -36
- {pyship-0.1.6.dist-info → pyship-0.3.1.dist-info/licenses}/LICENSE +0 -0
- {pyship-0.1.6.dist-info → pyship-0.3.1.dist-info}/top_level.txt +0 -0
pyship/__init__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""pyship - ship python apps"""
|
|
2
2
|
|
|
3
3
|
python_interpreter_exes = {True: "pythonw.exe", False: "python.exe"} # True is GUI, False is CLI
|
|
4
4
|
|
|
@@ -21,6 +21,7 @@ from .nsis import run_nsis
|
|
|
21
21
|
from .download import file_download, extract
|
|
22
22
|
from .create_launcher import create_pyship_launcher
|
|
23
23
|
from .clip import create_base_clip, install_target_app, create_clip, create_clip_file
|
|
24
|
+
from .uv_util import find_or_bootstrap_uv, uv_python_install, uv_venv_create, uv_pip_install, uv_build
|
|
24
25
|
from .cloud import PyShipCloud
|
|
25
26
|
from .pyship import PyShip
|
|
26
27
|
from .main import main
|
pyship/__main__.py
CHANGED
pyship/_version_.py
CHANGED
|
@@ -2,7 +2,7 @@ from pyshipupdate import __author__ as pyship_author
|
|
|
2
2
|
|
|
3
3
|
__application_name__ = "pyship"
|
|
4
4
|
__author__ = pyship_author
|
|
5
|
-
__version__ = "0.1
|
|
5
|
+
__version__ = "0.3.1"
|
|
6
6
|
__title__ = __application_name__
|
|
7
7
|
__author_email__ = "j@abel.co"
|
|
8
8
|
__url__ = "https://github.com/jamesabel/pyship"
|
pyship/app_info.py
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import os
|
|
2
1
|
from pathlib import Path
|
|
3
2
|
from dataclasses import dataclass
|
|
4
|
-
from typing import Union
|
|
5
|
-
import subprocess
|
|
6
|
-
import sys
|
|
3
|
+
from typing import Optional, Union
|
|
7
4
|
|
|
8
5
|
import toml
|
|
9
6
|
from semver import VersionInfo
|
|
@@ -88,21 +85,12 @@ def get_app_info_wheel(app_info: AppInfo, dist_path: Path) -> AppInfo:
|
|
|
88
85
|
|
|
89
86
|
|
|
90
87
|
@typechecked
|
|
91
|
-
def
|
|
92
|
-
script_path = Path(target_app_project_dir, script_file_name)
|
|
93
|
-
if script_path.exists():
|
|
94
|
-
pyship_print(f'running "{script_file_name}" (cwd="{target_app_project_dir}")')
|
|
95
|
-
make_venv_process = subprocess.run(script_file_name, cwd=str(target_app_project_dir), capture_output=True)
|
|
96
|
-
log.info(make_venv_process.stdout)
|
|
97
|
-
log.info(make_venv_process.stderr)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
@typechecked
|
|
101
|
-
def get_app_info(target_app_project_dir: Path, target_app_dist_dir: Path) -> AppInfo:
|
|
88
|
+
def get_app_info(target_app_project_dir: Path, target_app_dist_dir: Path, cache_dir: Optional[Path] = None) -> AppInfo:
|
|
102
89
|
"""
|
|
103
90
|
Get combined app info from all potential sources.
|
|
104
91
|
:param target_app_project_dir: app project dir, e.g. where a pyproject.toml may reside. (optional)
|
|
105
92
|
:param target_app_dist_dir: the "distribution" dir, e.g. where a wheel may reside (optional)
|
|
93
|
+
:param cache_dir: cache dir for uv bootstrap (optional)
|
|
106
94
|
:return: an AppInfo instance
|
|
107
95
|
"""
|
|
108
96
|
|
|
@@ -117,18 +105,15 @@ def get_app_info(target_app_project_dir: Path, target_app_dist_dir: Path) -> App
|
|
|
117
105
|
wheel_glob = f"{app_info.name}*.whl"
|
|
118
106
|
wheel_list = list(target_app_dist_dir.glob(wheel_glob))
|
|
119
107
|
if len(wheel_list) < 1:
|
|
120
|
-
# No wheel file exists
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
run_script(target_app_project_dir, os.environ.get("PYSHIP_BUILD_SCRIPT", "build.bat")) # will be .sh for Linux/MacOS whenever they're supported ...
|
|
130
|
-
|
|
131
|
-
wheel_list = list(target_app_dist_dir.glob(wheel_glob)) # try again to find the wheel
|
|
108
|
+
# No wheel file exists - build it using uv
|
|
109
|
+
from pyship.uv_util import find_or_bootstrap_uv, uv_build
|
|
110
|
+
|
|
111
|
+
if cache_dir is not None:
|
|
112
|
+
uv_path = find_or_bootstrap_uv(cache_dir)
|
|
113
|
+
uv_build(uv_path, target_app_project_dir, target_app_dist_dir)
|
|
114
|
+
wheel_list = list(target_app_dist_dir.glob(wheel_glob)) # try again to find the wheel
|
|
115
|
+
else:
|
|
116
|
+
log.warning("no cache_dir provided, cannot bootstrap uv to build wheel")
|
|
132
117
|
|
|
133
118
|
if len(wheel_list) == 0:
|
|
134
119
|
log.error(f"{app_info.name} : no wheel at {target_app_dist_dir} ({target_app_dist_dir.absolute()})")
|
pyship/clip.py
CHANGED
|
@@ -1,44 +1,32 @@
|
|
|
1
|
-
import glob
|
|
2
|
-
import os
|
|
3
1
|
import platform
|
|
4
|
-
import re
|
|
5
2
|
import shutil
|
|
6
|
-
import subprocess
|
|
7
|
-
import tkinter
|
|
8
3
|
from pathlib import Path
|
|
9
|
-
from platform import system
|
|
10
|
-
import inspect
|
|
11
4
|
|
|
12
5
|
from typeguard import typechecked
|
|
13
6
|
from balsa import get_logger
|
|
14
7
|
|
|
15
|
-
from
|
|
16
|
-
import
|
|
17
|
-
import pyship.patch.pyship_patch
|
|
18
|
-
from pyship import AppInfo, file_download, pyship_print, extract, __application_name__, subprocess_run, CLIP_EXT, PyshipCouldNotGetVersion
|
|
19
|
-
|
|
8
|
+
from pyship import AppInfo, pyship_print, __application_name__, CLIP_EXT
|
|
9
|
+
from pyship.uv_util import find_or_bootstrap_uv, uv_python_install, uv_venv_create, uv_pip_install
|
|
20
10
|
|
|
21
11
|
log = get_logger(__application_name__)
|
|
22
12
|
|
|
23
13
|
|
|
24
14
|
@typechecked
|
|
25
|
-
def create_clip(target_app_info: AppInfo, app_dir: Path,
|
|
15
|
+
def create_clip(target_app_info: AppInfo, app_dir: Path, target_app_package_dist_dir: Path, cache_dir: Path, find_links: list) -> Path:
|
|
26
16
|
"""
|
|
27
17
|
create clip (Complete Location Independent Python) environment
|
|
28
18
|
clip is a stand-alone, relocatable directory that contains the entire python environment (including all libraries and the target app) needed to execute the target python application
|
|
29
19
|
:param target_app_info: target app info
|
|
30
20
|
:param app_dir: app gets built here (i.e. the output of this function)
|
|
31
|
-
:param remove_pth: remove remove python*._pth files as a workaround (see bug URL below)
|
|
32
21
|
:param target_app_package_dist_dir: target app module dist dir (as a package)
|
|
33
22
|
:param cache_dir: cache dir
|
|
34
23
|
:param find_links: a (potentially empty) list of "find links" to add to pip invocation
|
|
35
24
|
:return: path to the clip dir
|
|
36
25
|
"""
|
|
37
26
|
|
|
38
|
-
# create the clip dir
|
|
39
27
|
clip_dir = create_base_clip(target_app_info, app_dir, cache_dir)
|
|
40
28
|
assert isinstance(target_app_info.name, str)
|
|
41
|
-
install_target_app(target_app_info.name, clip_dir, target_app_package_dist_dir,
|
|
29
|
+
install_target_app(target_app_info.name, clip_dir, target_app_package_dist_dir, cache_dir, find_links)
|
|
42
30
|
return clip_dir
|
|
43
31
|
|
|
44
32
|
|
|
@@ -56,7 +44,7 @@ def create_clip_file(clip_dir: Path) -> Path:
|
|
|
56
44
|
@typechecked
|
|
57
45
|
def create_base_clip(target_app_info: AppInfo, app_dir: Path, cache_dir: Path) -> Path:
|
|
58
46
|
"""
|
|
59
|
-
create pyship python environment called clip
|
|
47
|
+
create pyship python environment called clip using uv
|
|
60
48
|
|
|
61
49
|
:param target_app_info: target app info
|
|
62
50
|
:param app_dir: app gets built here (i.e. the output of this function)
|
|
@@ -64,111 +52,37 @@ def create_base_clip(target_app_info: AppInfo, app_dir: Path, cache_dir: Path) -
|
|
|
64
52
|
:return absolute path to created clip
|
|
65
53
|
"""
|
|
66
54
|
|
|
67
|
-
# use project's Python (in this venv) to determine target Python version
|
|
68
|
-
python_ver_str = platform.python_version()
|
|
69
55
|
python_ver_tuple = platform.python_version_tuple()
|
|
56
|
+
python_version = f"{python_ver_tuple[0]}.{python_ver_tuple[1]}"
|
|
70
57
|
|
|
71
|
-
# get the embedded python interpreter
|
|
72
|
-
search = re.search(r"([0-9]+)", python_ver_tuple[2])
|
|
73
|
-
if search is not None:
|
|
74
|
-
base_patch_str = search.group(1)
|
|
75
|
-
else:
|
|
76
|
-
raise PyshipCouldNotGetVersion(python_ver_tuple)
|
|
77
|
-
|
|
78
|
-
# version but with numbers only and not the extra release info (e.g. b, rc, etc.)
|
|
79
|
-
ver_base_str = f"{python_ver_tuple[0]}.{python_ver_tuple[1]}.{base_patch_str}"
|
|
80
|
-
zip_file = Path(f"python-{python_ver_str}-embed-amd64.zip")
|
|
81
|
-
zip_url = f"https://www.python.org/ftp/python/{ver_base_str}/{zip_file}"
|
|
82
|
-
file_download(zip_url, cache_dir, zip_file)
|
|
83
58
|
clip_dir_name = f"{target_app_info.name}_{str(target_app_info.version)}"
|
|
84
59
|
clip_dir = Path(app_dir, clip_dir_name).absolute()
|
|
85
60
|
pyship_print(f'building clip {clip_dir_name} ("{clip_dir}")')
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
# todo: refactor to use Path
|
|
91
|
-
glob_path = os.path.abspath(os.path.join(clip_dir, "python*._pth"))
|
|
92
|
-
pth_glob = glob.glob(glob_path)
|
|
93
|
-
if pth_glob is None or len(pth_glob) != 1:
|
|
94
|
-
log.critical("could not find '._pth' file at %s" % glob_path)
|
|
95
|
-
else:
|
|
96
|
-
pth_path = pth_glob[0]
|
|
97
|
-
log.info("uncommenting import site in %s" % pth_path)
|
|
98
|
-
pth_contents = open(pth_path).read()
|
|
99
|
-
pth_save_path = pth_path.replace("._pth", "._pip_bug_pth")
|
|
100
|
-
shutil.move(pth_path, pth_save_path)
|
|
101
|
-
pth_contents = pth_contents.replace("#import site", "import site") # uncomment import site
|
|
102
|
-
pth_contents = "..\n" + pth_contents # add where pyship_main.py will be (one dir 'up' from python.exe)
|
|
103
|
-
open(pth_path, "w").write(pth_contents)
|
|
104
|
-
|
|
105
|
-
# install pip
|
|
106
|
-
# this is how get-pip was originally obtained
|
|
107
|
-
# get_pip_file = "get-pip.py"
|
|
108
|
-
# get_file("https://bootstrap.pypa.io/get-pip.py", cache_folder, get_pip_file)
|
|
109
|
-
get_pip_file = "get-pip.py"
|
|
110
|
-
get_pip_path = os.path.join(os.path.dirname(pyship.__file__), get_pip_file)
|
|
111
|
-
log.info(f"{get_pip_path=}")
|
|
112
|
-
get_pip_cmd = ["python.exe", os.path.abspath(get_pip_path), "--no-warn-script-location"]
|
|
113
|
-
log.info(f"{get_pip_cmd=}")
|
|
114
|
-
subprocess.run(get_pip_cmd, cwd=clip_dir, capture_output=True, shell=True, check=True) # subprocess_run uses typeguard and we don't have that yet so just use subprocess.run
|
|
115
|
-
|
|
116
|
-
# upgrade pip
|
|
117
|
-
pip_upgrade_cmd = ["python.exe", "-m", "pip", "install", "--no-deps", "--upgrade", "pip"]
|
|
118
|
-
log.info(f"{pip_upgrade_cmd=}")
|
|
119
|
-
subprocess.run(pip_upgrade_cmd, cwd=clip_dir, capture_output=True, shell=True, check=True) # subprocess_run uses typeguard and we don't have that yet so just use subprocess.run
|
|
120
|
-
|
|
121
|
-
# the embedded Python doesn't ship with tkinter, so add it to clip
|
|
122
|
-
# https://stackoverflow.com/questions/37710205/python-embeddable-zip-install-tkinter
|
|
123
|
-
if is_windows():
|
|
124
|
-
python_base_install_dir = Path(tkinter.__file__).parent.parent.parent
|
|
125
|
-
copy_tree(Path(python_base_install_dir), clip_dir, "tcl") # tcl dir
|
|
126
|
-
copy_tree(Path(python_base_install_dir, "Lib"), Path(clip_dir, "Lib", "site-packages"), "tkinter") # tkinter dir
|
|
127
|
-
# dlls
|
|
128
|
-
for file_name in ["_tkinter.pyd", "tcl86t.dll", "tk86t.dll"]:
|
|
129
|
-
shutil.copy2(str(Path(python_base_install_dir, "DLLs", file_name)), str(clip_dir))
|
|
130
|
-
else:
|
|
131
|
-
log.fatal(f"Unsupported OS: {system()}")
|
|
132
|
-
|
|
133
|
-
# write out patch files
|
|
134
|
-
Path(clip_dir, "pyship_patch.pth").write_text("import pyship_patch") # this causes the pyship_patch.py to be loaded and therefore executed
|
|
135
|
-
patch_string = f"{inspect.getsource(pyship.patch.pyship_patch.pyship_patch)}\n\npyship_patch()\n"
|
|
136
|
-
Path(clip_dir, "pyship_patch.py").write_text(patch_string) # due to the above, this file gets executed at Python interpreter startup
|
|
61
|
+
|
|
62
|
+
uv_path = find_or_bootstrap_uv(cache_dir)
|
|
63
|
+
uv_python_install(uv_path, python_version)
|
|
64
|
+
uv_venv_create(uv_path, clip_dir, python_version)
|
|
137
65
|
|
|
138
66
|
return clip_dir
|
|
139
67
|
|
|
140
68
|
|
|
141
69
|
@typechecked
|
|
142
|
-
def install_target_app(module_name: str,
|
|
70
|
+
def install_target_app(module_name: str, clip_dir: Path, target_app_package_dist_dir: Path, cache_dir: Path, find_links: list):
|
|
143
71
|
"""
|
|
144
72
|
install target app as a module (and its dependencies) into clip
|
|
145
73
|
:param module_name: module name
|
|
146
|
-
:param
|
|
74
|
+
:param clip_dir: clip dir (a relocatable venv)
|
|
147
75
|
:param target_app_package_dist_dir: target app module dist dir (as a package)
|
|
148
|
-
:param
|
|
76
|
+
:param cache_dir: cache dir
|
|
149
77
|
:param find_links: a list of "find links" to add to pip invocation
|
|
150
78
|
"""
|
|
151
79
|
|
|
152
|
-
# install this local app in the embedded python dir
|
|
153
80
|
log.info(f"installing {module_name}")
|
|
154
81
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
# https://github.com/PythonCharmers/python-future/issues/411
|
|
158
|
-
pth_glob_list = [p for p in Path(python_env_dir).glob("python*._pth")]
|
|
159
|
-
if len(pth_glob_list) == 1:
|
|
160
|
-
pth_path = str(pth_glob_list[0])
|
|
161
|
-
pth_save_path = pth_path.replace("._pth", "._future_bug_pth")
|
|
162
|
-
shutil.move(pth_path, pth_save_path)
|
|
163
|
-
else:
|
|
164
|
-
log.error(f"unexpected {pth_glob_list=} found at {python_env_dir=}")
|
|
165
|
-
|
|
166
|
-
# install the target module (and its dependencies)
|
|
167
|
-
cmd = [str(Path(python_env_dir, "python.exe")), "-m", "pip", "install", "-U", module_name, "--no-warn-script-location"]
|
|
168
|
-
|
|
169
|
-
find_links.append(str(target_app_package_dist_dir.absolute()))
|
|
82
|
+
uv_path = find_or_bootstrap_uv(cache_dir)
|
|
83
|
+
target_python = Path(clip_dir, "Scripts", "python.exe")
|
|
170
84
|
|
|
171
|
-
|
|
172
|
-
|
|
85
|
+
all_find_links = list(find_links) # copy to avoid mutating caller's list
|
|
86
|
+
all_find_links.append(str(target_app_package_dist_dir.absolute()))
|
|
173
87
|
|
|
174
|
-
|
|
88
|
+
uv_pip_install(uv_path, target_python, [module_name], all_find_links, upgrade=True)
|
pyship/cloud.py
CHANGED
|
@@ -30,7 +30,8 @@ class PyShipCloud:
|
|
|
30
30
|
:return: URL of uploaded file
|
|
31
31
|
"""
|
|
32
32
|
s3_key = file_path.name
|
|
33
|
-
|
|
33
|
+
# Note: AWS S3 now defaults to BucketOwnerEnforced which disables ACLs
|
|
34
|
+
# Use bucket policies or pre-signed URLs for access control instead
|
|
34
35
|
self.s3_access.create_bucket()
|
|
35
36
|
self.s3_access.upload(file_path, s3_key)
|
|
36
37
|
return self.s3_access.get_s3_object_url(s3_key)
|
pyship/constants.py
CHANGED
pyship/create_launcher.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import shutil
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
import subprocess
|
|
4
3
|
|
|
5
4
|
from typeguard import typechecked
|
|
6
5
|
from balsa import get_logger
|
|
6
|
+
from semver import VersionInfo
|
|
7
7
|
|
|
8
8
|
from pyshipupdate import mkdirs
|
|
9
9
|
import pyship
|
|
@@ -11,7 +11,7 @@ from pyship import AppInfo, pyship_print, get_icon
|
|
|
11
11
|
from pyship import __application_name__ as pyship_application_name
|
|
12
12
|
from pyship.launcher import application_name as launcher_application_name
|
|
13
13
|
from pyship.launcher import calculate_metadata, load_metadata, store_metadata
|
|
14
|
-
|
|
14
|
+
from pyship.launcher_stub import compile_launcher_stub
|
|
15
15
|
|
|
16
16
|
log = get_logger(launcher_application_name)
|
|
17
17
|
|
|
@@ -19,7 +19,7 @@ log = get_logger(launcher_application_name)
|
|
|
19
19
|
@typechecked
|
|
20
20
|
def create_pyship_launcher(target_app_info: AppInfo, app_path_output: Path):
|
|
21
21
|
"""
|
|
22
|
-
|
|
22
|
+
Create the launcher executable using a compiled C# stub and standalone Python launcher script.
|
|
23
23
|
:param target_app_info: target app info
|
|
24
24
|
:param app_path_output: app gets built here
|
|
25
25
|
:return: True if launcher was built
|
|
@@ -32,91 +32,58 @@ def create_pyship_launcher(target_app_info: AppInfo, app_path_output: Path):
|
|
|
32
32
|
else:
|
|
33
33
|
metadata_filename = f"{target_app_info.name}_metadata.json"
|
|
34
34
|
|
|
35
|
-
#
|
|
36
|
-
|
|
37
|
-
# find the launcher source file path
|
|
35
|
+
# find the launcher source directory (for metadata hashing)
|
|
38
36
|
assert hasattr(pyship, "__path__")
|
|
39
|
-
pyship_path_list = pyship.__path__
|
|
37
|
+
pyship_path_list = pyship.__path__
|
|
40
38
|
if len(pyship_path_list) != 1:
|
|
41
39
|
log.warning(f"not length of 1: {pyship_path_list}")
|
|
42
|
-
pyship_path = pyship_path_list[0]
|
|
40
|
+
pyship_path = Path(pyship_path_list[0])
|
|
43
41
|
launcher_module_dir = Path(pyship_path, launcher_application_name)
|
|
44
42
|
|
|
45
|
-
|
|
46
|
-
launcher_exe_path = Path(
|
|
43
|
+
launcher_dir = Path(app_path_output, target_app_info.name)
|
|
44
|
+
launcher_exe_path = Path(launcher_dir, f"{target_app_info.name}.exe")
|
|
47
45
|
|
|
48
46
|
icon_path = get_icon(target_app_info, pyship_print)
|
|
49
47
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
#
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
launcher_candidate_parents = (site_packages_dir, Path())
|
|
87
|
-
for parent in launcher_candidate_parents:
|
|
88
|
-
source_candidate = Path(parent, pyship_application_name, launcher_application_name, f"{launcher_application_name}.py").absolute()
|
|
89
|
-
if source_candidate.exists():
|
|
90
|
-
launcher_source_path = source_candidate
|
|
91
|
-
break
|
|
92
|
-
if launcher_source_path is None:
|
|
93
|
-
log.fatal(f"could not find launcher path in {launcher_candidate_parents=}")
|
|
94
|
-
return False
|
|
48
|
+
mkdirs(app_path_output)
|
|
49
|
+
|
|
50
|
+
# Compute metadata for cache invalidation
|
|
51
|
+
assert isinstance(target_app_info.author, str)
|
|
52
|
+
assert isinstance(target_app_info.is_gui, bool)
|
|
53
|
+
assert isinstance(target_app_info.version, VersionInfo)
|
|
54
|
+
metadata = calculate_metadata(target_app_info.name, target_app_info.author, target_app_info.version, launcher_module_dir, icon_path, target_app_info.is_gui)
|
|
55
|
+
if not launcher_exe_path.exists() or metadata != load_metadata(app_path_output, metadata_filename):
|
|
56
|
+
pyship_print(f'building launcher ("{launcher_exe_path}")')
|
|
57
|
+
|
|
58
|
+
# 1. Compile the C# stub
|
|
59
|
+
compile_launcher_stub(
|
|
60
|
+
app_name=target_app_info.name,
|
|
61
|
+
icon_path=icon_path,
|
|
62
|
+
is_gui=target_app_info.is_gui,
|
|
63
|
+
output_path=launcher_dir,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# 2. Copy the standalone launcher script alongside the stub
|
|
67
|
+
standalone_source = Path(launcher_module_dir, "launcher_standalone.py")
|
|
68
|
+
standalone_dest = Path(launcher_dir, f"{target_app_info.name}_launcher.py")
|
|
69
|
+
shutil.copy2(str(standalone_source), str(standalone_dest))
|
|
70
|
+
log.info(f"copied launcher script to {standalone_dest}")
|
|
71
|
+
|
|
72
|
+
# 3. Copy the icon alongside
|
|
73
|
+
if icon_path.exists():
|
|
74
|
+
icon_dest = Path(launcher_dir, f"{target_app_info.name}.ico")
|
|
75
|
+
shutil.copy2(str(icon_path), str(icon_dest))
|
|
76
|
+
log.info(f"copied icon to {icon_dest}")
|
|
77
|
+
|
|
78
|
+
# 4. Store metadata for cache invalidation
|
|
79
|
+
store_metadata(app_path_output, metadata_filename, metadata)
|
|
80
|
+
|
|
81
|
+
if launcher_exe_path.exists():
|
|
82
|
+
built_it = True
|
|
83
|
+
log.info(f"launcher built ({launcher_exe_path})")
|
|
95
84
|
else:
|
|
96
|
-
log.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
# avoid re-building launcher if its functionality wouldn't change
|
|
100
|
-
assert isinstance(target_app_info.author, str)
|
|
101
|
-
assert isinstance(target_app_info.is_gui, bool)
|
|
102
|
-
metadata = calculate_metadata(target_app_info.name, target_app_info.author, target_app_info.version, Path(launcher_module_dir), icon_path, target_app_info.is_gui)
|
|
103
|
-
if not launcher_exe_path.exists() or metadata != load_metadata(app_path_output, metadata_filename):
|
|
104
|
-
pyship_print(f'building launcher ("{launcher_exe_path}")')
|
|
105
|
-
log.info(f"project_dir={str(target_app_info.project_dir)}")
|
|
106
|
-
log.info(f"{command_line=}")
|
|
107
|
-
# pyinstaller outputs regular status messages to stderr for some reason so just capture all output but also check for error return code
|
|
108
|
-
launcher_run = subprocess.run(command_line, cwd=target_app_info.project_dir, capture_output=True, text=True, check=True)
|
|
109
|
-
# metadata is in the app parent dir
|
|
110
|
-
store_metadata(app_path_output, metadata_filename, metadata)
|
|
111
|
-
|
|
112
|
-
if launcher_exe_path.exists():
|
|
113
|
-
built_it = True
|
|
114
|
-
log.info(f"launcher built ({launcher_exe_path})")
|
|
115
|
-
else:
|
|
116
|
-
# launcher wasn't built - there was an error - so display the pyinstaller output to the user
|
|
117
|
-
pyship_print(launcher_run.stdout)
|
|
118
|
-
pyship_print(launcher_run.stderr)
|
|
119
|
-
else:
|
|
120
|
-
log.info(f"{launcher_exe_path} already built - no need to rebuild")
|
|
85
|
+
log.error(f"launcher exe not found after build: {launcher_exe_path}")
|
|
86
|
+
else:
|
|
87
|
+
log.info(f"{launcher_exe_path} already built - no need to rebuild")
|
|
121
88
|
|
|
122
89
|
return built_it
|
pyship/download.py
CHANGED
|
@@ -32,6 +32,22 @@ def file_download(url: str, destination_folder: Path, file_name: Path):
|
|
|
32
32
|
return destination_path
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
def is_within_directory(directory: Path, target: Path):
|
|
36
|
+
abs_directory = directory.absolute()
|
|
37
|
+
abs_target = target.absolute()
|
|
38
|
+
prefix = os.path.commonprefix([abs_directory, abs_target])
|
|
39
|
+
return prefix == str(abs_directory)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def safe_extract(tar, path: Path = Path(".")):
|
|
43
|
+
# for CVE-2007-4559
|
|
44
|
+
for member in tar.getmembers():
|
|
45
|
+
member_path = Path(path, member.name)
|
|
46
|
+
if not is_within_directory(path, member_path):
|
|
47
|
+
raise Exception("Attempted Path Traversal in Tar File")
|
|
48
|
+
tar.extractall(path, None, numeric_owner=False)
|
|
49
|
+
|
|
50
|
+
|
|
35
51
|
@typechecked
|
|
36
52
|
def extract(source_folder: Path, source_file: Path, destination_folder: Path):
|
|
37
53
|
mkdirs(destination_folder, remove_first=True)
|
|
@@ -43,9 +59,9 @@ def extract(source_folder: Path, source_file: Path, destination_folder: Path):
|
|
|
43
59
|
zf.extractall(destination_folder)
|
|
44
60
|
elif source_file.suffix == ".tgz":
|
|
45
61
|
with tarfile.open(source) as tf:
|
|
46
|
-
tf
|
|
62
|
+
safe_extract(tf, destination_folder)
|
|
47
63
|
elif source_file.suffix == ".gz":
|
|
48
64
|
with tarfile.open(source) as tf:
|
|
49
|
-
tf
|
|
65
|
+
safe_extract(tf, destination_folder)
|
|
50
66
|
else:
|
|
51
67
|
raise Exception(f"Unsupported file type {source_file.suffix} ({source_file})")
|
pyship/launcher/__init__.py
CHANGED