Skip to content
Merged
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
46 changes: 16 additions & 30 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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] = [
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()`.
Expand Down
72 changes: 15 additions & 57 deletions Sources/BashCommandKit/API/Shell+Sqlite3.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
167 changes: 28 additions & 139 deletions Sources/BashCommandKit/API/Shell+SwiftPortsCommands.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/BashInterpreter/FileSystems/BinCatalog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
13 changes: 9 additions & 4 deletions Tests/BashCommandKitTests/Sqlite3CommandTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down