Skip to content

Commit 9f0c329

Browse files
add checksums and installation security
1 parent 394d274 commit 9f0c329

File tree

8 files changed

+959
-62
lines changed

8 files changed

+959
-62
lines changed

install_pieces_cli.ps1

Lines changed: 228 additions & 29 deletions
Large diffs are not rendered by default.

install_pieces_cli.sh

100644100755
Lines changed: 228 additions & 19 deletions
Large diffs are not rendered by default.

poetry.lock

Lines changed: 37 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ sentry-sdk = "^2.34.1"
3636

3737
[tool.poetry.group.dev.dependencies]
3838
pytest = "^8.0.0"
39+
pytest-xdist = "^3.5.0"
3940
pyinstaller = "^6.13.0"
4041
requests = "^2.31.0"
4142
pytest-asyncio = "^1.0.0"
@@ -52,9 +53,23 @@ build-backend = "poetry.core.masonry.api"
5253
pieces = "pieces.app:main"
5354

5455
[tool.pytest.ini_options]
55-
asyncio_mode = "strict"
56-
asyncio_default_fixture_loop_scope = "function"
5756
testpaths = ["tests"]
5857
python_files = ["test_*.py", "*_test.py"]
5958
python_classes = ["Test*"]
6059
python_functions = ["test_*"]
60+
asyncio_mode = "strict"
61+
asyncio_default_fixture_loop_scope = "function"
62+
63+
# Concurrent execution settings
64+
addopts = [
65+
"--strict-markers",
66+
"--tb=short",
67+
"-ra",
68+
"--showlocals",
69+
]
70+
71+
# Logging for better debugging
72+
log_cli = true
73+
log_cli_level = "INFO"
74+
log_cli_format = "%(asctime)s [%(levelname)8s] %(name)s: %(message)s"
75+
log_cli_date_format = "%Y-%m-%d %H:%M:%S"

src/pieces/command_interface/manage_commands/utils.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,43 @@ def _check_command_availability(command: str) -> bool:
3535
def _get_executable_location() -> Optional[Path]:
3636
"""Get the location of the current pieces executable."""
3737
try:
38-
return Path(os.path.abspath(sys.argv[0]))
38+
# Method 1: Try sys.argv[0] if it looks like an executable path
39+
if sys.argv and sys.argv[0]:
40+
argv_path = Path(os.path.abspath(sys.argv[0]))
41+
# If it's a Python file, we're likely running via python -m pieces
42+
if argv_path.suffix in {".py", ".pyc"}:
43+
# Try to find 'pieces' in PATH instead
44+
pieces_exec = shutil.which("pieces")
45+
if pieces_exec:
46+
return Path(pieces_exec)
47+
else:
48+
# Direct executable invocation
49+
return argv_path
50+
51+
# Method 2: Try finding 'pieces' in PATH
52+
pieces_exec = shutil.which("pieces")
53+
if pieces_exec:
54+
return Path(pieces_exec)
55+
56+
# Method 3: Check if we're in a known installation structure
57+
# This handles cases where we're running from a venv or specific install location
58+
current_file = Path(__file__).resolve()
59+
60+
# Check for installer method structure: ~/.pieces-cli/venv/lib/python*/site-packages/pieces/...
61+
pieces_cli_dir = Path.home() / ".pieces-cli"
62+
if pieces_cli_dir in current_file.parents:
63+
wrapper_script = pieces_cli_dir / "pieces"
64+
if wrapper_script.exists():
65+
return wrapper_script
66+
67+
# Method 4: Look for pieces executable relative to current Python
68+
python_dir = Path(sys.executable).parent
69+
for name in ["pieces", "pieces.exe", "pieces.cmd"]:
70+
candidate = python_dir / name
71+
if candidate.exists():
72+
return candidate
73+
74+
return None
3975
except Exception:
4076
return None
4177

src/pieces/command_interface/simple_commands.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from pieces.gui import print_version_details
1717
from pieces import __version__
1818
from pieces.settings import Settings
19-
from pieces.help_structure import HelpBuilder
19+
from pieces.help_structure import CommandHelp, HelpBuilder
2020

2121

2222
class RunCommand(BaseCommand):
@@ -273,8 +273,14 @@ def get_help(self) -> str:
273273
def get_description(self) -> str:
274274
return "Update PiecesOS"
275275

