Compare commits

..

6 commits

Author SHA1 Message Date
37194f5d29 chore: Use explicit ./artifacts path in release workflow
Some checks failed
CI / Test on Linux (push) Has been cancelled
CI / Test on Linux-1 (push) Has been cancelled
CI / Test on macOS (push) Has been cancelled
CI / Test on macOS-1 (push) Has been cancelled
2025-10-23 20:46:37 +08:00
b84662843b feat: Add workflow to release TailwindCSS CLI artifacts 2025-10-23 20:12:16 +08:00
d637b95450 chore: Add Swift 6.2 to CI matrix and cache packages 2025-10-23 20:01:04 +08:00
da0c9046ae fix: Update TailwindCSSCLI binary target to 1.1.0-test.4 2025-10-05 17:44:50 +08:00
caa5c6651b fix: expand triple for bundle info.json too. 2025-10-05 17:37:39 +08:00
b4f1cf9dbd refactor: Update TailwindCSSCLI binary target and refactor builder 2025-10-05 17:25:53 +08:00
8 changed files with 263 additions and 104 deletions

View file

@ -12,13 +12,22 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
swift-version: ["6.1"] swift-version: ["6.1", "6.2"]
container: container:
image: swift:${{ matrix.swift-version }} image: swift:${{ matrix.swift-version }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Cache Swift packages
uses: actions/cache@v4
with:
path: .build
key: ${{ runner.os }}-spm-${{ matrix.swift-version }}-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-${{ matrix.swift-version }}-
${{ runner.os }}-spm-
- name: Build package - name: Build package
run: swift build --verbose run: swift build --verbose
@ -30,7 +39,7 @@ jobs:
runs-on: macos-latest runs-on: macos-latest
strategy: strategy:
matrix: matrix:
swift-version: ["6.1"] swift-version: ["6.1", "6.2"]
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -40,6 +49,15 @@ jobs:
with: with:
swift-version: ${{ matrix.swift-version }} swift-version: ${{ matrix.swift-version }}
- name: Cache Swift packages
uses: actions/cache@v4
with:
path: .build
key: ${{ runner.os }}-spm-${{ matrix.swift-version }}-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-${{ matrix.swift-version }}-
${{ runner.os }}-spm-
- name: Build package - name: Build package
run: swift build --verbose run: swift build --verbose

View file

@ -0,0 +1,104 @@
name: Release TailwindCSS CLI Artifacts
on:
workflow_dispatch:
inputs:
tailwind_version:
description: "TailwindCSS release version (e.g., v4.1.14)"
required: true
type: string
jobs:
build-and-release:
name: Build and Release Artifacts
runs-on: macos-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Swift
uses: swift-actions/setup-swift@v2
with:
swift-version: "6.1"
- name: Cache Swift packages
uses: actions/cache@v4
with:
path: .build
key: ${{ runner.os }}-spm-release-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-release-
${{ runner.os }}-spm-
- name: Validate version format
run: |
if [[ ! "${{ inputs.tailwind_version }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Version must be in format vX.Y.Z (e.g., v4.1.14)"
exit 1
fi
echo "Version format is valid: ${{ inputs.tailwind_version }}"
- name: Build artifact bundler
run: |
echo "Building TailwindCSSCLIArtifactBundler..."
swift build -c release --product TailwindCSSCLIArtifactBundler
- name: Create artifact bundles
run: |
echo "Creating artifact bundles for TailwindCSS ${{ inputs.tailwind_version }}..."
mkdir -p ./artifacts
.build/release/TailwindCSSCLIArtifactBundler \
--version "${{ inputs.tailwind_version }}" \
--work-dir "${{ runner.temp }}/tailwindcss-bundles" \
--output-dir ./artifacts
- name: List generated artifacts
run: |
echo "Generated artifacts:"
ls -lh artifacts/
- name: Compute checksum
id: checksum
run: |
CHECKSUM=$(shasum -a 256 artifacts/tailwindcss.artifactbundleindex | awk '{print $1}')
echo "checksum=$CHECKSUM" >> $GITHUB_OUTPUT
echo "Artifact bundle index checksum: $CHECKSUM"
- name: Create release
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
with:
tag_name: TailwindCSSCLI-${{ inputs.tailwind_version }}
name: TailwindCSS CLI ${{ inputs.tailwind_version }}
body: |
## TailwindCSS CLI Artifact Bundles
This release contains Swift Package Manager artifact bundles for TailwindCSS CLI version `${{ inputs.tailwind_version }}`.
### Included Artifacts
- Linux x64 (`x86_64-unknown-linux-gnu`)
- macOS x64 (`x86_64-apple-darwin`)
- macOS ARM64 (`aarch64-apple-darwin`)
### Usage
Add the artifact bundle to your `Package.swift`:
```swift
.binaryTarget(
name: "TailwindCSSCLI",
url: "https://github.com/${{ github.repository }}/releases/download/TailwindCSSCLI@${{ inputs.tailwind_version }}/tailwindcss.artifactbundleindex",
checksum: "${{ steps.checksum.outputs.checksum }}"
)
```
### Checksum
```
${{ steps.checksum.outputs.checksum }}
```
draft: false
prerelease: false
make_latest: false
files: |
artifacts/*.zip
artifacts/tailwindcss.artifactbundleindex

View file

@ -25,8 +25,8 @@ let package = Package(
.binaryTarget( .binaryTarget(
name: "TailwindCSSCLI", name: "TailwindCSSCLI",
url: url:
"https://github.com/laosb/SwiftTailwind/releases/download/1.1.0-test.2+tw.4.1.14/tailwindcss.artifactbundleindex", "https://github.com/laosb/SwiftTailwind/releases/download/1.1.0-test.4+tw.4.1.14/tailwindcss.artifactbundleindex",
checksum: "4462492d557c16c07a3c8c07980eea54e4460d925c1b9a097ad91f1c901440ec" checksum: "ec4df49e361db5fc3159c431e5661af2e1b22d8575c64ce0482a8e84cfd93d04"
), ),
.target( .target(
name: "SwiftTailwindExample", name: "SwiftTailwindExample",

View file

@ -0,0 +1,41 @@
import Foundation
extension ArtifactBundleBuilder {
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))
}
func makeExecutable(path: String) throws {
let attributes = [FileAttributeKey.posixPermissions: 0o755]
try fileManager.setAttributes(attributes, ofItemAtPath: path)
}
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
}
}
}

View file

@ -0,0 +1,46 @@
import Crypto
import Foundation
extension ArtifactBundleBuilder {
/// 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.
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
}
}
}

View file

@ -0,0 +1,29 @@
extension ArtifactBundleBuilder {
/// Expands a list of triples into a stricter list of triples.
///
/// To workaround https://github.com/swiftlang/swift-package-manager/issues/7362.
func expandingTriple(_ triple: String) -> [String] {
switch triple {
case "aarch64-apple-darwin":
[
"aarch64-apple-darwin",
"arm64-apple-macosx12.0",
"arm64-apple-macosx13.0",
"arm64-apple-macosx14.0",
"arm64-apple-macosx15.0",
"arm64-apple-macosx26.0",
]
case "x86_64-apple-darwin":
[
"x86_64-apple-darwin",
"x86_64-apple-macosx12.0",
"x86_64-apple-macosx13.0",
"x86_64-apple-macosx14.0",
"x86_64-apple-macosx15.0",
"x86_64-apple-macosx26.0",
]
// TODO: Does linux need more detailed triple variants?
default: [triple]
}
}
}

View file

@ -1,11 +1,10 @@
import Crypto
import Foundation import Foundation
class ArtifactBundleBuilder { class ArtifactBundleBuilder {
private let version: String private let version: String
private let workDir: String private let workDir: String
private let outputDir: String private let outputDir: String
private let fileManager = FileManager.default let fileManager = FileManager.default
private let binaryConfigurations: [BinaryConfiguration] = [ private let binaryConfigurations: [BinaryConfiguration] = [
BinaryConfiguration(binaryName: "tailwindcss-linux-x64", triple: "x86_64-unknown-linux-gnu"), BinaryConfiguration(binaryName: "tailwindcss-linux-x64", triple: "x86_64-unknown-linux-gnu"),
@ -106,26 +105,12 @@ class ArtifactBundleBuilder {
return BundleInfo(fileName: zipFileName, checksum: checksum, triple: config.triple) 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 { private func createInfoJSON(bundleDir: String, binaryPath: String, triple: String) throws {
let artifact = Artifact( let artifact = Artifact(
version: version, version: version,
type: "executable", type: "executable",
variants: [ variants: [
ArtifactVariant(path: binaryPath, supportedTriples: [triple]) ArtifactVariant(path: binaryPath, supportedTriples: expandingTriple(triple))
] ]
) )
@ -142,71 +127,6 @@ class ArtifactBundleBuilder {
try jsonData.write(to: URL(fileURLWithPath: infoPath)) 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 { private func generateArtifactBundleIndex(bundleInfos: [BundleInfo]) throws {
print("Generating tailwindcss.artifactbundleindex...") print("Generating tailwindcss.artifactbundleindex...")
@ -219,7 +139,7 @@ class ArtifactBundleBuilder {
Bundle( Bundle(
fileName: bundleInfo.fileName, fileName: bundleInfo.fileName,
checksum: bundleInfo.checksum, checksum: bundleInfo.checksum,
supportedTriples: [bundleInfo.triple] supportedTriples: expandingTriple(bundleInfo.triple)
) )
} }
@ -236,20 +156,3 @@ class ArtifactBundleBuilder {
try jsonData.write(to: URL(fileURLWithPath: indexPath)) 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"
}
}
}

View file

@ -0,0 +1,18 @@
import Foundation
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"
}
}
}