mirror of
https://github.com/laosb/SwiftTailwind.git
synced 2025-11-28 22:01:38 +00:00
refactor: Replace shell script with Swift CLI for artifact bundling
This commit is contained in:
parent
dca866c3a9
commit
5a3c64f599
7 changed files with 386 additions and 107 deletions
|
|
@ -0,0 +1,255 @@
|
|||
import Crypto
|
||||
import Foundation
|
||||
|
||||
class ArtifactBundleBuilder {
|
||||
private let version: String
|
||||
private let workDir: String
|
||||
private let outputDir: String
|
||||
private let fileManager = FileManager.default
|
||||
|
||||
private let binaryConfigurations: [BinaryConfiguration] = [
|
||||
BinaryConfiguration(binaryName: "tailwindcss-linux-x64", triple: "x86_64-unknown-linux-gnu"),
|
||||
BinaryConfiguration(binaryName: "tailwindcss-macos-x64", triple: "x86_64-apple-darwin"),
|
||||
BinaryConfiguration(binaryName: "tailwindcss-macos-arm64", triple: "aarch64-apple-darwin"),
|
||||
]
|
||||
|
||||
init(version: String, workDir: String, outputDir: String) {
|
||||
self.version = version
|
||||
self.workDir = workDir
|
||||
self.outputDir = outputDir
|
||||
}
|
||||
|
||||
func buildArtifactBundles() throws {
|
||||
try setupWorkingDirectory()
|
||||
|
||||
var bundleInfos: [BundleInfo] = []
|
||||
|
||||
print("Creating individual bundles...")
|
||||
for config in binaryConfigurations {
|
||||
let bundleInfo = try createBundle(for: config)
|
||||
bundleInfos.append(bundleInfo)
|
||||
}
|
||||
|
||||
try generateArtifactBundleIndex(bundleInfos: bundleInfos)
|
||||
|
||||
print("=== BUILD COMPLETE ===")
|
||||
print("All bundles created successfully:")
|
||||
print("")
|
||||
print("Generated artifact bundle index: \(outputDir)/tailwindcss.artifactbundleindex")
|
||||
print("")
|
||||
|
||||
let indexChecksum = try computeChecksum(
|
||||
filePath: "\(outputDir)/tailwindcss.artifactbundleindex", usingSHA256Directly: true)
|
||||
print("Index checksum: \(indexChecksum)")
|
||||
print("")
|
||||
|
||||
for bundleInfo in bundleInfos {
|
||||
print("Bundle: \(bundleInfo.fileName)")
|
||||
print(" Checksum: \(bundleInfo.checksum)")
|
||||
print(" Triple: \(bundleInfo.triple)")
|
||||
print("")
|
||||
}
|
||||
}
|
||||
|
||||
private func setupWorkingDirectory() throws {
|
||||
print("Creating working directory: \(workDir)")
|
||||
|
||||
if fileManager.fileExists(atPath: workDir) {
|
||||
try fileManager.removeItem(atPath: workDir)
|
||||
}
|
||||
|
||||
try fileManager.createDirectory(atPath: workDir, withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
private func createBundle(for config: BinaryConfiguration) throws -> BundleInfo {
|
||||
let bundleDirName = "tailwindcss-\(version)-\(config.triple).artifactbundle"
|
||||
let bundleDir = "\(workDir)/\(bundleDirName)"
|
||||
let binaryPath = "bin/tailwindcss"
|
||||
|
||||
print("Creating bundle for \(config.triple)...")
|
||||
|
||||
// Create bundle directory structure
|
||||
let binDir = "\(bundleDir)/bin"
|
||||
try fileManager.createDirectory(atPath: binDir, withIntermediateDirectories: true)
|
||||
|
||||
// Download binary
|
||||
print(" Downloading \(config.binaryName)...")
|
||||
let binaryURL =
|
||||
"https://github.com/tailwindlabs/tailwindcss/releases/download/\(version)/\(config.binaryName)"
|
||||
let binaryDestination = "\(bundleDir)/\(binaryPath)"
|
||||
|
||||
try downloadFile(from: binaryURL, to: binaryDestination)
|
||||
try makeExecutable(path: binaryDestination)
|
||||
print(" ✓ Downloaded and made executable: \(binaryDestination)")
|
||||
|
||||
// Create info.json
|
||||
print(" Creating info.json...")
|
||||
try createInfoJSON(bundleDir: bundleDir, binaryPath: binaryPath, triple: config.triple)
|
||||
print(
|
||||
" ✓ Created info.json with version \(version), path \(binaryPath), triple \(config.triple)")
|
||||
|
||||
// Create ZIP file
|
||||
let zipFileName = "\(bundleDirName).zip"
|
||||
let zipPath = "\(outputDir)/\(zipFileName)"
|
||||
print(" Creating ZIP file: \(zipPath)")
|
||||
|
||||
try createZipFile(bundleDir: bundleDir, zipPath: zipPath)
|
||||
|
||||
// Compute checksum
|
||||
print(" Computing checksum...")
|
||||
let checksum = try computeChecksum(filePath: zipPath)
|
||||
|
||||
print(" ✓ Bundle created: \(zipPath)")
|
||||
print(" ✓ Checksum: \(checksum)")
|
||||
print("")
|
||||
|
||||
return BundleInfo(fileName: zipFileName, checksum: checksum, triple: config.triple)
|
||||
}
|
||||
|
||||
private func downloadFile(from urlString: String, to destination: String) throws {
|
||||
guard let url = URL(string: urlString) else {
|
||||
throw ArtifactBundleError.invalidURL(urlString)
|
||||
}
|
||||
|
||||
let data = try Data(contentsOf: url)
|
||||
try data.write(to: URL(fileURLWithPath: destination))
|
||||
}
|
||||
|
||||
private func makeExecutable(path: String) throws {
|
||||
let attributes = [FileAttributeKey.posixPermissions: 0o755]
|
||||
try fileManager.setAttributes(attributes, ofItemAtPath: path)
|
||||
}
|
||||
|
||||
private func createInfoJSON(bundleDir: String, binaryPath: String, triple: String) throws {
|
||||
let artifact = Artifact(
|
||||
version: version,
|
||||
type: "executable",
|
||||
variants: [
|
||||
ArtifactVariant(path: binaryPath, supportedTriples: [triple])
|
||||
]
|
||||
)
|
||||
|
||||
let info = ArtifactBundleInfo(
|
||||
schemaVersion: "1.0",
|
||||
artifacts: ["tailwindcss": artifact]
|
||||
)
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
||||
|
||||
let jsonData = try encoder.encode(info)
|
||||
let infoPath = "\(bundleDir)/info.json"
|
||||
try jsonData.write(to: URL(fileURLWithPath: infoPath))
|
||||
}
|
||||
|
||||
private func createZipFile(bundleDir: String, zipPath: String) throws {
|
||||
// Remove existing ZIP file if it exists
|
||||
if fileManager.fileExists(atPath: zipPath) {
|
||||
try fileManager.removeItem(atPath: zipPath)
|
||||
}
|
||||
|
||||
let bundleDirURL = URL(fileURLWithPath: bundleDir)
|
||||
let workDirURL = bundleDirURL.deletingLastPathComponent()
|
||||
let bundleName = bundleDirURL.lastPathComponent
|
||||
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/zip")
|
||||
process.arguments = ["-r", zipPath, bundleName]
|
||||
process.currentDirectoryURL = workDirURL
|
||||
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
|
||||
guard process.terminationStatus == 0 else {
|
||||
throw ArtifactBundleError.zipCreationFailed
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the SHA256 checksum of a file.
|
||||
///
|
||||
/// If `usingSHA256Directly` is true, it uses Swift Crypto's SHA256 implementation.
|
||||
/// This is to workaround https://github.com/swiftlang/swift-package-manager/issues/9219.
|
||||
private func computeChecksum(
|
||||
filePath: String,
|
||||
usingSHA256Directly: Bool = false
|
||||
) throws -> String {
|
||||
if usingSHA256Directly {
|
||||
// Use swift-crypto's SHA256 implementation
|
||||
let fileURL = URL(fileURLWithPath: filePath)
|
||||
let data = try Data(contentsOf: fileURL)
|
||||
let hash = SHA256.hash(data: data)
|
||||
return hash.compactMap { String(format: "%02x", $0) }.joined()
|
||||
} else {
|
||||
// Use swift package compute-checksum command
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/swift")
|
||||
process.arguments = ["package", "compute-checksum", filePath]
|
||||
|
||||
let pipe = Pipe()
|
||||
process.standardOutput = pipe
|
||||
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
|
||||
guard process.terminationStatus == 0 else {
|
||||
throw ArtifactBundleError.checksumComputationFailed
|
||||
}
|
||||
|
||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
let output = String(data: data, encoding: .utf8)?.trimmingCharacters(
|
||||
in: .whitespacesAndNewlines)
|
||||
|
||||
guard let checksum = output, !checksum.isEmpty else {
|
||||
throw ArtifactBundleError.checksumComputationFailed
|
||||
}
|
||||
|
||||
return checksum
|
||||
}
|
||||
}
|
||||
|
||||
private func generateArtifactBundleIndex(bundleInfos: [BundleInfo]) throws {
|
||||
print("Generating tailwindcss.artifactbundleindex...")
|
||||
|
||||
// Create output directory if it doesn't exist
|
||||
if !fileManager.fileExists(atPath: outputDir) {
|
||||
try fileManager.createDirectory(atPath: outputDir, withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
let bundles = bundleInfos.map { bundleInfo in
|
||||
Bundle(
|
||||
fileName: bundleInfo.fileName,
|
||||
checksum: bundleInfo.checksum,
|
||||
supportedTriples: [bundleInfo.triple]
|
||||
)
|
||||
}
|
||||
|
||||
let index = ArtifactBundleIndex(
|
||||
schemaVersion: "1.0",
|
||||
bundles: bundles
|
||||
)
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
||||
|
||||
let jsonData = try encoder.encode(index)
|
||||
let indexPath = "\(outputDir)/tailwindcss.artifactbundleindex"
|
||||
try jsonData.write(to: URL(fileURLWithPath: indexPath))
|
||||
}
|
||||
}
|
||||
|
||||
enum ArtifactBundleError: Error, LocalizedError {
|
||||
case invalidURL(String)
|
||||
case zipCreationFailed
|
||||
case checksumComputationFailed
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidURL(let url):
|
||||
return "Invalid URL: \(url)"
|
||||
case .zipCreationFailed:
|
||||
return "Failed to create ZIP file"
|
||||
case .checksumComputationFailed:
|
||||
return "Failed to compute checksum"
|
||||
}
|
||||
}
|
||||
}
|
||||
53
Sources/TailwindCSSCLIArtifactBundler/Models.swift
Normal file
53
Sources/TailwindCSSCLIArtifactBundler/Models.swift
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import Foundation
|
||||
|
||||
// MARK: - Artifact Bundle Info Models
|
||||
|
||||
/// Represents the info.json file structure for an artifact bundle
|
||||
struct ArtifactBundleInfo: Codable {
|
||||
let schemaVersion: String
|
||||
let artifacts: [String: Artifact]
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case schemaVersion, artifacts
|
||||
}
|
||||
}
|
||||
|
||||
struct Artifact: Codable {
|
||||
let version: String
|
||||
let type: String
|
||||
let variants: [ArtifactVariant]
|
||||
}
|
||||
|
||||
struct ArtifactVariant: Codable {
|
||||
let path: String
|
||||
let supportedTriples: [String]
|
||||
}
|
||||
|
||||
// MARK: - Artifact Bundle Index Models
|
||||
|
||||
/// Represents the .artifactbundleindex file structure
|
||||
struct ArtifactBundleIndex: Codable {
|
||||
let schemaVersion: String
|
||||
let bundles: [Bundle]
|
||||
}
|
||||
|
||||
struct Bundle: Codable {
|
||||
let fileName: String
|
||||
let checksum: String
|
||||
let supportedTriples: [String]
|
||||
}
|
||||
|
||||
// MARK: - Internal Data Models
|
||||
|
||||
/// Configuration for a binary platform
|
||||
struct BinaryConfiguration {
|
||||
let binaryName: String
|
||||
let triple: String
|
||||
}
|
||||
|
||||
/// Information about a created bundle
|
||||
struct BundleInfo {
|
||||
let fileName: String
|
||||
let checksum: String
|
||||
let triple: String
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import ArgumentParser
|
||||
import Foundation
|
||||
|
||||
@main
|
||||
struct TailwindCSSCLIArtifactBundler: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: "TailwindCSSCLIArtifactBundler",
|
||||
abstract: "Build TailwindCSS CLI artifact bundles for Swift Package Manager",
|
||||
version: "1.0.0"
|
||||
)
|
||||
|
||||
@Option(name: .shortAndLong, help: "TailwindCSS version to build (e.g., v4.1.14)")
|
||||
var version: String
|
||||
|
||||
@Option(name: .shortAndLong, help: "Working directory for temporary files")
|
||||
var workDir: String = "/tmp/tailwindcss-bundles"
|
||||
|
||||
@Option(name: .shortAndLong, help: "Output directory for the artifact bundle index")
|
||||
var outputDir: String = "."
|
||||
|
||||
func run() throws {
|
||||
print("Building artifact bundles for TailwindCSS version: \(version)")
|
||||
|
||||
let bundler = ArtifactBundleBuilder(
|
||||
version: version,
|
||||
workDir: workDir,
|
||||
outputDir: outputDir
|
||||
)
|
||||
|
||||
try bundler.buildArtifactBundles()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue