diff --git a/.gitignore b/.gitignore index 47bd4c1..e86b8c2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ /Packages xcuserdata/ DerivedData/ -.swiftpm/configuration/registries.json -.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.swiftpm/ .netrc Package.resolved diff --git a/Package.swift b/Package.swift index 8c083aa..63f4947 100644 --- a/Package.swift +++ b/Package.swift @@ -3,13 +3,24 @@ import PackageDescription let package = Package( name: "darwin-apk", + platforms: [ + .macOS(.v13), + ], dependencies: [ .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), + .package(url: "https://github.com/tsolomko/SWCompression", from: "4.8.6"), ], targets: [ + .target( + name: "darwin-apk", + dependencies: [ + .product(name: "SWCompression", package: "SWCompression"), + ], + path: "Sources/apk"), .executableTarget( name: "dpk", dependencies: [ + "darwin-apk", .product(name: "ArgumentParser", package: "swift-argument-parser"), ], path: "Sources/dpk-cli" diff --git a/Sources/apk/Index/ApkIndex.swift b/Sources/apk/Index/ApkIndex.swift new file mode 100644 index 0000000..4ec800a --- /dev/null +++ b/Sources/apk/Index/ApkIndex.swift @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 + +struct ApkIndex { + let packages: [ApkIndexPackage] +} + +extension ApkIndex { + func first(name: String) -> ApkIndexPackage? { + self.packages.first { + $0.name == name + } + } +} + +extension ApkIndex { + static func merge(_ tables: S) -> Self where S.Element == Self { + Self.init(packages: tables.flatMap(\.packages)) + } + + static func merge(_ tables: Self...) -> ApkIndex { + Self.init(packages: tables.flatMap(\.packages)) + } +} + +extension ApkIndex { + init(raw: ApkRawIndex) throws { + self.packages = try raw.packages.map { + try ApkIndexPackage(raw: $0) + } + } +} diff --git a/Sources/apk/Index/ApkIndexDownloader.swift b/Sources/apk/Index/ApkIndexDownloader.swift new file mode 100644 index 0000000..b08139c --- /dev/null +++ b/Sources/apk/Index/ApkIndexDownloader.swift @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +struct ApkIndexDownloader { + func downloadFile(remote remoteURL: URL, destination destLocalURL: URL) { + let sem = DispatchSemaphore.init(value: 0) + let downloadTask = URLSession.shared.downloadTask(with: remoteURL) { url, response, error in + if let localURL = url { + do { + // Replace existing APKINDEX.tar.gz files + if FileManager.default.fileExists(atPath: destLocalURL.path()) { + try FileManager.default.removeItem(at: destLocalURL) + } + // Move temporary to the new location + try FileManager.default.moveItem(at: localURL, to: destLocalURL) + } catch { + print("Download error: \(error.localizedDescription)") + } + } + sem.signal() + } + downloadTask.resume() + sem.wait() + } +} diff --git a/Sources/apk/Index/ApkIndexPackage.swift b/Sources/apk/Index/ApkIndexPackage.swift new file mode 100644 index 0000000..98892c4 --- /dev/null +++ b/Sources/apk/Index/ApkIndexPackage.swift @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +struct ApkIndexPackage { + let indexChecksum: String //TODO: Decode cus why not + let name: String + let version: String + let architecture: String? + let packageSize: UInt64 + let installedSize: UInt64 + let packageDescription: String + let url: String + let license: String + let origin: String? + let maintainer: String? + let buildTime: Date? + let commit: String? + let providerPriority: UInt16? + let dependencies: [String] //TODO: stuff + let provides: [String] //TODO: stuff + let installIf: [String] //TODO: stuff + + var downloadFilename: String { "\(self.name)-\(version).apk" } + + //TODO: Implementation + //lazy var semanticVersion: (Int, Int, Int) = (0, 0, 0) +} + +extension ApkIndexPackage { + init(raw rawEntry: ApkRawIndexEntry) throws(Self.ParseError) { + // Required fields + var indexChecksum: String? = nil + var name: String? = nil + var version: String? = nil + var description: String? = nil + var url: String? = nil + var license: String? = nil + var packageSize: UInt64? = nil + var installedSize: UInt64? = nil + + var dependencies = [String]() + var provides = [String]() + var installIf = [String]() + + // Optional fields + var architecture: String? = nil + var origin: String? = nil + var maintainer: String? = nil + var buildTime: Date? = nil + var commit: String? = nil + var providerPriority: UInt16? = nil + + // Read all the raw records for this entry + for record in rawEntry.fields { + switch record.key { + case "P": + name = record.value + case "V": + version = record.value + case "T": + description = record.value + case "U": + url = record.value + case "L": + license = record.value + case "A": + architecture = record.value + case "D": + dependencies = record.value.components(separatedBy: " ") + case "C": + indexChecksum = record.value // base64-encoded SHA1 hash prefixed with "Q1" + case "S": + guard let value = UInt64(record.value, radix: 10) else { + throw .badValue(key: record.key) + } + packageSize = value + case "I": + guard let value = UInt64(record.value, radix: 10) else { + throw .badValue(key: record.key) + } + installedSize = value + case "p": + provides = record.value.components(separatedBy: " ") + case "i": + installIf = record.value.components(separatedBy: " ") + case "o": + origin = record.value + case "m": + maintainer = record.value + case "t": + guard let timet = UInt64(record.value, radix: 10), + let timetInterval = TimeInterval(exactly: timet) else { + throw .badValue(key: record.key) + } + buildTime = Date(timeIntervalSince1970: timetInterval) + case "c": + commit = record.value + case "k": + guard let value = UInt64(record.value, radix: 10), + (0..(or error: @autoclosure () -> E) throws(E) -> Wrapped { + switch self { + case .some(let v): + return v + case .none: + throw error() + } + } +} diff --git a/Sources/apk/Index/ApkIndexUpdate.swift b/Sources/apk/Index/ApkIndexUpdate.swift new file mode 100644 index 0000000..d5cd989 --- /dev/null +++ b/Sources/apk/Index/ApkIndexUpdate.swift @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import SWCompression +import CryptoKit + +public struct ApkIndexUpdater { + var repositories: [String] + var architectures: [String] + + public init() { + self.repositories = [ + "https://dl-cdn.alpinelinux.org/alpine/v3.21/main", + "https://dl-cdn.alpinelinux.org/alpine/edge/community" + ] + // other archs: "armhf", "armv7", "loongarch64", "ppc64le", "riscv64", "s390x", "x86" + self.architectures = [ "aarch64", "x86_64" ] + } + + public func update() { + let repositories = self.repositories.flatMap { repo in + self.architectures.map { arch in + Repository(name: repo, arch: arch) + } + } + + let downloader = ApkIndexDownloader() + for repo in repositories { + let localIndex = URL(filePath: repo.localName) +#if false + let shouldDownload = true +#else + let shouldDownload = !FileManager.default.fileExists(atPath: localIndex.path()) +#endif + if shouldDownload { + print("Fetching index for \"\(repo.name)\"") + downloader.downloadFile(remote: repo.url, destination: localIndex) + } + } + + let index: ApkIndex + do { + let tables = try repositories.map { try readIndex(URL(filePath: $0.localName)) } + index = ApkIndex.merge(tables) + } catch { + fatalError(error.localizedDescription) + } + + for package in index.packages { + print(package) + } + } + + private func readIndex(_ indexURL: URL) throws -> ApkIndex { + let tarSignature: [TarReader.Entry] + let tarRecords: [TarReader.Entry] + + let tars = try GzipArchive.multiUnarchive( // Slow... + archive: Data(contentsOf: indexURL)) + assert(tars.count >= 2) + + var signatureStream = MemoryInputStream(buffer: tars[0].data) + tarSignature = try TarReader.read(&signatureStream) + var recordsStream = MemoryInputStream(buffer: tars[1].data) + tarRecords = try TarReader.read(&recordsStream) + + guard case .file(let signatureName, _) = tarSignature.first + else { fatalError("Missing signature") } + print(signatureName) + guard let apkIndexFile = tarRecords.firstFile(name: "APKINDEX") + else { fatalError("APKINDEX missing") } + guard let description = tarRecords.firstFile(name: "DESCRIPTION") + else { fatalError("DESCRIPTION missing") } + + let reader = TextInputStream(binaryStream: MemoryInputStream(buffer: apkIndexFile)) + return try ApkIndex(raw: + try ApkRawIndex(lines: reader.lines)) + } +} + +extension ApkIndexUpdater { + struct Repository { + let name: String + let arch: String + let discriminator: String + + private static func resolveApkIndex(_ repo: String, _ arch: String) + -> String { "\(repo)/\(arch)/APKINDEX.tar.gz" } + var url: URL { URL(string: Self.resolveApkIndex(self.name, self.arch))! } + var localName: String { "APKINDEX.\(discriminator).tar.gz" } + + init(name repo: String, arch: String) { + self.name = repo + self.arch = arch + + let urlSHA1Digest = Data(Insecure.SHA1.hash(data: Data(Self.resolveApkIndex(repo, arch).utf8))) + self.discriminator = urlSHA1Digest.subdata(in: 0..<3).map { String(format: "%02x", $0) }.joined() + } + } +} diff --git a/Sources/apk/Index/ApkRawIndex.swift b/Sources/apk/Index/ApkRawIndex.swift new file mode 100644 index 0000000..9064699 --- /dev/null +++ b/Sources/apk/Index/ApkRawIndex.swift @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +struct ApkRawIndex { + let packages: [ApkRawIndexEntry] + + init(lines: any Sequence) throws { + var packages = [ApkRawIndexEntry]() + + var recordLines = [String]() + recordLines.reserveCapacity(15) + + for line in lines { + if line.trimmingCharacters(in: .whitespaces).isEmpty { + if !recordLines.isEmpty { + packages.append(try .init(parsingEntryLines: recordLines)) + recordLines.removeAll(keepingCapacity: true) + } + } else { + recordLines.append(line) + } + } + if !recordLines.isEmpty { + packages.append(try .init(parsingEntryLines: recordLines)) + } + + self.packages = packages + } +} + +struct ApkRawIndexEntry { + let fields: [Record] + + struct Record { + let key: Character + let value: String + } +} + +extension ApkRawIndexEntry { + init(parsingEntryLines lines: any Sequence) throws { + self.fields = try lines.map { line in + guard let splitIdx = line.firstIndex(of: ":"), + line.distance(from: line.startIndex, to: splitIdx) == 1 else { + throw ApkRawIndexError.badPair + } + return Record( + key: line.first!, + value: String(line[line.index(after: splitIdx)...])) + } + } + + func toMap() -> [Character: String] { + Dictionary(uniqueKeysWithValues: self.fields.map { $0.pair }) + } + + func lookup(_ key: Character) -> String? { + fields.first(where: { $0.key == key })?.value + } +} + +extension ApkRawIndexEntry.Record { + var pair: (Character, String) { + (self.key, self.value) + } +} + +enum ApkRawIndexError: Error, LocalizedError { + case badPair + + var errorDescription: String? { + switch self { + case .badPair: "Malformed raw key-value pair" + } + } +} diff --git a/Sources/apk/Utility/FileInputStream.swift b/Sources/apk/Utility/FileInputStream.swift new file mode 100644 index 0000000..e3e0d6e --- /dev/null +++ b/Sources/apk/Utility/FileInputStream.swift @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +public struct FileInputStream: InputStream { + private var _hnd: FileHandle + + public init(_ fileURL: URL) throws { + self._hnd = try FileHandle(forReadingFrom: fileURL) + } + + public mutating func seek(_ whence: StreamWhence) throws(StreamError) { + let applyOffset = { (position: UInt64, offset: Int) throws(StreamError) -> UInt64 in + if offset < 0 { + let (newPosition, overflow) = position.subtractingReportingOverflow(UInt64(-offset)) + if overflow { throw .seekRange } + return newPosition + } else { + let (newPosition, overflow) = position.addingReportingOverflow(UInt64(offset)) + if overflow { throw .overflow } + return newPosition + } + } + + switch whence { + case .set(let position): + if position < 0 { throw .seekRange } + do { try self._hnd.seek(toOffset: UInt64(truncatingIfNeeded: position)) } + catch { + throw .fileHandleError(error) + } + case .current(let offset): + do { try self._hnd.seek(toOffset: try applyOffset(try self._hnd.offset(), offset)) } + catch { + if error is StreamError { + throw error as! StreamError + } else { + throw .fileHandleError(error) + } + } + case .end(let offset): + do { try self._hnd.seek(toOffset: applyOffset(try self._hnd.seekToEnd(), offset)) } + catch { + if error is StreamError { + throw error as! StreamError + } else { + throw .fileHandleError(error) + } + } + } + } + + public var tell: Int { + get throws(StreamError) { + let offset: UInt64 + do { offset = try self._hnd.offset() } + catch { + throw .fileHandleError(error) + } + if offset > Int.max { throw .overflow } + return Int(truncatingIfNeeded: offset) + } + } + + public mutating func read(_ count: Int) throws(StreamError) -> Data { + do { + return try self._hnd.read(upToCount: count) ?? Data() + } catch { + throw .fileHandleError(error) + } + } +} diff --git a/Sources/apk/Utility/InputStream.swift b/Sources/apk/Utility/InputStream.swift new file mode 100644 index 0000000..c8ce16c --- /dev/null +++ b/Sources/apk/Utility/InputStream.swift @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +public protocol InputStream: Stream, IteratorProtocol { + associatedtype Element = UInt8 + + mutating func read(_ count: Int) throws(StreamError) -> Data +} + +public extension InputStream { + mutating func read(_ size: Int, items: Int) throws(StreamError) -> Data { + try self.read(size * items) + } +} + +public extension InputStream { + mutating func next() -> UInt8? { + try? self.read(1).first + } +} diff --git a/Sources/apk/Utility/MemoryInputStream.swift b/Sources/apk/Utility/MemoryInputStream.swift new file mode 100644 index 0000000..3c820eb --- /dev/null +++ b/Sources/apk/Utility/MemoryInputStream.swift @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +public struct MemoryInputStream: InputStream { + private var _buf: [UInt8]! = nil + private let _sli: ArraySlice + private let _len: Int + private var _idx = 0 + + public init(buffer: Data) { + self._len = buffer.count + self._buf = [UInt8](repeating: 0, count: self._len) + self._buf.withUnsafeMutableBytes { _ = buffer.copyBytes(to: $0) } + self._sli = self._buf[...] + } + + public init(view: ArraySlice) { + self._sli = view + self._len = view.count + } + + public mutating func seek(_ whence: StreamWhence) throws(StreamError) { + let (position, overflow) = switch whence { + case .set(let position): (position, false) + case .current(let offset): self._idx.addingReportingOverflow(offset) + case .end(let offset): self._len.addingReportingOverflow(offset) + } + if overflow { + throw .overflow + } else if position < 0 { + throw .seekRange + } else { + self._idx = position + } + } + + public var tell: Int { + get throws(StreamError) { + self._idx + } + } + + public mutating func read(_ count: Int) throws(StreamError) -> Data { + let beg = min(self._idx, self._len) + let end = min(self._idx + count, self._len) + let bytes = Data(self._sli[beg.. UInt8? { + if self._idx < self._len { + let byte = self._sli[self._idx] + self._idx += 1 + return byte + } else { + return nil + } + } +} diff --git a/Sources/apk/Utility/Stream.swift b/Sources/apk/Utility/Stream.swift new file mode 100644 index 0000000..52ffd75 --- /dev/null +++ b/Sources/apk/Utility/Stream.swift @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +public protocol Stream { + mutating func seek(_ whence: StreamWhence) throws(StreamError) + var tell: Int { get throws(StreamError) } +} + +public enum StreamWhence { + case set(_ position: Int) + case current(_ offset: Int) + case end(_ offset: Int) +} + +public enum StreamError: Error, LocalizedError { + case unsupported + case seekRange + case overflow + case fileHandleError(_ error: any Error) + + public var errorDescription: String? { + switch self { + case .unsupported: "Unsupported operation" + case .seekRange: "Seek out of range" + case .overflow: "Stream position overflowed" + case .fileHandleError(let error): "Error from file handle: \(error.localizedDescription)" + } + } +} diff --git a/Sources/apk/Utility/TarMemoryReader.swift b/Sources/apk/Utility/TarMemoryReader.swift new file mode 100644 index 0000000..88e00a8 --- /dev/null +++ b/Sources/apk/Utility/TarMemoryReader.swift @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +public struct TarReader { + private static let tarBlockSize = 512 + private static let tarTypeOffset = 156 + private static let tarNameOffset = 0 + private static let tarNameSize = 100 + private static let tarSizeOffset = 124 + private static let tarSizeSize = 12 + + public enum Entry { + case file(name: String, data: Data) + case directory(name: String) + } + + public static func read(_ stream: inout S) throws -> [Entry] { + var entries = [Entry]() + + while true { + let tarBlock = try stream.read(Self.tarBlockSize) + if tarBlock.isEmpty { break } + if tarBlock.count < Self.tarBlockSize { throw TarError.unexpectedEndOfStream } + + let type = UnicodeScalar(tarBlock[Self.tarTypeOffset]) + switch type { + case "0": // Regular file + // Read metadata + let name = try Self.readName(tarBlock) + let size = try Self.readSize(tarBlock) + + // Read file data + var data = Data() + var bytesRemaining = size, readAmount = 0 + while bytesRemaining > 0 { + //FIXME: just read the whole thing at once tbh + readAmount = min(bytesRemaining, Self.tarBlockSize) + let block = try stream.read(readAmount) + if block.count < readAmount { throw TarError.unexpectedEndOfStream } + data += block + bytesRemaining -= readAmount + } + entries.append(.file(name: name, data: data)) + + // Seek to next block + let seekAmount = Self.tarBlockSize - readAmount + if seekAmount > 0 { + try stream.seek(.current(seekAmount)) + } + case "5": + // Directory + let name = try Self.readName(tarBlock) + entries.append(.directory(name: name)) + case "\0": + // Null block, might also be a legacy regular file + break + case "x": + // Extended header block + try stream.seek(.current(Self.tarBlockSize)) + // Symlink, Reserved, Character, Block, FIFO, Reserved, Global, ignore all these + case "1", "2", "3", "4", "6", "7", "g": + let size = try self.readSize(tarBlock) + let blockCount = (size - 1) / Self.tarBlockSize + 1 // Compute blocks to skip + try stream.seek(.current(Self.tarBlockSize * blockCount)) + default: throw TarError.invalidType(type: type) // Not a TAR type + } + } + return entries + } + + private static func readName(_ tar: Data, offset: Int = Self.tarNameOffset) throws (TarError) -> String { + var nameSize = Self.tarNameSize + for i in 0...Self.tarNameSize { + if tar[offset + i] == 0 { + nameSize = i + break + } + } + let data = tar.subdata(in: offset.. Int { + let sizeData = tar.subdata(in: offset.. { + func firstFile(name firstNameMatch: String) -> Data? { + for entry in self { + if case .file(let name, let data) = entry { + if name == firstNameMatch { + return data + } + } + } + return nil + } +} diff --git a/Sources/apk/Utility/TextInputStream.swift b/Sources/apk/Utility/TextInputStream.swift new file mode 100644 index 0000000..cfb386a --- /dev/null +++ b/Sources/apk/Utility/TextInputStream.swift @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Apache-2.0 + +//FIXME: I don't like this, also SLOWWW +struct TextInputStream where InStream.Element == UInt8 { + private var _stream: InStream + + public init(binaryStream: InStream) { + _stream = binaryStream + } + + public var lines: LineSequence { + LineSequence(_stream: self._stream) + } + + public struct LineSequence: Sequence { + public typealias Element = String + + fileprivate var _stream: InStream + + public struct Iterator: IteratorProtocol where InStream.Element == UInt8 { + public typealias Element = String + + fileprivate init(stream: InStream) { + self._stream = stream + } + + private var _stream: InStream + private var _utf8Decoder = UTF8() + private var _scalars = [Unicode.Scalar]() + private var _lastChar: UnicodeScalar = "\0" + private var _eof = false + + private mutating func decodeScalarsLine() { + Decode: while true { + switch self._utf8Decoder.decode(&self._stream) { + case .scalarValue(let value): + if value == "\n" { + if self._lastChar == "\n" { break } + else { break Decode } + } else if value == "\r" { + break Decode + } + self._scalars.append(value) + self._lastChar = value + case .emptyInput: + self._eof = true + break Decode + case .error: + break Decode + //FIXME: repair like the stdlib does + //scalars.append(UTF8.encodedReplacementCharacter) + //lastChar = UTF8.encodedReplacementCharacter + } + } + } + + public mutating func next() -> String? { + // Return early if we already hit the end of the stream + guard !self._eof else { + return nil + } + + // Decode a line of scalars + self.decodeScalarsLine() + defer { + self._scalars.removeAll(keepingCapacity: true) + } + + // Ignore the final empty newline + guard !self._eof || !self._scalars.isEmpty else { + return nil + } + + // Convert to string and return + var string = String() + string.unicodeScalars.append(contentsOf: self._scalars) + return string + } + } + + public func makeIterator() -> Iterator { + Iterator(stream: self._stream) + } + } +} diff --git a/Sources/dpk-cli/CommandLine.swift b/Sources/dpk-cli/CommandLine.swift index f508b80..89e86fd 100644 --- a/Sources/dpk-cli/CommandLine.swift +++ b/Sources/dpk-cli/CommandLine.swift @@ -8,68 +8,9 @@ struct DarwinApkCLI: ParsableCommand { commandName: "dpk", abstract: "Command-line interface for managing packages installed via darwin-apk.", subcommands: [ - Install.self, - Remove.self, - Update.self, - Upgrade.self + DpkInstallCommand.self, + DpkRemoveCommand.self, + DpkUpdateCommand.self, + DpkUpgradeCommand.self ]) } - -extension DarwinApkCLI { - struct Install: ParsableCommand { - static let configuration = CommandConfiguration( - commandName: "add", - abstract: "Install package(s) to the system.", - aliases: [ "install", "i", "a" ]) - - @Argument(help: "One or more package names to install to the system.") - var packages: [String] - - func run() throws { - print("installing \"\(packages.joined(separator: "\", \""))\"") - } - } - - struct Remove: ParsableCommand { - static let configuration = CommandConfiguration( - commandName: "remove", - abstract: "Remove specified package(s) from the system.", - aliases: [ "uninstall", "del", "rem", "r" ]) - - @Argument(help: "One or more package names to uninstall from the system.") - var packages: [String] - - func run() throws { - print("uninstalling \"\(packages.joined(separator: "\", \""))\"") - } - } - - struct Update: ParsableCommand { - static let configuration = CommandConfiguration( - commandName: "update", - abstract: "Update the system package repositories.", - aliases: [ "u" ]) - - func run() throws { - print("updating package repositories") - } - } - - struct Upgrade: ParsableCommand { - static let configuration = CommandConfiguration( - commandName: "upgrade", - abstract: "Upgrade installed packages.", - aliases: [ "U" ]) - - @Argument(help: "Optionally specify packages to upgrade. Otherwise upgrade all packages installed on the system.") - var packages: [String] = [] - - func run() throws { - if packages.isEmpty { - print("upgrading system") - } else { - print("upgrading invidual packages: \"\(packages.joined(separator: "\", \""))\"") - } - } - } -} diff --git a/Sources/dpk-cli/Subcommands/DpkInstallCommand.swift b/Sources/dpk-cli/Subcommands/DpkInstallCommand.swift new file mode 100644 index 0000000..1dc1746 --- /dev/null +++ b/Sources/dpk-cli/Subcommands/DpkInstallCommand.swift @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 + +import ArgumentParser + +struct DpkInstallCommand: ParsableCommand { + static let configuration = CommandConfiguration( + commandName: "add", + abstract: "Install package(s) to the system.", + aliases: [ "a", "install" ]) + + @Argument(help: "One or more package names to install to the system.") + var packages: [String] + + func run() throws { + print("installing \"\(packages.joined(separator: "\", \""))\"") + } +} diff --git a/Sources/dpk-cli/Subcommands/DpkRemoveCommand.swift b/Sources/dpk-cli/Subcommands/DpkRemoveCommand.swift new file mode 100644 index 0000000..750d1e4 --- /dev/null +++ b/Sources/dpk-cli/Subcommands/DpkRemoveCommand.swift @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 + +import ArgumentParser + +struct DpkRemoveCommand: ParsableCommand { + static let configuration = CommandConfiguration( + commandName: "remove", + abstract: "Remove specified package(s) from the system.", + aliases: [ "r", "rem", "del", "uninstall" ]) + + @Argument(help: "One or more package(s) to uninstall from the system.") + var packages: [String] + + func run() throws { + print("uninstalling \"\(packages.joined(separator: "\", \""))\"") + } +} diff --git a/Sources/dpk-cli/Subcommands/DpkUpdateCommand.swift b/Sources/dpk-cli/Subcommands/DpkUpdateCommand.swift new file mode 100644 index 0000000..aaae988 --- /dev/null +++ b/Sources/dpk-cli/Subcommands/DpkUpdateCommand.swift @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import ArgumentParser +import darwin_apk + +struct DpkUpdateCommand: ParsableCommand { + static let configuration = CommandConfiguration( + commandName: "update", + abstract: "Update the system package repositories.", + aliases: [ "u" ]) + + func run() throws { + print("Updating package repositories") + var updater = ApkIndexUpdater() + updater.update() + } +} diff --git a/Sources/dpk-cli/Subcommands/DpkUpgradeCommand.swift b/Sources/dpk-cli/Subcommands/DpkUpgradeCommand.swift new file mode 100644 index 0000000..8a15fd1 --- /dev/null +++ b/Sources/dpk-cli/Subcommands/DpkUpgradeCommand.swift @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 + +import ArgumentParser + +struct DpkUpgradeCommand: ParsableCommand { + static let configuration = CommandConfiguration( + commandName: "upgrade", + abstract: "Upgrade installed packages.", + aliases: [ "U" ]) + + @Argument(help: "Optionally specify packages to upgrade. Otherwise upgrade all packages installed on the system.") + var packages: [String] = [] + + func run() throws { + if packages.isEmpty { + print("upgrading system") + } else { + print("upgrading invidual packages: \"\(packages.joined(separator: "\", \""))\"") + } + } +}