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 CHANGED
@@ -1,4 +1,4 @@
1
- """ pyship - ship python apps """
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
@@ -2,6 +2,5 @@ from ismain import is_main
2
2
 
3
3
  from pyship import main
4
4
 
5
-
6
5
  if is_main():
7
6
  main()
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.6"
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 run_script(target_app_project_dir: Path, script_file_name: str):
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, but if there's a .bat file to build it, try that. In general the pyship user should have already created their wheel, but this is also
121
- # handy for testing so the pyship repo can be cloned and pytest run, without otherwise having to build the test app's wheel.
122
-
123
- venvs = list(Path(target_app_project_dir).glob("*venv")) # does venv or .venv exist?
124
- if len(venvs) < 1:
125
- # Try to build the venv if it doesn't exist. Note that this uses specific script file names, but they can be set via env var.
126
- run_script(target_app_project_dir, os.environ.get("PYSHIP_MAKE_VENV_SCRIPT", "make_venv.bat")) # will be .sh for Linux/MacOS whenever they're supported ...
127
-
128
- # run script to build the wheel
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 pyshipupdate import is_windows, copy_tree
16
- import pyship
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, remove_pth: bool, target_app_package_dist_dir: Path, cache_dir: Path, find_links: list) -> 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, remove_pth, find_links)
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
- extract(cache_dir, zip_file, clip_dir)
87
-
88
- # Programmatically edit ._pth file, e.g. python38._pth
89
- # see https://github.com/pypa/pip/issues/4207
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, python_env_dir: Path, target_app_package_dist_dir: Path, remove_pth: bool, find_links: list):
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 python_env_dir: venv or clip dir
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 remove_pth: remove remove python*._pth files as a workaround (see bug URL below)
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
- if remove_pth:
156
- # remove python*._pth
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
- for find_link in find_links:
172
- cmd.extend(["-f", f"file://{str(find_link)}"])
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
- subprocess_run(cmd, python_env_dir)
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
- self.s3_access.set_public_readable(True) # all uploads are public readable (disable upload to keep installers and clips private)
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
@@ -1 +1,8 @@
1
+ import os
2
+
1
3
  dist_dir = "dist"
4
+
5
+
6
+ def is_ci() -> bool:
7
+ """Check if running in a CI environment (GitHub Actions, etc.)."""
8
+ return os.environ.get("CI", "").lower() == "true" or os.environ.get("GITHUB_ACTIONS", "").lower() == "true"
pyship/create_launcher.py CHANGED
@@ -1,9 +1,9 @@
1
- import sys
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
- create the launcher executable
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
- # create launcher
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__ # type: ignore
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] # parent dir of launcher source
40
+ pyship_path = Path(pyship_path_list[0])
43
41
  launcher_module_dir = Path(pyship_path, launcher_application_name)
44
42
 
45
- launcher_exe_filename = f"{target_app_info.name}.exe"
46
- launcher_exe_path = Path(app_path_output, target_app_info.name, launcher_exe_filename)
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
- python_interpreter_path = sys.executable
51
- if python_interpreter_path is None or len(python_interpreter_path) < 1:
52
- log.error("python interpreter path not found")
53
- else:
54
- mkdirs(app_path_output)
55
-
56
- explicit_modules_to_import = [
57
- "ismain",
58
- "sentry_sdk",
59
- "typeguard",
60
- "sentry_sdk.integrations.stdlib",
61
- "pyship", # pyship is needed since launcher calls other routines in pyship
62
- ]
63
-
64
- venv_dir = Path(target_app_info.project_dir, "venv") # venv for the target app
65
- pyinstaller_exe_path = Path(venv_dir, "Scripts", "pyinstaller.exe")
66
- if not pyinstaller_exe_path.exists():
67
- raise FileNotFoundError(str(pyinstaller_exe_path))
68
- command_line = [str(pyinstaller_exe_path), "--clean", "-i", str(icon_path), "-n", target_app_info.name, "--distpath", str(app_path_output.absolute())]
69
- for explicit_module_to_import in explicit_modules_to_import:
70
- # modules pyinstaller doesn't seem to be able to find on its own
71
- command_line.extend(["--hiddenimport", explicit_module_to_import])
72
-
73
- # "-F" or "--onefile" is too slow of a start up - was measured at 15 sec for the launch app (experiment with just a print as the app was still 2 sec startup). --onedir is ~1 sec.
74
- # I could probably get the full app down a bit, but it'll never be 1 sec.
75
- # https://stackoverflow.com/questions/9469932/app-created-with-pyinstaller-has-a-slow-startup
76
- # command_line.append("--onefile")
77
- command_line.append("--onedir")
78
-
79
- if target_app_info.is_gui:
80
- command_line.append("--noconsole")
81
- # command_line.extend(["--debug", "all"]) # todo: remove once we get the launcher working again
82
- site_packages_dir = Path(venv_dir, "Lib", "site-packages")
83
-
84
- # find the launcher source code, so we can make an executable from it
85
- launcher_source_path = None
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.info(f"{launcher_source_path=}")
97
- command_line.append(str(launcher_source_path))
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.extractall(destination_folder)
62
+ safe_extract(tf, destination_folder)
47
63
  elif source_file.suffix == ".gz":
48
64
  with tarfile.open(source) as tf:
49
- tf.extractall(destination_folder)
65
+ safe_extract(tf, destination_folder)
50
66
  else:
51
67
  raise Exception(f"Unsupported file type {source_file.suffix} ({source_file})")
@@ -3,7 +3,7 @@ pyship launcher
3
3
  """
4
4
 
5
5
  from .restart_monitor import RestartMonitor
6
- from .launcher import launch
6
+ from .launcher_standalone import launch
7
7
  from .hash import get_file_sha256
8
8
  from .metadata import calculate_metadata, load_metadata, store_metadata
9
9