diff --git a/Package.swift b/Package.swift index 96efa8a..eaf23db 100644 --- a/Package.swift +++ b/Package.swift @@ -62,24 +62,18 @@ let buildingForAndroid = Context.environment["TARGET_OS_ANDROID"] == "1" // `Shell+SwiftPortsCommands` surface is already `#if !os(Android)`, so on // Android we reference none of them; drop the list from the graph too. let swiftPortsCommandProducts: [Target.Dependency] = buildingForAndroid ? [] : [ - .product(name: "JqCommand", package: "SwiftPorts"), - .product(name: "GhCommand", package: "SwiftPorts"), - .product(name: "GlabCommand", package: "SwiftPorts"), - .product(name: "GitCommand", package: "SwiftPorts"), - .product(name: "TarCommand", package: "SwiftPorts"), - .product(name: "ZipCommand", package: "SwiftPorts"), - .product(name: "UnzipCommand", package: "SwiftPorts"), - .product(name: "GzipCommand", package: "SwiftPorts"), - .product(name: "Bzip2Command", package: "SwiftPorts"), - .product(name: "XzCommand", package: "SwiftPorts"), - .product(name: "ZstdCommand", package: "SwiftPorts"), - .product(name: "Lz4Command", package: "SwiftPorts"), - .product(name: "RgCommand", package: "SwiftPorts"), - .product(name: "FdCommand", package: "SwiftPorts"), - // NB: sqlite3 is NOT here. It's registered from the + // SwiftPorts vends its whole ArgumentParser-backed CLI surface — jq, gh, + // glab, git, rg, fd, the archive + compression families — as ready-to- + // install ShellKit `Command`s behind one product. `Shell+SwiftPortsCommands` + // installs them as virtual bins; no bridging happens in SwiftBash. Gated to + // non-Android: the products are ArgumentParser-bearing and SwiftPorts' + // transitive C-library graph isn't Android-supported. + // + // NB: sqlite3 is NOT here. It's registered from SwiftPorts' // ArgumentParser-free `Sqlite3Shell` product (a direct, unconditional // BashCommandKit dependency below) so the `sqlite3` builtin works on // Android too — see `Shell+Sqlite3.swift`. + .product(name: "SwiftPortsCommands", package: "SwiftPorts"), ] var products: [Product] = [ @@ -143,15 +137,6 @@ let package = Package( // Pinned to `main` until SwiftPorts ships a tagged release. .package(url: "https://github.com/Cocoanetics/SwiftPorts", branch: "main"), - // SQLiteKit — the SQLite SDK and its ArgumentParser-free - // `Sqlite3Shell` CLI driver (the argv parser + dot-command / REPL - // engine). `Shell+Sqlite3.swift` registers the `sqlite3` builtin from - // `Sqlite3Shell` via a native ShellKit command, so it works on every - // platform — Android included. (The SDK was extracted from SwiftPorts; - // the shell driver followed it, so this is now a direct dependency.) - // Pinned to `main` until SQLiteKit ships a tagged release. - .package(url: "https://github.com/Cocoanetics/SQLiteKit", - branch: "main"), // SwiftScript — Swift tree-walking interpreter that reads // its IO / FS / network / identity / exit through // `ShellKit.Shell.current`. The `BashSwiftScript` target @@ -204,12 +189,13 @@ let package = Package( "BashInterpreter", .product(name: "ArgumentParser", package: "swift-argument-parser"), .product(name: "Crypto", package: "swift-crypto"), - // sqlite3's shell driver — SQLiteKit's ArgumentParser-free - // `Sqlite3Shell` (Parser + `Sqlite3Executable`). Depended on - // unconditionally (incl. Android): it builds wherever the - // SQLiteKit SDK does, and `Shell+Sqlite3.swift` registers the - // `sqlite3` builtin from it via a native ShellKit command. - .product(name: "Sqlite3Shell", package: "SQLiteKit"), + // sqlite3's shell port — SwiftPorts' ArgumentParser-free + // `Sqlite3Shell` (Parser + `Sqlite3Executable` + the + // `Sqlite3Builtin` ShellKit command). Depended on + // unconditionally (incl. Android): being ArgumentParser-free it + // builds wherever its SQLiteKit-SDK + ShellKit subgraph does, + // and `Shell+Sqlite3.swift` installs the `sqlite3` builtin. + .product(name: "Sqlite3Shell", package: "SwiftPorts"), // The rest of the SwiftPorts CLI family (jq / gh / glab / // git / the archive + compression set / rg / fd), // registered as builtins by `registerSwiftPortsCommands()`. diff --git a/Sources/BashCommandKit/API/Shell+Sqlite3.swift b/Sources/BashCommandKit/API/Shell+Sqlite3.swift index 1d0506c..f99ff9d 100644 --- a/Sources/BashCommandKit/API/Shell+Sqlite3.swift +++ b/Sources/BashCommandKit/API/Shell+Sqlite3.swift @@ -3,68 +3,26 @@ import Sqlite3Shell extension Shell { - /// Register `sqlite3` — SwiftPorts' SQLite shell port — as a builtin. + /// Register SwiftPorts' `sqlite3` shell port as a builtin at + /// `/usr/bin/sqlite3` (where real macOS ships it). /// - /// Unlike the rest of the SwiftPorts CLI family (`jq` / `gh` / `git` / - /// the archive + compression set), which ``registerSwiftPortsCommands()`` - /// installs from ArgumentParser-based `*Command` products and which are - /// dropped on Android, `sqlite3` is driven through the - /// ArgumentParser-free ``Sqlite3Shell/Sqlite3Executable`` via a native - /// ShellKit ``Command``. That target builds on every platform, so this - /// builtin works everywhere — Android included. It is therefore - /// registered unconditionally from ``registerStandardCommands()``, - /// outside the `#if !os(Android)` gate the rest of the family sits behind. + /// `Sqlite3Builtin` is SwiftPorts' ArgumentParser-free ShellKit ``Command`` + /// face of the `sqlite3` shell port (`Sqlite3Executable`). Because it + /// carries no ArgumentParser it installs on every platform — Android + /// included — unlike the rest of the SwiftPorts family + /// (``registerSwiftPortsCommands()``, which is `#if !os(Android)`). It is + /// therefore registered unconditionally, from ``registerStandardCommands()``. /// - /// The driver reads/writes through ``Shell/current`` (stdin / stdout / - /// stderr) and resolves + authorizes database / `.read` / `.backup` - /// paths through the host sandbox, so the builtin participates fully in - /// pipes / `<` `>` redirection / `$(...)` capture, exactly like the - /// bridged ones. + /// This installer adds nothing of its own: the command and all its behavior + /// (argv parsing, dot-commands, the REPL, sandbox-authorized path handling, + /// version reporting) live in SwiftPorts on ShellKit's command base. + /// SwiftBash only installs it. /// - /// Installed at `/usr/bin/sqlite3` (where real macOS ships it); it isn't - /// in `BinCatalog`, so the explicit-path overload is used. The - /// registry-driven `BinCatalogOverlay` surfaces it under `/usr/bin` from - /// this install alone, so `ls /usr/bin/sqlite3`, `[ -x … ]`, and + /// `sqlite3` isn't in `BinCatalog`, so the explicit-path overload is used; + /// the registry-driven `BinCatalogOverlay` then surfaces it under `/usr/bin` + /// from this install alone, so `ls /usr/bin/sqlite3`, `[ -x … ]`, and /// tool-presence checks all succeed — not just `which sqlite3`. public func registerSqlite3() { install(Sqlite3Builtin(), at: "/usr/bin/sqlite3") } } - -/// Bridges SwiftPorts' ``Sqlite3Shell/Sqlite3Executable`` (the -/// ArgumentParser-free `sqlite3` driver) to a ShellKit ``Command``. -/// -/// The whole argv is handed to the driver, which does SQLite's single-dash -/// long-option parsing (`-csv`, `-header`, `-separator X`, …), dot-command -/// dispatch, and the REPL itself — no ArgumentParser involved, so this -/// compiles and runs on Android where the `Sqlite3` `AsyncParsableCommand` -/// wrapper can't. The bridged ``AsyncParsableCommandBridge`` it replaces was -/// pure passthrough for this command anyway (the `Sqlite3` type captures -/// argv with `.captureForPassthrough` and forwards it verbatim), so behavior -/// is unchanged on the platforms that had it. -struct Sqlite3Builtin: Command { - let name = "sqlite3" - - func run(_ argv: [String]) async throws -> ExitStatus { - // `Sqlite3Executable` expects argv without the command name, per the - // `execve` convention `ArgumentParserBridge.dispatch` also follows. - let args = Array(argv.dropFirst()) - - // Uniform GNU-style `--version` banner, matching - // `ArgumentParserBridge.dispatch` (the path the other builtins take). - // `sqlite3 -version` (single dash, the spelling real sqlite3 uses) - // is left to the driver, which reports the SQLite library version. - if args.first == "--version" { - Shell.bashCurrent.stdout( - "sqlite3 (SwiftBash) \(SwiftBashVersion.packageVersion)\n") - return .success - } - - let code = try await Sqlite3Executable.run( - argv: args, - stdin: Shell.current.stdin, - stdout: Shell.current.stdout, - stderr: Shell.current.stderr) - return ExitStatus(code) - } -} diff --git a/Sources/BashCommandKit/API/Shell+SwiftPortsCommands.swift b/Sources/BashCommandKit/API/Shell+SwiftPortsCommands.swift index abee457..5640d44 100644 --- a/Sources/BashCommandKit/API/Shell+SwiftPortsCommands.swift +++ b/Sources/BashCommandKit/API/Shell+SwiftPortsCommands.swift @@ -1,153 +1,42 @@ import BashInterpreter -// Android: SwiftPorts is not yet a supported target — its transitive -// C-library graph (libgit2, BoringSSL, swift-archive, the -// pkg-config-driven systemLibrary shims for zlib / lz4 / zstd / -// lzma / bz2) injects unconditional `-lz` / `-ldl` and host -// pkg-config search paths that pull `/lib/x86_64-linux-gnu/` onto -// ld.lld's resolver, breaking Bionic libc symbol resolution. The -// SwiftPorts product references in Package.swift are gated to -// non-Android platforms; mirror that gate at the source level so -// this file's imports don't fail on Android. The companion call -// site in `Shell+StandardCommands.swift` is gated the same way. +// Android: SwiftPorts' ArgumentParser-backed command surface is not a +// supported target there — its transitive C-library graph (libgit2, BoringSSL, +// the pkg-config-driven systemLibrary shims for zlib / lz4 / zstd / lzma / bz2) +// injects unconditional `-lz` / `-ldl` and host pkg-config search paths that +// break Bionic libc resolution. The `SwiftPortsCommands` product is gated to +// non-Android platforms in Package.swift; mirror that gate here so this file's +// import doesn't fail on Android. `sqlite3` is the exception — its +// ArgumentParser-free shell port installs everywhere, registered separately by +// `registerSqlite3()` (see `Shell+Sqlite3.swift`). #if !os(Android) -import Bzip2Command -import FdCommand -import GhCommand -import GitCommand -import GlabCommand -import GzipCommand -import JqCommand -import Lz4Command -import RgCommand -import TarCommand -import UnzipCommand -import XzCommand -import ZipCommand -import ZstdCommand +import SwiftPortsCommands extension Shell { - /// Register every CLI that [SwiftPorts](https://github.com/Cocoanetics/SwiftPorts) - /// ships as Bash builtins on this shell. + /// Install every SwiftPorts CLI port as a Bash builtin (a virtual bin). /// - /// Every SwiftPorts CLI is an `AsyncParsableCommand` whose body - /// reads from / writes to ``ShellKit/Shell/current`` rather than - /// `FileHandle.standard*`, so the registered builtins - /// participate fully in pipes / `<` `>` redirection / `$(...)` - /// capture / background jobs — no `Process()` fork, no OS pipe - /// per pipeline stage. The shell binds itself onto - /// `ShellKit.Shell.current` for every dispatch, so the routing - /// is automatic. + /// SwiftPorts vends each port as a ready-to-install ShellKit ``Command`` — + /// already bridged from its `AsyncParsableCommand` through ShellKit's own + /// `ShellCommandKit`. This installer does **no** bridging of its own: it + /// just places each command at its `BinCatalog` path. The command-building + /// base lives in ShellKit, the ports in SwiftPorts; SwiftBash only installs. /// - /// Registered surface (in order of likely-use): + /// Every SwiftPorts CLI reads from / writes to ``ShellKit/Shell/current`` + /// rather than `FileHandle.standard*`, so the installed builtins + /// participate fully in pipes / `<` `>` redirection / `$(...)` capture / + /// background jobs — no `Process()` fork, no OS pipe per pipeline stage. /// - /// • **`jq`** — pure-Swift JSON processor (full standard - /// surface incl. `--slurpfile` / `--rawfile` / `--args`). - /// • **`gh`** — GitHub CLI (`api`, `auth`, `pr`, `issue`, - /// `release`, `run`, `gist`, `project`, `repo`, …). - /// • **`glab`** — GitLab CLI (`mr`, `issue`, `ci`, `repo`, - /// `release`, `tag`, `variable`, `auth`, `api`). - /// • **`git`** — libgit2-backed `git` (full local-side - /// surface — clone, fetch, pull, push, log, status, diff, - /// stash, rebase, cherry-pick, branch, tag, remote, …). - /// • **Archives:** `tar`, `zip`, `unzip`. - /// • **gzip family:** `gzip` / `gunzip` / `zcat` (always — - /// zlib is on every supported platform). - /// • **bzip2 / xz / zstd / lz4 families** — gated to the - /// platforms where the underlying C library is available. - /// The `#if` guards mirror the platform gates SwiftPorts' - /// own command targets carry, so this code compiles - /// identically on every supported OS. - /// - /// Per-binary personalities (`gunzip` / `zcat` / `bunzip2` / - /// etc.) are separate `AsyncParsableCommand` types in - /// SwiftPorts; we register each one individually so the bash - /// `which` / `type` / `compgen -c` introspection sees them - /// distinctly. + /// Registered surface: `jq`, `gh`, `glab`, `git`, `rg`, `fd`, the archive + /// family (`tar` / `zip` / `unzip`), and the gzip / bzip2 / xz / zstd / lz4 + /// compression families — each gated, in SwiftPorts, to the platforms whose + /// underlying C library is available. `sqlite3` is installed separately by + /// ``registerSqlite3()`` so it works on Android too. public func registerSwiftPortsCommands() { - // jq — JSON processor. Standalone-CLI surface, no - // GitHub-style subcommand tree. - install(Jq.self) - - // gh / glab / git — large multi-tool CLIs. Their - // root command's `subcommands:` list pulls in the entire - // subcommand tree automatically; registering the root - // makes `gh issue list`, `glab mr view`, `git log`, etc. - // all addressable as one builtin per top-level command. - install(GhCommand.self) - install(GlabCommand.self) - install(GitCommand.self) - - // rg / fd — pure-Swift ports of BurntSushi's ripgrep and - // sharkdp's fd. Supersede `BashCommandKit/Commands/RgCommand` - // (whose `register(RgCommand.self)` call was dropped from - // `registerStandardCommands()` for that reason). The new rg - // honours `.gitignore` / `.ignore` / `.rgignore`, walks parent - // dirs, supports `-F` fixed-string matching and - // `--no-require-git`; fd reuses RipgrepKit's walker so its - // gitignore semantics line up. - // - // RipgrepKit's command type is named `Rg` (not `RgCommand`) so - // it doesn't collide with BashCommandKit's local - // `Commands/RgCommand.swift` type — that local type still - // ships for source compat. Use the explicit module-level type - // here to make sure the SwiftPorts one is what gets registered. - install(Rg.self) - // `fd` isn't in the BinCatalog yet — slot under - // `/usr/local/bin` to match the Homebrew / user-skill - // convention used for the rest of the SwiftPorts surface. - install(FdCommand.self, at: "/usr/local/bin/fd") - - // NB: `sqlite3` is registered separately by `registerSqlite3()` - // (called unconditionally from `registerStandardCommands()`), not - // here. It drives SwiftPorts' ArgumentParser-free `Sqlite3Shell` - // through a native ShellKit command, so it works on Android too — - // unlike this `#if !os(Android)`-gated family. See - // `Shell+Sqlite3.swift`. - - // Archive family. - install(TarCommand.self) - install(ZipCommand.self) - install(UnzipCommand.self) - - // gzip personalities — zlib is universally available, no - // platform gate. - install(Gzip.self) - install(Gunzip.self) - install(Zcat.self) - - // bzip2 / zstd — libbz2 / libzstd aren't in the iOS / - // tvOS / watchOS / visionOS SDK and aren't in Android's - // NDK. SwiftPorts gates these command types behind - // `#if os(macOS) || os(Linux) || os(Windows)`; mirror - // that gate here. - #if os(macOS) || os(Linux) || os(Windows) - install(Bzip2.self) - install(Bunzip2.self) - install(Bzcat.self) - - install(Zstd.self) - install(Unzstd.self) - install(Zstdcat.self) - #endif - - // xz / lz4 — Apple platforms back these via the - // Compression framework (`canImport(Compression)`); Linux - // / Windows have system liblzma / liblz4. Android has - // neither. SwiftPorts gates the command types behind - // `#if canImport(Compression) || os(Linux) || os(Windows)`; - // mirror that. - #if canImport(Compression) || os(Linux) || os(Windows) - install(Xz.self) - install(Unxz.self) - install(Xzcat.self) - - install(Lz4.self) - install(Unlz4.self) - install(Lz4cat.self) - #endif + for command in SwiftPortsCommands.argumentParserCommands { + install(command) + } } } diff --git a/Sources/BashInterpreter/FileSystems/BinCatalog.swift b/Sources/BashInterpreter/FileSystems/BinCatalog.swift index 9599044..68aa8e2 100644 --- a/Sources/BashInterpreter/FileSystems/BinCatalog.swift +++ b/Sources/BashInterpreter/FileSystems/BinCatalog.swift @@ -64,7 +64,7 @@ enum BinCatalog { // user-installed). We slot them under /usr/local/bin so // their location matches the convention macOS users expect. for name in [ - "rg", "yq", + "rg", "fd", "yq", // SwiftPorts CLI surface — git/gh/glab and the // compression family ship via the BashCommandKit / // SwiftPorts registration, but `which` and `compgen -c` diff --git a/Tests/BashCommandKitTests/Sqlite3CommandTests.swift b/Tests/BashCommandKitTests/Sqlite3CommandTests.swift index 7632179..df17a82 100644 --- a/Tests/BashCommandKitTests/Sqlite3CommandTests.swift +++ b/Tests/BashCommandKitTests/Sqlite3CommandTests.swift @@ -82,12 +82,17 @@ import Foundation atPath: (dir as NSString).appendingPathComponent("data.db"))) } - @Test func doubleDashVersionGivesUniformBanner() async throws { + @Test func doubleDashVersionMatchesRealSqlite3() async throws { let (cap, dir) = try makeShell(); defer { cleanup(dir) } - // `--version` (double dash) gets SwiftBash's uniform builtin banner, - // matching the ArgumentParser bridge the other commands route through. + // `--version` and `-version` both report the SQLite library version — + // exactly like the real sqlite3 CLI and the standalone SwiftPorts + // `sqlite3` executable. SwiftPorts' shell port owns this behavior; + // SwiftBash adds no `--version` banner of its own, it just installs the + // command. (Previously SwiftBash hijacked `--version` for a uniform + // "(SwiftBash) x.y.z" builtin banner — a deviation from real sqlite3.) try await cap.shell.run("sqlite3 --version") - #expect(cap.stdout == "sqlite3 (SwiftBash) \(SwiftBashVersion.packageVersion)\n") + #expect(cap.stdout.hasSuffix(" (64-bit)\n")) + #expect(!cap.stdout.contains("SwiftBash")) } @Test func singleDashVersionGivesSQLiteVersion() async throws {