276-
def get_examples(self) -> list[str]:
277-
return ["pieces update"]
276+
def get_examples(self) -> CommandHelp:
277+
builder = HelpBuilder()
278+
279+
builder.section(
280+
header="Update PiecesOS:", command_template="pieces update"
281+
).example("pieces update", "Update PiecesOS to the latest version")
282+
283+
return builder.build()
278284

279285
def get_docs(self) -> str:
280286
return URLs.CLI_UPDATE_DOCS.value
@@ -284,7 +290,6 @@ def execute(self, **kwargs) -> int | CommandResult:
284290
return 0 if update_pieces_os() else 1
285291

286292

287-
288293
class RestartPiecesOSCommand(BaseCommand):
289294
"""Command to restart PiecesOS."""
290295

tests/manage_commands/test_utils.py

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,15 +80,79 @@ class TestExecutableLocation:
8080

8181
def test_finds_pieces_executable(self):
8282
"""Test finding pieces executable using sys.argv[0]."""
83-
with patch("sys.argv", ["/usr/local/bin/pieces", "manage", "status"]):
84-
result = _get_executable_location()
85-
assert result == Path("/usr/local/bin/pieces")
83+
test_path = "/usr/local/bin/pieces"
84+
with patch("sys.argv", [test_path, "manage", "status"]):
85+
with patch("shutil.which", return_value=None): # Disable PATH fallback
86+
result = _get_executable_location()
87+
# Convert expected path to absolute path for platform compatibility
88+
expected = Path(os.path.abspath(test_path))
89+
assert result == expected
8690

8791
def test_executable_not_found(self):
88-
"""Test when sys.argv is empty."""
92+
"""Test when sys.argv is empty and no fallbacks work."""
8993
with patch("sys.argv", []):
90-
result = _get_executable_location()
91-
assert result is None
94+
with patch("shutil.which", return_value=None):
95+
with patch("pathlib.Path.exists", return_value=False):
96+
result = _get_executable_location()
97+
assert result is None
98+
99+
def test_finds_pieces_via_path(self):
100+
"""Test finding pieces executable via PATH when sys.argv[0] is a Python file."""
101+
with patch("sys.argv", ["/path/to/python/script.py", "manage", "status"]):
102+
with patch("shutil.which", return_value="/usr/local/bin/pieces"):
103+
result = _get_executable_location()
104+
assert result == Path("/usr/local/bin/pieces")
105+
106+
def test_finds_pieces_via_path_fallback(self):
107+
"""Test finding pieces executable via PATH as fallback."""
108+
with patch("sys.argv", []): # Empty sys.argv
109+
with patch("shutil.which", return_value="/usr/local/bin/pieces"):
110+
result = _get_executable_location()
111+
assert result == Path("/usr/local/bin/pieces")
112+
113+
def test_finds_pieces_in_installer_structure(self):
114+
"""Test finding pieces in installer directory structure."""
115+
installer_dir = Path.home() / ".pieces-cli"
116+
wrapper_script = installer_dir / "pieces"
117+
118+
with patch("sys.argv", []):
119+
with patch("shutil.which", return_value=None):
120+
with patch(
121+
"pathlib.Path.__file__",
122+
str(
123+
installer_dir
124+
/ "venv/lib/python3.11/site-packages/pieces/app.py"
125+
),
126+
):
127+
with patch.object(wrapper_script, "exists", return_value=True):
128+
# Need to mock the Path constructor and parents
129+
mock_current_file = Mock()
130+
mock_current_file.parents = [
131+
installer_dir / "venv/lib/python3.11/site-packages/pieces",
132+
installer_dir / "venv/lib/python3.11/site-packages",
133+
installer_dir / "venv/lib/python3.11",
134+
installer_dir / "venv/lib",
135+
installer_dir / "venv",
136+
installer_dir,
137+
Path.home(),
138+
]
139+
140+
with patch(
141+
"pathlib.Path.resolve", return_value=mock_current_file
142+
):
143+
result = _get_executable_location()
144+
assert result == wrapper_script
145+
146+
def test_finds_pieces_relative_to_python(self):
147+
"""Test finding pieces executable relative to current Python."""
148+
python_dir = Path(sys.executable).parent
149+
pieces_executable = python_dir / "pieces"
150+
151+
with patch("sys.argv", []):
152+
with patch("shutil.which", return_value=None):
153+
with patch.object(pieces_executable, "exists", return_value=True):
154+
result = _get_executable_location()
155+
assert result == pieces_executable
92156

93157
def test_exception_handling(self):
94158
"""Test exception handling during detection."""

0 commit comments

Comments
 (0)