Support optional CLI and MCP in Windows source archives
This commit is contained in:
1 parent
4e277a107f
commit
2be6b3d2b4
4 files changed
+72
-5
No files matched your search
@@ -11,6 +11,30 @@ Build the CLI with `cargo build --release -p searchi-cli --locked`. Create and
|
||||
index files through the normal CLI/desktop workflow first. MCP requires an
|
||||
explicit existing database; it never creates a new empty database after a typo.
|
||||
|
||||
Windows source archives can include the compiled CLI using
|
||||
`python tools/package_windows.py --cli target/release/searchi.exe`. In that
|
||||
archive, `searchi.exe` opens the desktop and `searchi-cli.exe` runs CLI/MCP
|
||||
commands. Keep both beside the bundled runtime helpers. The published v0.1.0
|
||||
installer does not include the CLI; this packaging option is for a subsequent
|
||||
archive and does not change that release.
|
||||
|
||||
Packaging evidence, Windows VM 2026-10-13: 16 package tests passed in normal
|
||||
and optimized Python modes, including CLI checksum inventory and rejection of
|
||||
a mismatched CLI architecture without replacing the previous archive. The
|
||||
locked release CLI built successfully. An actual local archive containing the
|
||||
existing desktop release and this CLI passed size/SHA verification for all 40
|
||||
files. Its extracted `searchi-cli.exe mcp --help` executed successfully outside
|
||||
the build directory. This checks packaging and startup, not a new semantic MCP
|
||||
session, GPU behavior or a clean-machine install.
|
||||
|
||||
Ignored archive: `.searchi/packages/searchi-windows-agent-preview.zip`,
|
||||
152,098,987 bytes, SHA-256
|
||||
`9721a657458f140dc9271b508c810ca52b7e4b89ccff8f1cd3edb053dbe591a0`.
|
||||
CLI SHA-256:
|
||||
`7b40a3959e5bca811f79e0a0da1e0cf77505ede703cf92c8394f79b9a6167ab5`.
|
||||
The archive is local only; the Gitea attachment size limit previously rejected
|
||||
even the smaller desktop archive. Published installer contents are unchanged.
|
||||
|
||||
For names/content only:
|
||||
|
||||
```sh
|
||||
@@ -105,4 +129,4 @@ cargo test -p searchi-cli --test mcp
|
||||
Default tests use genuine SQLite retrieval without downloaded model assets.
|
||||
Hybrid mode uses the same real Google service; it has no simulated embeddings.
|
||||
Native Linux/macOS MCP execution and cross-platform packaging require their own
|
||||
recorded checks; Windows CPU checks do not establish those releases.
|
||||
recorded checks; Windows CPU checks do not establish those releases.
|
||||
+13
-1
@@ -210,11 +210,23 @@ python tools/embeddinggemma2/build_runtime.py --backend auto --jobs 2
|
||||
python tools/embeddinggemma2/stage_runtime.py --backend auto
|
||||
python tools/media-runtime/provision.py --stage
|
||||
Set-Location apps/desktop
|
||||
npm run tauri -- build --no-bundle
|
||||
npm run tauri -- build --features custom-protocol --no-bundle
|
||||
Set-Location ../..
|
||||
python tools/package_windows.py
|
||||
```
|
||||
|
||||
To include the CLI and read-only MCP server in this source archive:
|
||||
|
||||
```powershell
|
||||
cargo build --release -p searchi-cli --locked
|
||||
python tools/package_windows.py --cli target/release/searchi.exe
|
||||
```
|
||||
|
||||
The additional binary is named `searchi-cli.exe`; `searchi.exe` remains the
|
||||
desktop launcher. Its model cache and database must be selected explicitly,
|
||||
as described in [agent integration](AGENT-INTEGRATION.md). This option does not
|
||||
add the CLI to the already published v0.1.0 installer.
|
||||
|
||||
The generated `.searchi/packages/searchi-windows.zip` contains the desktop,
|
||||
matching-architecture Google worker, FFmpeg/FFprobe and notices. It excludes
|
||||
model caches, local settings and indexes. `PACKAGE.json` records SHA-256 for
|
||||
|
||||
@@ -101,7 +101,7 @@ def pe_machine(path):
|
||||
return struct.unpack_from("<H", signature, 4)[0]
|
||||
|
||||
|
||||
def package(root, executable, output):
|
||||
def package(root, executable, output, cli=None):
|
||||
resources = root / "apps/desktop/src-tauri/resources"
|
||||
machine = pe_machine(executable)
|
||||
architecture = {0x8664: "x86_64", 0xAA64: "aarch64"}.get(machine)
|
||||
@@ -145,6 +145,10 @@ def package(root, executable, output):
|
||||
media = media_provenance(root, resources, architecture)
|
||||
expected.update(media)
|
||||
selected = [(executable, "searchi.exe"), (root / "LICENSE", "LICENSE.txt")]
|
||||
if cli is not None:
|
||||
if cli.is_symlink() or not cli.is_file() or not 0 < cli.stat().st_size <= MAX_MEMBER:
|
||||
raise ValueError("CLI executable is missing, linked or oversized")
|
||||
selected.append((cli, "searchi-cli.exe"))
|
||||
for name in ["searchi-gemma-worker.exe", "LLAMA-LICENSE.txt", "MSVC-REDISTRIBUTION.txt", "vulkan-1.dll", "VULKAN-LICENSE.txt", "media-runtime/manifest.json",
|
||||
"media-runtime/bin/ffmpeg.exe", "media-runtime/bin/ffprobe.exe",
|
||||
"licenses/Inter-OFL.txt", "licenses/Fluent-MIT.txt", "media-runtime/licenses/LICENSE.txt"]:
|
||||
@@ -162,7 +166,7 @@ def package(root, executable, output):
|
||||
for path, name in selected:
|
||||
if not path.is_file():
|
||||
raise ValueError(f"Required package file is missing: {name}")
|
||||
if path != executable and path != root / "LICENSE" and not path.resolve().is_relative_to(resources.resolve()):
|
||||
if path not in (executable, root / "LICENSE", cli) and not path.resolve().is_relative_to(resources.resolve()):
|
||||
raise ValueError("Resource resolves outside the staging directory")
|
||||
if name.endswith((".exe", ".dll")) and pe_machine(path) != machine:
|
||||
raise ValueError(f"Executable architecture differs: {name}")
|
||||
@@ -194,6 +198,14 @@ def package(root, executable, output):
|
||||
"First launch downloads approximately 865 MB of verified Google model files.\n"
|
||||
"Choose Add folder to index your own files; ordinary search stays local.\n"
|
||||
"This is an unsigned development build, not a certified production release.\n")
|
||||
if cli is not None:
|
||||
archive.writestr("AGENT-SETUP.txt",
|
||||
"searchi-cli.exe provides command-line search and a read-only MCP server.\n"
|
||||
"Run searchi-cli.exe --help, or searchi-cli.exe mcp --help.\n"
|
||||
"Select your existing desktop index explicitly with --db; do not create another index accidentally.\n"
|
||||
"For semantic MCP search also supply --model-dir and --embedding-worker.\n"
|
||||
"Use the verified model cache prepared by the desktop and the bundled searchi-gemma-worker.exe.\n"
|
||||
"See https://tea.karlcom.de/karl/searchi/src/branch/main/docs/AGENT-INTEGRATION.md\n")
|
||||
os.replace(temporary_path, output)
|
||||
finally:
|
||||
temporary_path.unlink(missing_ok=True)
|
||||
@@ -204,8 +216,9 @@ def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--executable", type=Path, default=ROOT / "apps/desktop/src-tauri/target/release/searchi-desktop.exe")
|
||||
parser.add_argument("--output", type=Path, default=ROOT / ".searchi/packages/searchi-windows.zip")
|
||||
parser.add_argument("--cli", type=Path, help="Include the compiled CLI/MCP client as searchi-cli.exe")
|
||||
args = parser.parse_args()
|
||||
architecture = package(ROOT, args.executable, args.output)
|
||||
architecture = package(ROOT, args.executable, args.output, args.cli)
|
||||
print(f"Packaged Windows {architecture}: {args.output}")
|
||||
|
||||
|
||||
|
||||
@@ -30,6 +30,24 @@ def inventory(resources):
|
||||
|
||||
|
||||
class PackageTests(unittest.TestCase):
|
||||
def test_optional_cli_manifest_and_architecture(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root, _, app, output = self.fixture(directory)
|
||||
cli = root / "build/cli.exe"
|
||||
executable(cli)
|
||||
package(root, app, output, cli)
|
||||
with zipfile.ZipFile(output) as archive:
|
||||
self.assertEqual(archive.read("searchi-cli.exe"), cli.read_bytes())
|
||||
manifest = json.loads(archive.read("PACKAGE.json"))
|
||||
record = next(row for row in manifest["files"] if row["path"] == "searchi-cli.exe")
|
||||
self.assertEqual(record["sha256"], hashlib.sha256(cli.read_bytes()).hexdigest())
|
||||
self.assertIn("--db", archive.read("AGENT-SETUP.txt").decode())
|
||||
previous = output.read_bytes()
|
||||
executable(cli, 0xAA64)
|
||||
with self.assertRaisesRegex(ValueError, "architecture differs"):
|
||||
package(root, app, output, cli)
|
||||
self.assertEqual(output.read_bytes(), previous)
|
||||
|
||||
def fixture(self, directory):
|
||||
root = Path(directory)
|
||||
resources = root / "apps/desktop/src-tauri/resources"
|
||||
|
||||
Reference in new issue
Block a user