pypilot-cli 1.0.5__tar.gz → 1.0.6__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pypilot-cli
3
- Version: 1.0.5
3
+ Version: 1.0.6
4
4
  Summary: Deterministic Python environment doctor, dependency solver, and hardware compatibility manager.
5
5
  Project-URL: Homepage, https://github.com/Abdullah-Masood-05/pypilot
6
6
  Project-URL: Repository, https://github.com/Abdullah-Masood-05/pypilot
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pypilot-cli"
7
- version = "1.0.5"
7
+ version = "1.0.6"
8
8
  description = "Deterministic Python environment doctor, dependency solver, and hardware compatibility manager."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -10,7 +10,7 @@ from typing import List, Optional
10
10
 
11
11
  from .binary import ensure_binary, get_cache_dir
12
12
 
13
- __version__ = "1.0.5"
13
+ __version__ = "1.0.6"
14
14
 
15
15
 
16
16
  def run(args: Optional[List[str]] = None, capture_output: bool = False) -> subprocess.CompletedProcess[str]:
@@ -17,7 +17,7 @@ import zipfile
17
17
  from pathlib import Path
18
18
 
19
19
  GITHUB_REPO = "Abdullah-Masood-05/pypilot"
20
- DEFAULT_TAG = "v1.0.4"
20
+ DEFAULT_TAG = "v1.0.5"
21
21
  # Oldest helper accepted from ~/.cargo/bin or PATH.
22
22
  MIN_VERSION = (1, 0, 2)
23
23
  # The release this package ships with. A cached download older than this is
@@ -87,7 +87,10 @@ def probe_binary(path: Path | str) -> tuple[int, int, int] | None:
87
87
  [str(path), "--version"],
88
88
  capture_output=True,
89
89
  text=True,
90
- timeout=5,
90
+ # macOS scans a freshly downloaded binary on its first launch, which
91
+ # can take several seconds. Timing out here made a good engine look
92
+ # broken and triggered a needless re-download.
93
+ timeout=30,
91
94
  check=False,
92
95
  )
93
96
  if res.returncode == 0:
@@ -143,14 +146,37 @@ def download_binary(tag: str = DEFAULT_TAG) -> Path:
143
146
  else:
144
147
  raise FileNotFoundError(f"{bin_name} not found in downloaded archive {archive_name}")
145
148
 
146
- shutil.copy2(extracted_bin, dest_binary)
147
-
148
- if sys.platform != "win32":
149
- dest_binary.chmod(dest_binary.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
149
+ install_binary(extracted_bin, dest_binary)
150
150
 
151
151
  return dest_binary
152
152
 
153
153
 
154
+ def install_binary(src: Path, dest: Path) -> None:
155
+ """Put `src` at `dest` as a new file, never by rewriting `dest` in place.
156
+
157
+ macOS caches what it knows about an executable per file. Overwriting a
158
+ binary that was just run leaves the old cache entry pointing at new bytes,
159
+ and the next launch fails with "Exec format error". Copying to a temporary
160
+ name and renaming over `dest` gives it a fresh file instead.
161
+
162
+ Windows has no such cache, but its antivirus briefly holds a freshly
163
+ written .exe open, which makes an immediate rename fail with "Access is
164
+ denied". There, a plain copy is both sufficient and reliable.
165
+ """
166
+ if sys.platform == "win32":
167
+ shutil.copy2(src, dest)
168
+ return
169
+
170
+ tmp = dest.with_name(f".{dest.name}.{os.getpid()}.tmp")
171
+ try:
172
+ shutil.copy2(src, tmp)
173
+ tmp.chmod(tmp.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
174
+ os.replace(tmp, dest)
175
+ finally:
176
+ if tmp.exists():
177
+ tmp.unlink()
178
+
179
+
154
180
  def ensure_binary() -> Path:
155
181
  """
156
182
  Locates an existing binary on PATH or in cache, or downloads it from GitHub.
@@ -87,3 +87,26 @@ def test_extract_archive_refuses_members_outside_the_destination(tmp_path):
87
87
  with pytest.raises(tarfile.TarError):
88
88
  extract_archive(archive, out)
89
89
  assert not (tmp_path / "escaped").exists()
90
+
91
+
92
+ def test_install_binary_replaces_instead_of_rewriting_in_place(tmp_path):
93
+ import os
94
+ import sys
95
+
96
+ from pypilot.binary import install_binary
97
+
98
+ dest = tmp_path / "pypilot"
99
+ dest.write_bytes(b"old engine")
100
+ old_inode = os.stat(dest).st_ino
101
+
102
+ src = tmp_path / "new"
103
+ src.write_bytes(b"new engine")
104
+ install_binary(src, dest)
105
+
106
+ assert dest.read_bytes() == b"new engine"
107
+ # A rename gives `dest` a new file; rewriting in place would keep the inode.
108
+ if sys.platform != "win32":
109
+ assert os.stat(dest).st_ino != old_inode
110
+ assert os.access(dest, os.X_OK)
111
+ # No temporary file is left behind.
112
+ assert sorted(p.name for p in tmp_path.iterdir()) == ["new", "pypilot"]
@@ -1157,7 +1157,7 @@ wheels = [
1157
1157
 
1158
1158
  [[package]]
1159
1159
  name = "pypilot-cli"
1160
- version = "1.0.5"
1160
+ version = "1.0.6"
1161
1161
  source = { editable = "." }
1162
1162
 
1163
1163
  [package.dev-dependencies]
File without changes
File without changes
File without changes
File without changes