Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 27 additions & 20 deletions synapse/cli/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import glob
import json
import os
import platform
import re
import shutil
import subprocess
Expand Down Expand Up @@ -41,7 +42,9 @@ def validate_manifest(manifest_path: str) -> dict[str, Any] | Literal[False]:

def detect_arch() -> str:
"""Return an architecture tag suffix (``arm64`` or ``amd64``)."""
arch = subprocess.check_output(["uname", "-m"]).decode("utf-8").strip()
# platform.machine() reports e.g. "arm64" (macOS), "aarch64" (Linux),
# or "ARM64"/"AMD64" (Windows), so compare case-insensitively.
arch = platform.machine().lower()
return "arm64" if arch in ("arm64", "aarch64") else "amd64"


Expand Down Expand Up @@ -205,6 +208,27 @@ def build_docker_image(
return tags


def locate_app_binary(app_dir: str, app_name: str) -> str | None:
"""Return the path of the first file named *app_name* under *app_dir*.

Pure-Python replacement for the previous
``find <app_dir> -type f -name <app_name> -not -path '*/.*'`` shell-out
(which does not exist on Windows): regular files only, exact name match,
hidden directories and hidden names excluded. Returns ``None`` when no
match is found.
"""
if app_name.startswith("."):
return None

for root, dirnames, filenames in os.walk(app_dir):
dirnames[:] = [d for d in dirnames if not d.startswith(".")]
if app_name in filenames:
candidate = os.path.join(root, app_name)
if os.path.isfile(candidate):
return candidate
return None


def build_app(
app_dir: str, app_name: str, force_rebuild: bool = False, clean: bool = False
) -> bool:
Expand Down Expand Up @@ -331,25 +355,8 @@ def build_app(
)

try:
binary_found = subprocess.run(
[
"find",
app_dir,
"-type",
"f",
"-name",
app_name,
"-not",
"-path",
"*/.*",
],
capture_output=True,
text=True,
check=False,
).stdout.strip()

if binary_found:
located = binary_found.split("\n")[0]
located = locate_app_binary(app_dir, app_name)
if located:
build_dir = os.path.dirname(binary_path)
os.makedirs(build_dir, exist_ok=True)
shutil.copy(located, binary_path)
Expand Down
116 changes: 116 additions & 0 deletions synapse/tests/cli/test_locate_app_binary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""Tests for ``locate_app_binary``, the pure-Python replacement for the
``find <app_dir> -type f -name <app_name> -not -path '*/.*'`` shell-out.

The behavioral tests below encode the old ``find`` invocation's semantics so
they run on every platform (including Windows, where ``find`` is a different
program entirely). ``test_parity_with_gnu_find`` additionally runs the real
``find`` command side by side on POSIX hosts to demonstrate identical
functionality against the original implementation.
"""

from __future__ import annotations

import os
import subprocess
import sys

import pytest

from synapse.cli.build import locate_app_binary


APP = "myapp"


def _touch(*parts):
path = os.path.join(*[str(p) for p in parts])
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w") as fh:
fh.write("bin")
return path


def test_finds_binary_in_nested_build_dir(tmp_path):
expected = _touch(tmp_path, "build", "aarch64", "release", APP)
assert locate_app_binary(str(tmp_path), APP) == expected


def test_returns_none_when_absent(tmp_path):
_touch(tmp_path, "build", "other-binary")
assert locate_app_binary(str(tmp_path), APP) is None


def test_hidden_directories_are_skipped(tmp_path):
# A match buried under a hidden dir (e.g. .git) must not win over a
# visible one -- and must not be returned at all.
_touch(tmp_path, ".git", "objects", APP)
visible = _touch(tmp_path, "build", APP)
assert locate_app_binary(str(tmp_path), APP) == visible


def test_match_only_under_hidden_directory_returns_none(tmp_path):
_touch(tmp_path, ".cache", APP)
assert locate_app_binary(str(tmp_path), APP) is None


def test_exact_name_match_only(tmp_path):
_touch(tmp_path, "build", APP + ".o")
_touch(tmp_path, "build", "lib" + APP)
assert locate_app_binary(str(tmp_path), APP) is None


def test_directory_with_matching_name_is_not_a_match(tmp_path):
# `find -type f` never matches directories.
os.makedirs(os.path.join(str(tmp_path), "build", APP))
assert locate_app_binary(str(tmp_path), APP) is None


def test_hidden_app_name_returns_none(tmp_path):
# `-not -path '*/.*'` also excluded hidden *files*.
_touch(tmp_path, "build", ".hidden")
assert locate_app_binary(str(tmp_path), ".hidden") is None


def test_root_level_match_wins_over_nested(tmp_path):
# os.walk yields the walk root first, so a top-level match is returned
# before any nested one. (With multiple matches the old `find` contract
# was only ever "some match" -- its first output line -- and the caller
# copies whichever match is returned to the canonical location.)
root_match = _touch(tmp_path, APP)
_touch(tmp_path, "build", APP)
assert locate_app_binary(str(tmp_path), APP) == root_match


@pytest.mark.skipif(
sys.platform == "win32", reason="POSIX `find` is not available on Windows"
)
@pytest.mark.parametrize(
"layout",
[
[], # empty tree
["build/aarch64/" + APP], # the common case
[".git/" + APP], # only a hidden match
["a/" + APP, "b/" + APP, APP], # multiple matches
["build/" + APP + ".dbg", "docs/readme"], # near-misses only
],
)
def test_parity_with_gnu_find(tmp_path, layout):
"""The exact `find` argv that locate_app_binary replaced is run against the same tree to verify identical behavior."""
for rel in layout:
_touch(tmp_path, *rel.split("/"))

find_out = subprocess.run(
["find", str(tmp_path), "-type", "f", "-name", APP,
"-not", "-path", "*/.*"],
capture_output=True,
text=True,
check=False,
).stdout.strip()
find_matches = find_out.split("\n") if find_out else []

result = locate_app_binary(str(tmp_path), APP)

if not find_matches:
assert result is None
else:
assert result in find_matches