diff --git a/.github/workflows/build-matrix.yaml b/.github/workflows/build-matrix.yaml
index e70a557..7c464ee 100644
--- a/.github/workflows/build-matrix.yaml
+++ b/.github/workflows/build-matrix.yaml
@@ -76,3 +76,155 @@ jobs:
with:
name: skillet-checksums
path: dist/SHA256SUMS
+
+ qemu-smoke:
+ needs: build
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
+
+ - name: Setup QEMU
+ uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e8
+
+ - name: Download build artifacts
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
+ with:
+ path: dist
+ merge-multiple: true
+
+ - name: Make Linux artifacts executable
+ shell: bash
+ run: |
+ set -euo pipefail
+ chmod +x dist/skillet-linux-*
+
+ - name: Smoke test arm64 musl artifact under emulation
+ shell: bash
+ run: |
+ set -euo pipefail
+ OUTPUT=$(docker run --rm --platform linux/arm64/v8 -v "$PWD/dist:/artifacts:ro" alpine:3.22 /artifacts/skillet-linux-arm64-musl --version)
+ printf '%s\n' "$OUTPUT"
+ grep -F "sklt/" <<< "$OUTPUT"
+
+ - name: Smoke test arm64 gnu artifact under emulation
+ shell: bash
+ run: |
+ set -euo pipefail
+ OUTPUT=$(docker run --rm --platform linux/arm64/v8 -v "$PWD/dist:/artifacts:ro" ubuntu:24.04 /artifacts/skillet-linux-arm64-gnu --version)
+ printf '%s\n' "$OUTPUT"
+ grep -F "sklt/" <<< "$OUTPUT"
+
+ wine-smoke:
+ needs: build
+ runs-on: ubuntu-latest
+
+ env:
+ WINEDEBUG: -all
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
+
+ - name: Setup mise
+ uses: jdx/mise-action@c37c93293d6b742fc901e1406b8f764f6fb19dac
+
+ - name: Install Wine
+ shell: bash
+ run: |
+ set -euo pipefail
+ sudo apt-get update
+ sudo apt-get install -y wine64
+
+ - name: Download build artifacts
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
+ with:
+ path: dist
+ merge-multiple: true
+
+ - name: Smoke test Windows artifact with Wine
+ run: bun scripts/smoke-artifact.ts --target=windows-x64 --artifact-dir=dist --runner=wine64
+
+ docker-integration:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
+
+ - name: Setup mise
+ uses: jdx/mise-action@c37c93293d6b742fc901e1406b8f764f6fb19dac
+
+ - name: Setup QEMU
+ uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e8
+
+ - name: Build musl binaries
+ run: mise run build -- --targets=linux-x64-musl,linux-arm64-musl
+
+ - name: Smoke test amd64 Docker image
+ shell: bash
+ run: |
+ set -euo pipefail
+ docker buildx create --name skillet-smoke-builder --use || docker buildx use skillet-smoke-builder
+ docker buildx build --platform linux/amd64 --load -t skillet:amd64 .
+ docker run --rm --platform linux/amd64 skillet:amd64 --help
+
+ - name: Smoke test arm64 Docker image
+ shell: bash
+ run: |
+ set -euo pipefail
+ docker buildx use skillet-smoke-builder
+ docker buildx build --platform linux/arm64 --load -t skillet:arm64 .
+ docker run --rm --platform linux/arm64 skillet:arm64 --help
+
+ packaging-validate:
+ needs: checksums
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
+
+ - name: Setup mise
+ uses: jdx/mise-action@c37c93293d6b742fc901e1406b8f764f6fb19dac
+
+ - name: Download checksum artifact
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
+ with:
+ name: skillet-checksums
+ path: dist
+
+ - name: Validate rendered packaging assets
+ run: mise run package-validate
+
+ - name: Validate Homebrew formula syntax
+ run: ruby -c packaging/homebrew/skillet.rb
+
+ windows-packaging-validate:
+ runs-on: windows-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
+
+ - name: Pack Chocolatey package
+ shell: pwsh
+ run: |
+ New-Item -ItemType Directory -Force -Path dist/choco | Out-Null
+ choco pack packaging/chocolatey/skillet.nuspec --outputdirectory dist/choco
+
+ - name: Validate Chocolatey PowerShell scripts
+ shell: pwsh
+ run: |
+ [scriptblock]::Create((Get-Content -Raw packaging/chocolatey/tools/chocolateyinstall.ps1)) | Out-Null
+ [scriptblock]::Create((Get-Content -Raw packaging/chocolatey/tools/chocolateyuninstall.ps1)) | Out-Null
+
+ - name: Validate winget manifests parse as YAML
+ shell: pwsh
+ run: |
+ $package = Get-Content package.json -Raw | ConvertFrom-Json
+ $wingetDir = Join-Path packaging/winget $package.version
+ Get-Content (Join-Path $wingetDir 'echohello-dev.skillet.yaml') -Raw | ConvertFrom-Yaml | Out-Null
+ Get-Content (Join-Path $wingetDir 'echohello-dev.skillet.installer.yaml') -Raw | ConvertFrom-Yaml | Out-Null
+ Get-Content (Join-Path $wingetDir 'echohello-dev.skillet.locale.en-US.yaml') -Raw | ConvertFrom-Yaml | Out-Null
diff --git a/.github/workflows/release-binaries.yaml b/.github/workflows/release-binaries.yaml
new file mode 100644
index 0000000..20f6fe4
--- /dev/null
+++ b/.github/workflows/release-binaries.yaml
@@ -0,0 +1,179 @@
+name: Release Binaries
+
+on:
+ release:
+ types:
+ - published
+ workflow_dispatch:
+ inputs:
+ ref:
+ description: Git ref to release, usually a tag like v1.0.0
+ required: true
+ type: string
+
+permissions:
+ contents: write
+
+concurrency:
+ group: release-binaries-${{ github.event_name == 'release' && github.event.release.tag_name || inputs.ref }}
+ cancel-in-progress: false
+
+jobs:
+ build:
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - os: ubuntu-latest
+ targets: linux-x64-gnu,linux-x64-musl,linux-arm64-gnu,linux-arm64-musl
+ - os: macos-latest
+ targets: darwin-arm64,darwin-x64
+ - os: windows-latest
+ targets: windows-x64
+
+ runs-on: ${{ matrix.os }}
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
+ with:
+ ref: ${{ github.event_name == 'release' && github.event.release.tag_name || inputs.ref }}
+
+ - name: Setup mise
+ uses: jdx/mise-action@c37c93293d6b742fc901e1406b8f764f6fb19dac
+
+ - name: Build targets
+ run: mise run build -- --targets=${{ matrix.targets }}
+
+ - name: Smoke test host artifact
+ run: bun scripts/smoke-artifact.ts
+
+ - name: Upload build artifacts
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
+ with:
+ name: release-${{ matrix.os }}
+ path: dist/*
+
+ upload:
+ needs: build
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
+ with:
+ ref: ${{ github.event_name == 'release' && github.event.release.tag_name || inputs.ref }}
+
+ - name: Setup mise
+ uses: jdx/mise-action@c37c93293d6b742fc901e1406b8f764f6fb19dac
+
+ - name: Download build artifacts
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
+ with:
+ path: .artifacts
+ merge-multiple: true
+
+ - name: Assemble release assets
+ shell: bash
+ run: |
+ set -euo pipefail
+ mkdir -p dist
+ cp .artifacts/skillet-* dist/ || true
+ cp .artifacts/*.exe dist/ || true
+ bun scripts/write-checksums.ts
+
+ - name: Upload release assets
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ RELEASE_TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || inputs.ref }}
+ shell: bash
+ run: |
+ set -euo pipefail
+ gh release upload "$RELEASE_TAG" dist/skillet-* dist/*.exe dist/SHA256SUMS --clobber
+
+ verify-homebrew:
+ needs: upload
+ runs-on: macos-latest
+ env:
+ HOMEBREW_NO_AUTO_UPDATE: 1
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
+ with:
+ ref: ${{ github.event_name == 'release' && github.event.release.tag_name || inputs.ref }}
+
+ - name: Install from Homebrew formula
+ run: brew install --formula ./packaging/homebrew/skillet.rb
+
+ - name: Verify Homebrew install
+ run: skillet --version
+
+ - name: Uninstall Homebrew formula
+ if: always()
+ run: brew uninstall skillet
+
+ verify-chocolatey:
+ needs: upload
+ runs-on: windows-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
+ with:
+ ref: ${{ github.event_name == 'release' && github.event.release.tag_name || inputs.ref }}
+
+ - name: Pack Chocolatey package
+ shell: pwsh
+ run: |
+ New-Item -ItemType Directory -Force -Path dist/choco | Out-Null
+ choco pack packaging/chocolatey/skillet.nuspec --outputdirectory dist/choco
+
+ - name: Install from Chocolatey package
+ shell: pwsh
+ run: |
+ choco install skillet --source "$PWD\dist\choco" --yes --no-progress
+
+ - name: Verify Chocolatey install
+ shell: pwsh
+ run: skillet --version
+
+ - name: Uninstall Chocolatey package
+ if: always()
+ shell: pwsh
+ run: |
+ choco uninstall skillet --yes --no-progress
+
+ verify-winget:
+ needs: upload
+ runs-on: windows-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
+ with:
+ ref: ${{ github.event_name == 'release' && github.event.release.tag_name || inputs.ref }}
+
+ - name: Validate winget manifests
+ shell: pwsh
+ run: |
+ $package = Get-Content package.json -Raw | ConvertFrom-Json
+ $manifest = Join-Path (Join-Path packaging/winget $package.version) 'echohello-dev.skillet.yaml'
+ winget validate $manifest
+
+ - name: Install from winget manifest
+ shell: pwsh
+ run: |
+ $package = Get-Content package.json -Raw | ConvertFrom-Json
+ $manifest = Join-Path (Join-Path packaging/winget $package.version) 'echohello-dev.skillet.yaml'
+ winget install --manifest $manifest --accept-source-agreements --accept-package-agreements --disable-interactivity
+
+ - name: Verify winget install
+ shell: pwsh
+ run: skillet --version
+
+ - name: Uninstall winget package
+ if: always()
+ shell: pwsh
+ run: |
+ winget uninstall --id echohello-dev.skillet --exact --disable-interactivity || exit 0
diff --git a/.mise.toml b/.mise.toml
index 2fcdb13..7f8c01e 100644
--- a/.mise.toml
+++ b/.mise.toml
@@ -31,6 +31,9 @@ run = "mise run build -- --targets=linux-x64-musl,linux-arm64-musl && docker bui
[tasks.build-npm]
run = "mise run install && bun scripts/build-npm-cli.ts"
+[tasks.package-validate]
+run = "mise run install && bun scripts/validate-packaging.ts"
+
[tasks.npm-smoke]
run = "mise run build-npm && npm pack >/tmp/skillet-npm-pack.log && PACKAGE=$(tail -n 1 /tmp/skillet-npm-pack.log) && npx --yes --package \"./$PACKAGE\" sklt --help && REPO_DIR=\"$PWD\" && TMP_DIR=$(mktemp -d) && (cd \"$TMP_DIR\" && bun init -y >/dev/null 2>&1 && bun add \"$REPO_DIR/$PACKAGE\" >/dev/null && bunx --bun sklt --help) && rm -rf \"$TMP_DIR\" \"$PACKAGE\""
diff --git a/packaging/winget/0.0.0/echohello-dev.skillet.installer.yaml b/packaging/winget/0.0.0/echohello-dev.skillet.installer.yaml
index 8690aa1..78b1333 100644
--- a/packaging/winget/0.0.0/echohello-dev.skillet.installer.yaml
+++ b/packaging/winget/0.0.0/echohello-dev.skillet.installer.yaml
@@ -2,11 +2,9 @@ PackageIdentifier: echohello-dev.skillet
PackageVersion: 0.0.0
Installers:
- Architecture: x64
- InstallerType: exe
+ InstallerType: portable
InstallerUrl: https://github.com/echohello-dev/skillet/releases/download/v0.0.0/skillet-windows-x64.exe
InstallerSha256: 51CDDEFDE243F0F27E501CA420D5E1D1B9CAD548DC9884EA4052D2915AFEF179
- AppsAndFeaturesEntries:
- - DisplayName: skillet
Commands:
- skillet
ManifestType: installer
diff --git a/scripts/smoke-artifact.ts b/scripts/smoke-artifact.ts
index 98b5ba5..1e307a3 100644
--- a/scripts/smoke-artifact.ts
+++ b/scripts/smoke-artifact.ts
@@ -4,23 +4,31 @@ import { execFileSync } from "node:child_process";
import { RELEASE_TARGETS, getHostBuildTargetId } from "../src/build/targets";
function main(): void {
+ const args = process.argv.slice(2);
const hostId = getHostBuildTargetId(process.platform, process.arch);
- if (!hostId) {
+ const targetId = readArg(args, "--target") ?? hostId;
+ const artifactDir = path.resolve(readArg(args, "--artifact-dir") ?? "dist");
+ const runner = readArg(args, "--runner");
+
+ if (!targetId) {
console.log(`No smoke target mapping for ${process.platform}/${process.arch}`);
return;
}
- const hostTarget = RELEASE_TARGETS.find((target) => target.id === hostId);
- if (!hostTarget) {
- throw new Error(`Host target not found in RELEASE_TARGETS: ${hostId}`);
+ const target = RELEASE_TARGETS.find((candidate) => candidate.id === targetId);
+ if (!target) {
+ throw new Error(`Target not found in RELEASE_TARGETS: ${targetId}`);
}
- const artifactPath = path.join(process.cwd(), "dist", hostTarget.artifactName);
+ const artifactPath = path.join(artifactDir, target.artifactName);
if (!fs.existsSync(artifactPath)) {
throw new Error(`Host artifact missing: ${artifactPath}`);
}
- const output = execFileSync(artifactPath, ["--version"], {
+ const command = runner ? runner : artifactPath;
+ const commandArgs = runner ? [artifactPath, "--version"] : ["--version"];
+
+ const output = execFileSync(command, commandArgs, {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
});
@@ -29,7 +37,31 @@ function main(): void {
throw new Error(`Unexpected smoke output: ${output}`);
}
- console.log(`Smoke test passed for ${hostId}: ${output.trim()}`);
+ console.log(`Smoke test passed for ${targetId}: ${output.trim()}`);
+}
+
+function readArg(args: string[], name: string): string | undefined {
+ for (let i = 0; i < args.length; i += 1) {
+ const token = args[i];
+ if (token === name) {
+ const next = args[i + 1];
+ if (!next || next.startsWith("--")) {
+ throw new Error(`Missing value for ${name}`);
+ }
+ return next;
+ }
+
+ const prefix = `${name}=`;
+ if (token.startsWith(prefix)) {
+ const value = token.slice(prefix.length);
+ if (value.length === 0) {
+ throw new Error(`Missing value for ${name}`);
+ }
+ return value;
+ }
+ }
+
+ return undefined;
}
main();
diff --git a/scripts/validate-packaging.ts b/scripts/validate-packaging.ts
new file mode 100644
index 0000000..58f6125
--- /dev/null
+++ b/scripts/validate-packaging.ts
@@ -0,0 +1,138 @@
+import fs from "node:fs";
+import path from "node:path";
+import { parse } from "yaml";
+import { parseSha256Sums } from "../src/distribution/checksums";
+import { renderChocolateyPackageFiles } from "../src/distribution/chocolatey";
+import { renderHomebrewFormula } from "../src/distribution/homebrew";
+import { renderWingetManifestFiles } from "../src/distribution/winget";
+
+function main(): void {
+ const args = process.argv.slice(2);
+ const version = readArg(args, "--version") ?? readVersionFromPackageJson();
+ const checksumsPath = path.resolve(readArg(args, "--checksums") ?? "dist/SHA256SUMS");
+ const packagingRoot = path.resolve(readArg(args, "--packaging-root") ?? "packaging");
+ const releaseUrlBase =
+ readArg(args, "--release-url-base") ??
+ `https://github.com/echohello-dev/skillet/releases/download/v${version}`;
+
+ if (!fs.existsSync(checksumsPath)) {
+ throw new Error(`Checksums file not found: ${checksumsPath}`);
+ }
+
+ const checksums = parseSha256Sums(fs.readFileSync(checksumsPath, "utf8"));
+ const expectedHomebrew = renderHomebrewFormula({
+ version,
+ releaseUrlBase,
+ checksumsByArtifact: checksums,
+ });
+ const expectedChocolatey = renderChocolateyPackageFiles({
+ version,
+ releaseUrlBase,
+ checksumsByArtifact: checksums,
+ });
+ const expectedWinget = renderWingetManifestFiles({
+ version,
+ releaseUrlBase,
+ checksumsByArtifact: checksums,
+ });
+
+ const wingetDir = path.join(packagingRoot, "winget", version);
+ const wingetVersionPath = path.join(wingetDir, "echohello-dev.skillet.yaml");
+ const wingetInstallerPath = path.join(wingetDir, "echohello-dev.skillet.installer.yaml");
+ const wingetLocalePath = path.join(wingetDir, "echohello-dev.skillet.locale.en-US.yaml");
+
+ assertContains(expectedHomebrew, `version \"${version}\"`, "generated Homebrew formula");
+ assertContains(expectedChocolatey.nuspec, `${version}`, "generated Chocolatey nuspec");
+ assertContains(expectedChocolatey.installScript, releaseUrlBase, "generated Chocolatey install script");
+ assertContains(expectedWinget.installerManifest, releaseUrlBase, "generated winget installer manifest");
+
+ const versionManifest = parse(expectedWinget.versionManifest) as Record;
+ const installerManifest = parse(expectedWinget.installerManifest) as Record;
+ const localeManifest = parse(expectedWinget.localeManifest) as Record;
+
+ assertField(versionManifest, "PackageVersion", version, "generated winget version manifest");
+ assertField(versionManifest, "ManifestType", "version", "generated winget version manifest");
+ assertField(installerManifest, "PackageVersion", version, "generated winget installer manifest");
+ assertField(installerManifest, "ManifestType", "installer", "generated winget installer manifest");
+ assertField(localeManifest, "PackageVersion", version, "generated winget locale manifest");
+ assertField(localeManifest, "ManifestType", "defaultLocale", "generated winget locale manifest");
+
+ assertCheckedInPackagingFilesExist(packagingRoot, version);
+ assertField(parse(fs.readFileSync(wingetVersionPath, "utf8")) as Record, "ManifestType", "version", wingetVersionPath);
+ assertField(parse(fs.readFileSync(wingetInstallerPath, "utf8")) as Record, "ManifestType", "installer", wingetInstallerPath);
+ assertField(parse(fs.readFileSync(wingetLocalePath, "utf8")) as Record, "ManifestType", "defaultLocale", wingetLocalePath);
+
+ console.log(`Validated packaging assets for ${version}`);
+}
+
+function assertCheckedInPackagingFilesExist(packagingRoot: string, version: string): void {
+ const requiredPaths = [
+ path.join(packagingRoot, "homebrew", "skillet.rb"),
+ path.join(packagingRoot, "chocolatey", "skillet.nuspec"),
+ path.join(packagingRoot, "chocolatey", "tools", "chocolateyinstall.ps1"),
+ path.join(packagingRoot, "chocolatey", "tools", "chocolateyuninstall.ps1"),
+ path.join(packagingRoot, "winget", version, "echohello-dev.skillet.yaml"),
+ path.join(packagingRoot, "winget", version, "echohello-dev.skillet.installer.yaml"),
+ path.join(packagingRoot, "winget", version, "echohello-dev.skillet.locale.en-US.yaml"),
+ ];
+
+ for (const filePath of requiredPaths) {
+ if (!fs.existsSync(filePath)) {
+ throw new Error(`Packaging file not found: ${filePath}`);
+ }
+ }
+}
+
+function assertContains(contents: string, expectedFragment: string, label: string): void {
+ if (!contents.includes(expectedFragment)) {
+ throw new Error(`Missing expected content in ${label}: ${expectedFragment}`);
+ }
+}
+
+function assertField(
+ document: Record,
+ field: string,
+ expectedValue: string,
+ filePath: string,
+): void {
+ const actualValue = document[field];
+ if (actualValue !== expectedValue) {
+ throw new Error(`Unexpected ${field} in ${filePath}: ${String(actualValue)}`);
+ }
+}
+
+function readArg(args: string[], name: string): string | undefined {
+ for (let i = 0; i < args.length; i += 1) {
+ const token = args[i];
+ if (token === name) {
+ const next = args[i + 1];
+ if (!next || next.startsWith("--")) {
+ throw new Error(`Missing value for ${name}`);
+ }
+ return next;
+ }
+
+ const prefix = `${name}=`;
+ if (token.startsWith(prefix)) {
+ const value = token.slice(prefix.length);
+ if (value.length === 0) {
+ throw new Error(`Missing value for ${name}`);
+ }
+ return value;
+ }
+ }
+
+ return undefined;
+}
+
+function readVersionFromPackageJson(): string {
+ const packageJsonPath = path.resolve("package.json");
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as { version?: string };
+ if (!packageJson.version || packageJson.version.length === 0) {
+ throw new Error("package.json version is missing");
+ }
+
+ return packageJson.version;
+}
+
+main();
diff --git a/src/distribution/winget.ts b/src/distribution/winget.ts
index fc6b93f..61425d1 100644
--- a/src/distribution/winget.ts
+++ b/src/distribution/winget.ts
@@ -29,11 +29,9 @@ ManifestVersion: 1.6.0
PackageVersion: ${options.version}
Installers:
- Architecture: x64
- InstallerType: exe
+ InstallerType: portable
InstallerUrl: ${installerUrl}
InstallerSha256: ${installerSha256}
- AppsAndFeaturesEntries:
- - DisplayName: skillet
Commands:
- skillet
ManifestType: installer
diff --git a/tests/distribution/winget.test.ts b/tests/distribution/winget.test.ts
index 2e68fba..f20e7d4 100644
--- a/tests/distribution/winget.test.ts
+++ b/tests/distribution/winget.test.ts
@@ -16,6 +16,7 @@ describe("renderWingetManifestFiles", () => {
expect(files.installerManifest).toContain(
"InstallerUrl: https://github.com/echohello-dev/skillet/releases/download/v1.2.3/skillet-windows-x64.exe",
);
+ expect(files.installerManifest).toContain("InstallerType: portable");
expect(files.installerManifest).toContain(
"InstallerSha256: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
);