diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 674806c..8f6f2b8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -57,9 +57,11 @@ jobs: if: github.ref_name == github.event.repository.default_branch run: ./gradlew publishAllPublicationsToStagingRepository - - name: Add bootstrap.sh to GitHub Pages artifact + - name: Add bootstrap scripts to GitHub Pages artifact if: github.ref_name == github.event.repository.default_branch - run: cp ./scripts/bootstrap.sh ./build/staging-maven-repository/bootstrap.sh + run: | + cp ./scripts/bootstrap.sh ./build/staging-maven-repository/bootstrap.sh + cp ./scripts/bootstrap.ps1 ./build/staging-maven-repository/bootstrap.ps1 - name: Upload GitHub Pages artifact if: github.ref_name == github.event.repository.default_branch diff --git a/.github/workflows/verify-template.yml b/.github/workflows/verify-template.yml index ba7e0eb..b743f2b 100644 --- a/.github/workflows/verify-template.yml +++ b/.github/workflows/verify-template.yml @@ -19,8 +19,8 @@ permissions: packages: read jobs: - verify: - name: Verify template builds + verify-linux: + name: Verify template builds (Linux) runs-on: ubuntu-latest timeout-minutes: 20 @@ -53,25 +53,85 @@ jobs: VENDOR_PASSWORD: ${{ secrets.VENDOR_PASSWORD }} - name: Create project from template - run: ./scripts/bootstrap.sh /tmp/cinpo-verify-project + run: ./scripts/bootstrap.sh "${{ runner.temp }}/cinpo-verify-project" - name: Prepare vendor files - run: cp -a vendor /tmp/cinpo-verify-project/vendor + run: cp -a vendor "${{ runner.temp }}/cinpo-verify-project/vendor" - name: Verify cinpoRun - working-directory: /tmp/cinpo-verify-project + working-directory: ${{ runner.temp }}/cinpo-verify-project run: ./gradlew --no-daemon --refresh-dependencies cinpoRun --args='--debug --test' env: GITHUB_ACTOR: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.GH_PACKAGES_TOKEN }} - name: Build cinpoJar - working-directory: /tmp/cinpo-verify-project + working-directory: ${{ runner.temp }}/cinpo-verify-project run: ./gradlew --no-daemon cinpoJar env: GITHUB_ACTOR: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.GH_PACKAGES_TOKEN }} - name: Verify cinpoJar appliance - working-directory: /tmp/cinpo-verify-project + working-directory: ${{ runner.temp }}/cinpo-verify-project run: java -jar build/libs/cinpo-appliance.jar write --test + + verify-windows: + name: Verify template builds (Windows) + runs-on: windows-latest + timeout-minutes: 30 + defaults: + run: + shell: pwsh + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{ inputs.ref }} + + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: "21" + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Configure GitHub auth for standalone template fetches + run: git config --global url."https://x-access-token:$env:GH_TOKEN@github.com/".insteadOf "https://github.com/" + env: + GH_TOKEN: ${{ secrets.GH_PACKAGES_TOKEN }} + + # No chmod: Windows has no POSIX execute bit, and gradlew.bat needs none. + - name: Extract vendor archives + run: .\scripts\extract_vendor.ps1 + env: + VENDOR_PASSWORD: ${{ secrets.VENDOR_PASSWORD }} + + - name: Create project from template + run: .\scripts\bootstrap.ps1 "${{ runner.temp }}\cinpo-verify-project" + + - name: Prepare vendor files + run: Copy-Item -Recurse -Force vendor "${{ runner.temp }}\cinpo-verify-project\vendor" + + # The backslash Windows path in --profile also exercises the CinpoRunTask + # argument tokenizer, which used to collapse "C:\Users\..." to "C:Users...". + - name: Verify cinpoRun + working-directory: ${{ runner.temp }}\cinpo-verify-project + run: .\gradlew.bat --no-daemon --refresh-dependencies cinpoRun --args="--debug --profile=${{ runner.temp }}\cinpo-verify-project\profile\jcdksim.yaml --test" + env: + GITHUB_ACTOR: ${{ github.actor }} + GITHUB_TOKEN: ${{ secrets.GH_PACKAGES_TOKEN }} + + - name: Build cinpoJar + working-directory: ${{ runner.temp }}\cinpo-verify-project + run: .\gradlew.bat --no-daemon cinpoJar + env: + GITHUB_ACTOR: ${{ github.actor }} + GITHUB_TOKEN: ${{ secrets.GH_PACKAGES_TOKEN }} + + - name: Verify cinpoJar appliance + working-directory: ${{ runner.temp }}\cinpo-verify-project + run: java -jar build\libs\cinpo-appliance.jar write --test diff --git a/README.md b/README.md index 3400b80..1825b57 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,23 @@ If you already have a checkout of this repository, you can run the script locall bash ./scripts/bootstrap.sh my-project ``` +On Windows, use [`scripts/bootstrap.ps1`](scripts/bootstrap.ps1). It is a port of the same +four phases and only requires `git`: + +```powershell +# From a checkout +powershell -ExecutionPolicy Bypass -File .\scripts\bootstrap.ps1 my-project + +# Without cloning +irm https://AokiApp.github.io/cinpo/bootstrap.ps1 -OutFile bootstrap.ps1 +powershell -ExecutionPolicy Bypass -File .\bootstrap.ps1 my-project +``` + +Both scripts accept `--template`/`-Template` and `--name`/`-Name`. The generated project is +identical either way: the PowerShell version checks the template out with `core.autocrlf=false` +and marks `gradlew` executable in the Git index, so a project created on Windows still builds +on Linux. + This creates a standalone starter project that you can customize manually. If you prefer, you can still copy [`template/`](template) yourself. The starter is intentionally small: a single Hello applet plus one provision task that issues and writes a leaf certificate, and one test task that reads it back. ``` diff --git a/build.gradle b/build.gradle index 77c0fab..04b574c 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ plugins { } group = 'app.aoki.cinpo' -version = '0.3.2' +version = '0.3.3' java { toolchain { diff --git a/scripts/bootstrap.ps1 b/scripts/bootstrap.ps1 new file mode 100644 index 0000000..1bb525b --- /dev/null +++ b/scripts/bootstrap.ps1 @@ -0,0 +1,413 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Windows counterpart of bootstrap.sh: creates a standalone CINPO starter project. + +.DESCRIPTION + Fetches the CINPO template with git into a temporary sparse checkout, copies + ./template/ into a new project directory, and prepares it as a + standalone starter project. + + When run from a CINPO Git checkout, the template is fetched from the current + origin/HEAD commit so the generated project matches that checked-out revision. + Otherwise it defaults to https://github.com/AokiApp/cinpo @ main. + + Environment overrides: + CINPO_TEMPLATE_REPO Git repository URL used when auto-detection is unavailable. + CINPO_TEMPLATE_REF Git ref used when auto-detection is unavailable. + +.EXAMPLE + powershell -ExecutionPolicy Bypass -File scripts\bootstrap.ps1 my-project + +.EXAMPLE + powershell -ExecutionPolicy Bypass -File scripts\bootstrap.ps1 -Template probe C:\src\my-project +#> +[CmdletBinding()] +param( + # PowerShell matches parameter names case-insensitively, so -template and + # -name work without aliases. + [string] $Template = 'basic', + + [string] $Name, + + [Parameter(Position = 0)] + [string] $Destination +) + +$ErrorActionPreference = 'Stop' + +$GradleVersion = '9.3.1' +$GradleRepo = 'https://github.com/gradle/gradle' +$DefaultRepo = 'https://github.com/AokiApp/cinpo' +$DefaultRef = 'main' +$TemplateProjectNameLine = "rootProject.name = 'cinpo-template'" + +# -------------------------------------------------------------------------- +# UI helpers +# +# All decorative output goes to the host, so a caller that pipes or redirects +# this script receives only the machine-readable summary written at the end. +# -------------------------------------------------------------------------- + +function Write-Banner { + Write-Host @' + し ん ぽ + _____ _____ __ _ _____ ____ + / ____|__ __| \ | | __ \ / __ \ +| | | | | | \| | |__) | | | | +| |____ _| |_| |\ | | ___/| |__| | + \_____|_____|_| \__|_| \____/ +'@ -ForegroundColor Cyan + Write-Host ' :: project bootstrapper ::' -ForegroundColor DarkGray +} + +function Write-Section([string] $Title) { + Write-Host '' + Write-Host "==> $Title" +} + +function Write-Info([string] $Key, [string] $Value) { + Write-Host (" {0,-14} {1}" -f $Key, $Value) +} + +function Stop-WithError([string] $Message) { + Write-Host "[x] Error: $Message" -ForegroundColor Red + exit 1 +} + +function Show-Usage { + Write-Host '' + Write-Host 'Usage: scripts\bootstrap.ps1 [-Template ] [-Name ] ' -ForegroundColor Cyan + Write-Host '' + Write-Host 'Fetches the CINPO template with git into a temporary sparse checkout, copies' + Write-Host './template/ into a new project directory, and prepares it as a' + Write-Host 'standalone starter project.' + Write-Host '' + Write-Host 'Options:' + Write-Host ' -Template Template to use (default: basic)' + Write-Host ' -Name Project name (default: destination directory basename)' + Write-Host '' + Write-Host 'Environment overrides:' + Write-Host ' CINPO_TEMPLATE_REPO Git repository URL used when auto-detection is unavailable.' + Write-Host ' CINPO_TEMPLATE_REF Git ref used when auto-detection is unavailable.' + Write-Host '' +} + +# Runs a step, hiding its output unless it fails. Mirrors run_step in bootstrap.sh. +function Invoke-Step { + param( + [int] $Number, + [int] $Total, + [string] $Label, + [scriptblock] $Action + ) + + Write-Host ("[..] [{0}/{1}] {2}..." -f $Number, $Total, $Label) -ForegroundColor DarkGray + $logFile = [IO.Path]::GetTempFileName() + try { + & $Action *> $logFile + Write-Host ("[OK] [{0}/{1}] {2}" -f $Number, $Total, $Label) -ForegroundColor Green + } catch { + Write-Host ("[x] [{0}/{1}] {2} (failed)" -f $Number, $Total, $Label) -ForegroundColor Red + $captured = Get-Content -LiteralPath $logFile -Raw -ErrorAction SilentlyContinue + if ($captured) { + Write-Host '--- captured output ---' -ForegroundColor DarkGray + Write-Host $captured + Write-Host '------------------------' -ForegroundColor DarkGray + } + Write-Host "[x] Error: $($_.Exception.Message)" -ForegroundColor Red + exit 1 + } finally { + Remove-Item -LiteralPath $logFile -Force -ErrorAction SilentlyContinue + } +} + +function Invoke-Git { + param([string[]] $Arguments) + + # git writes progress to stderr even on success. Windows PowerShell turns each + # such line into an ErrorRecord, which would terminate the script under + # $ErrorActionPreference = 'Stop', so flatten the streams to plain strings and + # judge success by the exit code alone. + $previousPreference = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { + & git @Arguments 2>&1 | ForEach-Object { "$_" } + } finally { + $ErrorActionPreference = $previousPreference + } + + if ($LASTEXITCODE -ne 0) { + throw "git $($Arguments -join ' ') failed with exit code $LASTEXITCODE" + } +} + +# Writes text as UTF-8 without a BOM and with LF line endings. Gradle wrapper +# properties and settings.gradle are consumed on every platform, so a CRLF or a +# BOM introduced here would follow the generated project to Linux. +function Write-TextFileLf { + param([string] $Path, [string] $Content) + + $normalized = $Content -replace "`r`n", "`n" + [IO.File]::WriteAllText($Path, $normalized, (New-Object Text.UTF8Encoding($false))) +} + +function Get-CheckoutSource { + param([string] $CandidateDirectory) + + if (-not $CandidateDirectory) { return $null } + + $previousPreference = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { + & git -C $CandidateDirectory rev-parse --show-toplevel 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { return $null } + + $repoUrl = & git -C $CandidateDirectory remote get-url origin 2>&1 | ForEach-Object { "$_" } + if ($LASTEXITCODE -ne 0) { return $null } + $ref = & git -C $CandidateDirectory rev-parse HEAD 2>&1 | ForEach-Object { "$_" } + if ($LASTEXITCODE -ne 0) { return $null } + } finally { + $ErrorActionPreference = $previousPreference + } + + if (-not $repoUrl -or -not $ref) { return $null } + return [PSCustomObject]@{ Repo = "$repoUrl".Trim(); Ref = "$ref".Trim() } +} + +function Invoke-SparseCheckout { + param( + [string] $RepoUrl, + [string] $Ref, + [string] $CheckoutDirectory, + [string[]] $SparsePaths + ) + + Invoke-Git @('init', '-q', $CheckoutDirectory) + Invoke-Git @('-C', $CheckoutDirectory, 'remote', 'add', 'origin', $RepoUrl) + Invoke-Git @('-C', $CheckoutDirectory, 'config', 'advice.detachedHead', 'false') + Invoke-Git @('-C', $CheckoutDirectory, 'config', 'core.sparseCheckout', 'true') + # Check out bytes verbatim. With the user's global core.autocrlf=true the POSIX + # gradlew copied into the generated project would otherwise get CRLF endings and + # fail with "bad interpreter: /bin/sh^M" on Linux and in the devcontainer. + Invoke-Git @('-C', $CheckoutDirectory, 'config', 'core.autocrlf', 'false') + Invoke-Git @('-C', $CheckoutDirectory, 'config', 'core.eol', 'lf') + + $infoDir = Join-Path $CheckoutDirectory '.git\info' + New-Item -ItemType Directory -Force -Path $infoDir | Out-Null + Write-TextFileLf -Path (Join-Path $infoDir 'sparse-checkout') -Content (($SparsePaths -join "`n") + "`n") + + Invoke-Git @('-C', $CheckoutDirectory, 'fetch', '--depth', '1', 'origin', $Ref) + Invoke-Git @('-C', $CheckoutDirectory, 'checkout', '--detach', '-q', 'FETCH_HEAD') +} + +# -------------------------------------------------------------------------- +# Validate arguments +# -------------------------------------------------------------------------- + +if (-not $Destination) { + Write-Banner + Show-Usage + exit 1 +} + +if (-not $Template) { + Stop-WithError 'Missing value for -Template' +} +if ($Template -match '[\\/]' -or $Template -eq '.' -or $Template -eq '..') { + Stop-WithError "Invalid template name: $Template" +} + +if (-not (Get-Command git -ErrorAction SilentlyContinue)) { + Stop-WithError 'Required command not found: git' +} + +# -------------------------------------------------------------------------- +# Resolve repo / ref / paths +# -------------------------------------------------------------------------- + +$templateRepo = $env:CINPO_TEMPLATE_REPO +$templateRef = $env:CINPO_TEMPLATE_REF + +if (-not $templateRepo -or -not $templateRef) { + $detected = Get-CheckoutSource -CandidateDirectory $PSScriptRoot + if ($detected) { + if (-not $templateRepo) { $templateRepo = $detected.Repo } + if (-not $templateRef) { $templateRef = $detected.Ref } + } +} +if (-not $templateRepo) { $templateRepo = $DefaultRepo } +if (-not $templateRef) { $templateRef = $DefaultRef } + +$destinationDir = [IO.Path]::GetFullPath([IO.Path]::Combine((Get-Location).ProviderPath, $Destination)) + +if (Test-Path -LiteralPath $destinationDir) { + Stop-WithError "Destination already exists: $destinationDir" +} + +$projectName = $Name +if (-not $projectName) { + $projectName = Split-Path -Leaf $destinationDir +} + +# -------------------------------------------------------------------------- +# Banner + configuration panel +# -------------------------------------------------------------------------- + +Write-Banner + +Write-Section 'Configuration' +Write-Info 'Template' $Template +Write-Info 'Project name' $projectName +Write-Info 'Destination' $destinationDir +Write-Info 'Source' "$templateRepo @ $templateRef" + +$workDir = Join-Path ([IO.Path]::GetTempPath()) ("cinpo-bootstrap-" + [guid]::NewGuid().ToString('N')) +New-Item -ItemType Directory -Force -Path $workDir | Out-Null + +try { + # ---------------------------------------------------------------------- + # Phase 1 - fetch template + # ---------------------------------------------------------------------- + + $templateCheckoutDir = Join-Path $workDir 'cinpo-source' + + Invoke-Step 1 4 'Fetching template' { + Invoke-SparseCheckout -RepoUrl $templateRepo -Ref $templateRef ` + -CheckoutDirectory $templateCheckoutDir -SparsePaths @("template/$Template/") + } + + $templateDir = Join-Path $templateCheckoutDir "template\$Template" + if (-not (Test-Path -LiteralPath $templateDir -PathType Container)) { + Write-Host "[x] Error: template not found: $Template" -ForegroundColor Red + Write-Host " Template source: $templateRepo @ $templateRef" + exit 1 + } + + # ---------------------------------------------------------------------- + # Phase 2 - copy files + configure project + # ---------------------------------------------------------------------- + + Invoke-Step 2 4 'Copying files & configuring project' { + $parent = Split-Path -Parent $destinationDir + if ($parent -and -not (Test-Path -LiteralPath $parent)) { + New-Item -ItemType Directory -Force -Path $parent | Out-Null + } + Copy-Item -LiteralPath $templateDir -Destination $destinationDir -Recurse -Force + + foreach ($stale in @('.gradle', 'build', 'vendor')) { + $stalePath = Join-Path $destinationDir $stale + if (Test-Path -LiteralPath $stalePath) { + Remove-Item -LiteralPath $stalePath -Recurse -Force + } + } + + $settingsFile = Join-Path $destinationDir 'settings.gradle' + if (-not (Test-Path -LiteralPath $settingsFile -PathType Leaf)) { + throw "settings.gradle not found in extracted template: $settingsFile" + } + + $escapedProjectName = $projectName -replace '\\', '\\' -replace "'", "\'" + $replacement = "rootProject.name = '$escapedProjectName'" + + $lines = [IO.File]::ReadAllLines($settingsFile) + $replaced = $false + for ($i = 0; $i -lt $lines.Length; $i++) { + if (-not $replaced -and $lines[$i] -ceq $TemplateProjectNameLine) { + $lines[$i] = $replacement + $replaced = $true + } + } + if (-not $replaced) { + throw "Expected template project name declaration not found in $settingsFile" + } + Write-TextFileLf -Path $settingsFile -Content (($lines -join "`n") + "`n") + } + + # ---------------------------------------------------------------------- + # Phase 3 - Gradle wrapper + # ---------------------------------------------------------------------- + + $wrapperDir = Join-Path $destinationDir 'gradle\wrapper' + + Invoke-Step 3 4 "Fetching Gradle wrapper ($GradleVersion)" { + New-Item -ItemType Directory -Force -Path $wrapperDir | Out-Null + + $gradleCheckoutDir = Join-Path $workDir 'gradle-source' + Invoke-SparseCheckout -RepoUrl $GradleRepo -Ref "v$GradleVersion" ` + -CheckoutDirectory $gradleCheckoutDir ` + -SparsePaths @('gradle/wrapper/gradle-wrapper.jar', 'gradlew', 'gradlew.bat') + + $wrapperJar = Join-Path $gradleCheckoutDir 'gradle\wrapper\gradle-wrapper.jar' + if (-not (Test-Path -LiteralPath $wrapperJar -PathType Leaf)) { + throw "gradle-wrapper.jar not found in fetched Gradle source: $GradleRepo @ v$GradleVersion" + } + + Copy-Item -LiteralPath $wrapperJar -Destination (Join-Path $wrapperDir 'gradle-wrapper.jar') -Force + Copy-Item -LiteralPath (Join-Path $gradleCheckoutDir 'gradlew') -Destination (Join-Path $destinationDir 'gradlew') -Force + Copy-Item -LiteralPath (Join-Path $gradleCheckoutDir 'gradlew.bat') -Destination (Join-Path $destinationDir 'gradlew.bat') -Force + + Write-TextFileLf -Path (Join-Path $wrapperDir 'gradle-wrapper.properties') -Content @" +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-$GradleVersion-all.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +"@ + } + + # ---------------------------------------------------------------------- + # Phase 4 - git init + # ---------------------------------------------------------------------- + + Invoke-Step 4 4 'Initializing git repository' { + Invoke-Git @('-C', $destinationDir, 'init', '-q') + # Windows has no POSIX execute bit, so record it in the index directly. + # Without this the generated project's gradlew is not executable on Linux. + Invoke-Git @('-C', $destinationDir, 'add', '-A') + Invoke-Git @('-C', $destinationDir, 'update-index', '--chmod=+x', 'gradlew') + Invoke-Git @('-C', $destinationDir, + '-c', 'user.name=CINPO Template', + '-c', 'user.email=cinpo-template@example.invalid', + 'commit', '-q', '-m', 'Initial commit') + } +} finally { + Remove-Item -LiteralPath $workDir -Recurse -Force -ErrorAction SilentlyContinue +} + +# -------------------------------------------------------------------------- +# Success panel (chrome to the host, summary to stdout) +# -------------------------------------------------------------------------- + +$box = '+--------------------------------------------------+' +Write-Host '' +Write-Host $box -ForegroundColor Green +Write-Host '| [OK] Project created successfully! |' -ForegroundColor Green +Write-Host ("| {0,-48}|" -f $destinationDir) -ForegroundColor Green +Write-Host $box -ForegroundColor Green +Write-Host '' + +Write-Host '[!] Before running, prepare the vendor directory:' -ForegroundColor Yellow +Write-Host " - Download the Java Card SDK Tools and Simulator from Oracle's website." +Write-Host ' - Place the archives (unextracted) into' +Write-Host " $destinationDir\vendor" +Write-Host '' +Write-Host 'Next steps' +Write-Host " 1. cd $destinationDir" +Write-Host ' 2. .\gradlew.bat cinpoRun' + +Write-Output @" +Created project from template: + $destinationDir + +Template: + $Template + +Project name: + $projectName + +Template source: + $templateRepo @ $templateRef +"@ diff --git a/scripts/extract_vendor.ps1 b/scripts/extract_vendor.ps1 new file mode 100644 index 0000000..7cfc53b --- /dev/null +++ b/scripts/extract_vendor.ps1 @@ -0,0 +1,77 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Windows counterpart of extract_vendor.sh: extracts the encrypted vendor archive + and verifies the Oracle Java Card kits it contains. + +.EXAMPLE + $env:VENDOR_PASSWORD = '' + powershell -ExecutionPolicy Bypass -File scripts\extract_vendor.ps1 +#> +[CmdletBinding()] +param( + [string] $Archive = 'vendor.zip', + [string] $Destination = 'vendor' +) + +$ErrorActionPreference = 'Stop' + +if (-not $env:VENDOR_PASSWORD) { + Write-Error "VENDOR_PASSWORD is required to extract $Archive." +} +if (-not (Test-Path -LiteralPath $Archive -PathType Leaf)) { + Write-Error "Vendor archive not found: $Archive" +} + +if (Test-Path -LiteralPath $Destination) { + Remove-Item -LiteralPath $Destination -Recurse -Force +} +New-Item -ItemType Directory -Path $Destination | Out-Null + +# bsdtar ships with Windows 10/11 and is the only in-box tool that reads +# password-protected zips. +tar -xf $Archive --passphrase $env:VENDOR_PASSWORD -C $Destination +if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to extract $Archive (tar exit code $LASTEXITCODE)." +} + +$requiredFiles = @( + 'java_card_devkit_tools-bin-v26.0-b_705-04-MAY-2026.zip' +) + +# Oracle ships one simulator kit per host OS. At least one must be present; which +# one is required depends on where the build eventually runs. +$simulatorFiles = @( + 'java_card_devkit_simulator-linux-bin-v26.0-b_788-05-MAY-2026.tar.gz', + 'java_card_devkit_simulator-win-bin-v26.0-b_788-05-MAY-2026.zip' +) + +$checksums = @{ + 'java_card_devkit_tools-bin-v26.0-b_705-04-MAY-2026.zip' = '86443cb1b64c006456e524d91082ba25d5ebb0ee5506c6e4d7088350ce251d9d' + 'java_card_devkit_simulator-linux-bin-v26.0-b_788-05-MAY-2026.tar.gz' = 'b8b999c3e1cfac5d56f7ef16654ce28c99cf472d96761d419a03ddf11f5811c0' + 'java_card_devkit_simulator-win-bin-v26.0-b_788-05-MAY-2026.zip' = '5ab2efc69486989bcd73d0ed235f4f8348e82df262448fbc781dabd1bcb9b9c5' +} + +foreach ($requiredFile in $requiredFiles) { + if (-not (Test-Path -LiteralPath (Join-Path $Destination $requiredFile) -PathType Leaf)) { + Write-Error "Required vendor file was not extracted: $Destination\$requiredFile" + } +} + +$presentSimulators = @($simulatorFiles | Where-Object { + Test-Path -LiteralPath (Join-Path $Destination $_) -PathType Leaf +}) +if ($presentSimulators.Count -eq 0) { + Write-Error "No Java Card simulator kit was extracted. Expected one of: $($simulatorFiles -join ', ')" +} + +# Verify only what was actually extracted, so a kit for another OS may be absent. +foreach ($entry in $checksums.GetEnumerator()) { + $path = Join-Path $Destination $entry.Key + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { continue } + $actual = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant() + if ($actual -ne $entry.Value) { + Write-Error "Checksum verification failed for $($entry.Key). Expected: $($entry.Value), Actual: $actual" + } + Write-Host "$($entry.Key): OK" +} diff --git a/scripts/extract_vendor.sh b/scripts/extract_vendor.sh index c39c71d..bdcd12f 100755 --- a/scripts/extract_vendor.sh +++ b/scripts/extract_vendor.sh @@ -21,7 +21,13 @@ unzip -q -P "${VENDOR_PASSWORD}" "${archive}" -d "${destination}" required_files=( "java_card_devkit_tools-bin-v26.0-b_705-04-MAY-2026.zip" +) + +# Oracle ships one simulator kit per host OS. At least one must be present; which +# one is required depends on where the build eventually runs. +simulator_files=( "java_card_devkit_simulator-linux-bin-v26.0-b_788-05-MAY-2026.tar.gz" + "java_card_devkit_simulator-win-bin-v26.0-b_788-05-MAY-2026.zip" ) for required_file in "${required_files[@]}"; do @@ -31,10 +37,29 @@ for required_file in "${required_files[@]}"; do fi done +present_simulators=() +for simulator_file in "${simulator_files[@]}"; do + if [[ -f "${destination}/${simulator_file}" ]]; then + present_simulators+=("${simulator_file}") + fi +done + +if [[ ${#present_simulators[@]} -eq 0 ]]; then + echo "No Java Card simulator kit was extracted. Expected one of: ${simulator_files[*]}" >&2 + exit 1 +fi + ( cd "${destination}" - sha256sum -c <<'CHECKSUMS' + all_checksums=$(cat <<'CHECKSUMS' 86443cb1b64c006456e524d91082ba25d5ebb0ee5506c6e4d7088350ce251d9d java_card_devkit_tools-bin-v26.0-b_705-04-MAY-2026.zip b8b999c3e1cfac5d56f7ef16654ce28c99cf472d96761d419a03ddf11f5811c0 java_card_devkit_simulator-linux-bin-v26.0-b_788-05-MAY-2026.tar.gz +5ab2efc69486989bcd73d0ed235f4f8348e82df262448fbc781dabd1bcb9b9c5 java_card_devkit_simulator-win-bin-v26.0-b_788-05-MAY-2026.zip CHECKSUMS +) + # Verify only what was actually extracted, so a kit for another OS may be absent. + while IFS= read -r checksum_line; do + [[ -f "${checksum_line##* }" ]] || continue + printf '%s\n' "${checksum_line}" | sha256sum -c + done <<< "${all_checksums}" ) diff --git a/src/main/groovy/app.aoki.cinpo.prepare-java-card-tools.gradle b/src/main/groovy/app.aoki.cinpo.prepare-java-card-tools.gradle index 937a9a7..297f47c 100644 --- a/src/main/groovy/app.aoki.cinpo.prepare-java-card-tools.gradle +++ b/src/main/groovy/app.aoki.cinpo.prepare-java-card-tools.gradle @@ -34,14 +34,37 @@ def detectCinpoArch = { return archName.replaceAll('[^a-z0-9]+', '-') } +// Oracle ships one simulator archive per host OS. They unpack to the same internal +// layout, but the archive format and the runtime binary name differ, so both the +// vendor file to read and the executable to configure are selected per OS. +def SIMULATOR_ARCHIVES = [ + 'linux' : 'java_card_devkit_simulator-linux-bin-v26.0-b_788-05-MAY-2026.tar.gz', + 'windows': 'java_card_devkit_simulator-win-bin-v26.0-b_788-05-MAY-2026.zip' +] + +def SIMULATOR_EXECUTABLES = [ + 'linux' : 'jcsl', + 'windows': 'jcsw.exe' +] + +def cinpoOs = detectCinpoOs() + +def simulatorArchiveName = SIMULATOR_ARCHIVES[cinpoOs] +if (simulatorArchiveName == null) { + throw new GradleException( + "No Oracle Java Card simulator archive is known for OS '${cinpoOs}'. " + + "Supported: ${SIMULATOR_ARCHIVES.keySet().join(', ')}.") +} + ext.javaCardToolsArchive = layout.projectDirectory.file('vendor/java_card_devkit_tools-bin-v26.0-b_705-04-MAY-2026.zip') -ext.javaCardSimulatorArchive = layout.projectDirectory.file('vendor/java_card_devkit_simulator-linux-bin-v26.0-b_788-05-MAY-2026.tar.gz') +ext.javaCardSimulatorArchive = layout.projectDirectory.file("vendor/${simulatorArchiveName}") +ext.javaCardSimulatorExecutableName = SIMULATOR_EXECUTABLES[cinpoOs] ext.extractedJavaCardToolsDir = layout.buildDirectory.dir('cinpo-cache/jcdk') -ext.configuredJcslDir = layout.buildDirectory.dir("cinpo-cache/jcsl/${detectCinpoOs()}-${detectCinpoArch()}") +ext.configuredJcslDir = layout.buildDirectory.dir("cinpo-cache/jcsl/${cinpoOs}-${detectCinpoArch()}") ext.extractedJavaCardToolsJar = extractedJavaCardToolsDir.map { it.file('tools/lib/tools.jar') } ext.extractedJavaCardSocketProviderJar = extractedJavaCardToolsDir.map { it.file('simulator/client/COMService/socketprovider.jar') } ext.extractedJavaCardConfiguratorJar = extractedJavaCardToolsDir.map { it.file('simulator/tools/Configurator.jar') } -ext.configuredJcsl = configuredJcslDir.map { it.file('jcsl') } +ext.configuredJcsl = configuredJcslDir.map { it.file(javaCardSimulatorExecutableName) } def targetApiVersionFromManifest = { File manifestFile = layout.projectDirectory.file('manifest.yaml').asFile @@ -132,10 +155,17 @@ def filterToolsJarToTargetApi = { def verifyDownloadedVendorFiles = { def vendorDir = file('vendor') - def requiredVendorFiles = [ + // Only the simulator archive for the current OS is required; the others may be + // absent from vendor/ on a host that never runs them. + def vendorChecksums = [ 'java_card_devkit_simulator-linux-bin-v26.0-b_788-05-MAY-2026.tar.gz': 'b8b999c3e1cfac5d56f7ef16654ce28c99cf472d96761d419a03ddf11f5811c0', + 'java_card_devkit_simulator-win-bin-v26.0-b_788-05-MAY-2026.zip': '5ab2efc69486989bcd73d0ed235f4f8348e82df262448fbc781dabd1bcb9b9c5', 'java_card_devkit_tools-bin-v26.0-b_705-04-MAY-2026.zip': '86443cb1b64c006456e524d91082ba25d5ebb0ee5506c6e4d7088350ce251d9d' ] + def requiredVendorFiles = [ + javaCardSimulatorArchive.asFile.name, + javaCardToolsArchive.asFile.name + ].collectEntries { [(it): vendorChecksums[it]] } def computeSHA256 = { File file -> def digest = java.security.MessageDigest.getInstance('SHA-256') @@ -161,6 +191,12 @@ def verifyDownloadedVendorFiles = { } } +def simulatorArchiveTree = { + File archive = javaCardSimulatorArchive.asFile + // The Linux kit is a gzipped tarball, the Windows kit a plain zip. + return archive.name.endsWith('.zip') ? zipTree(archive) : tarTree(resources.gzip(archive)) +} + def verifyExtractedJavaCardFiles = { String targetApiVersion = targetApiVersionFromManifest() if (!extractedJavaCardToolsJar.get().asFile.exists()) { @@ -197,8 +233,9 @@ def configureJcslForJcdkSim = { configuredJcslFile.setExecutable(true) + String javaLauncher = cinpoOs == 'windows' ? 'bin/java.exe' : 'bin/java' List command = [ - new File(System.getProperty('java.home'), 'bin/java').absolutePath, + new File(System.getProperty('java.home'), javaLauncher).absolutePath, '-jar', configuratorJar.absolutePath, '-binary', configuredJcslFile.absolutePath, '-force', @@ -224,8 +261,11 @@ def configureJcslForJcdkSim = { } def verifyConfiguredJcsl = { - if (!configuredJcsl.get().asFile.canExecute()) { - throw new GradleException("Expected configured Java Card simulator executable was not found or is not executable: ${configuredJcsl.get().asFile}") + File configuredJcslFile = configuredJcsl.get().asFile + // Windows has no POSIX execute bit; a .exe on disk is all that can be asserted there. + boolean usable = cinpoOs == 'windows' ? configuredJcslFile.isFile() : configuredJcslFile.canExecute() + if (!usable) { + throw new GradleException("Expected configured Java Card simulator executable was not found or is not executable: ${configuredJcslFile}") } } @@ -245,7 +285,7 @@ tasks.register('prepareJavaCardTools') { include "lib/${apiClassicJarName}" into 'tools' } - from(tarTree(resources.gzip(javaCardSimulatorArchive.asFile))) { + from(simulatorArchiveTree()) { include 'client/COMService/socketprovider.jar' include 'runtime/bin/**' include 'tools/Configurator.jar' diff --git a/src/main/java-gradle/app/aoki/cinpo/gradle/task/CinpoRunTask.java b/src/main/java-gradle/app/aoki/cinpo/gradle/task/CinpoRunTask.java index b0ae530..3a41f09 100644 --- a/src/main/java-gradle/app/aoki/cinpo/gradle/task/CinpoRunTask.java +++ b/src/main/java-gradle/app/aoki/cinpo/gradle/task/CinpoRunTask.java @@ -89,6 +89,15 @@ private static List normalizeArgs(List args) { return List.copyOf(normalized); } + /** + * Splits a {@code --args} string into CLI tokens using shell-like quoting. + * + *

A backslash escapes only the characters that quoting actually needs: another + * backslash, either quote character, and whitespace. Anywhere else it is an ordinary + * character, so Windows paths such as {@code C:\Users\me\profile.yaml} survive + * verbatim instead of collapsing to {@code C:Usersmeprofile.yaml}. Already-escaped + * input ({@code C:\\Users}) still resolves to a single backslash. + */ private static List tokenizeArgs(String argsString) { if (argsString == null || argsString.isBlank()) { return List.of(); @@ -107,7 +116,7 @@ private static List tokenizeArgs(String argsString) { escaping = false; continue; } - if (ch == '\\' && !singleQuoted) { + if (ch == '\\' && !singleQuoted && isEscapable(argsString, index + 1)) { escaping = true; continue; } @@ -129,9 +138,6 @@ private static List tokenizeArgs(String argsString) { current.append(ch); } - if (escaping) { - current.append('\\'); - } if (singleQuoted || doubleQuoted) { throw new IllegalArgumentException("Unclosed quote in --args: " + argsString); } @@ -140,4 +146,16 @@ private static List tokenizeArgs(String argsString) { } return List.copyOf(tokens); } + + /** + * Reports whether the character at {@code index} is one a backslash may escape. + * A trailing backslash escapes nothing and is therefore literal. + */ + private static boolean isEscapable(String argsString, int index) { + if (index >= argsString.length()) { + return false; + } + char next = argsString.charAt(index); + return next == '\\' || next == '\'' || next == '"' || Character.isWhitespace(next); + } } diff --git a/src/main/java/app/aoki/cinpo/runtime/jcresim/JcreSimAdapter.java b/src/main/java/app/aoki/cinpo/runtime/jcresim/JcreSimAdapter.java index f225411..75ca654 100644 --- a/src/main/java/app/aoki/cinpo/runtime/jcresim/JcreSimAdapter.java +++ b/src/main/java/app/aoki/cinpo/runtime/jcresim/JcreSimAdapter.java @@ -9,9 +9,12 @@ import java.io.InputStream; import java.net.InetSocketAddress; import java.net.SocketAddress; +import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import javax.smartcardio.Card; @@ -69,60 +72,107 @@ static Path resolveSimulatorExecutable() { return buildCachePath; } + String executableName = Util.simulatorExecutableName(); Path managedSimulator = Path.of("build").resolve("cinpo-cache").resolve("jcdk") - .resolve("simulator").resolve("runtime").resolve("bin").resolve("jcsl"); + .resolve("simulator").resolve("runtime").resolve("bin").resolve(executableName); if (Files.isExecutable(managedSimulator)) { return managedSimulator; } throw new IllegalStateException( - "Oracle JCRE simulator executable (jcsl) not found. Searched locations:\n" + + "Oracle JCRE simulator executable (" + executableName + ") not found. Searched locations:\n" + " - cinpo.simulator.path=" + (configuredPath == null ? "" : configuredPath) + "\n" + - " - build/cinpo-cache/jcsl/" + Util.detectOS() + "-" + Util.detectArch() + "/jcsl\n" + - " - build/cinpo-cache/jcdk/simulator/runtime/bin/jcsl\n" + + " - build/cinpo-cache/jcsl/" + Util.detectOS() + "-" + Util.detectArch() + "/" + executableName + "\n" + + " - build/cinpo-cache/jcdk/simulator/runtime/bin/" + executableName + "\n" + "Configure cinpo.simulator.path or prepare the managed JCDK build cache first."); } private static Path extractBundledSimulatorIfPresent() { String os = Util.detectOS(); String arch = Util.detectArch(); + String executableName = Util.simulatorExecutableName(); String resourcePrefix = "cinpo-simulator/" + os + "-" + arch + "/"; - String markerResource = resourcePrefix + "jcsl"; + String markerResource = resourcePrefix + executableName; ClassLoader loader = JcreSimAdapter.class.getClassLoader(); if (loader.getResource(markerResource) == null) { return null; } - Path targetDir = Path.of(System.getProperty("java.io.tmpdir"), "cinpo-simulator", os + "-" + arch); + List stagedFiles = new ArrayList<>(); + stagedFiles.add(executableName); + stagedFiles.addAll(Util.simulatorRuntimeLibraryNames()); + + Path sharedDir = Path.of(System.getProperty("java.io.tmpdir"), "cinpo-simulator", os + "-" + arch); try { - Files.createDirectories(targetDir); - for (String name : List.of("jcsl", "legacy.so", "libcrypto.so", "libcrypto.so.3", "libssl.so", "libssl.so.3")) { - String resource = resourcePrefix + name; - try (InputStream in = loader.getResourceAsStream(resource)) { - if (in == null) { - continue; - } - Path target = targetDir.resolve(name); - Files.copy(in, target, java.nio.file.StandardCopyOption.REPLACE_EXISTING); - target.toFile().setExecutable(true); - } + return stageBundledSimulator(loader, resourcePrefix, stagedFiles, sharedDir, executableName); + } catch (IOException sharedStagingFailed) { + // Windows refuses to replace an executable or DLL that another process still + // holds open, so a concurrent or recently finished run can make the shared + // location unwritable. Stage into a directory private to this JVM instead. + try { + Path privateDir = Files.createTempDirectory("cinpo-simulator-"); + return stageBundledSimulator(loader, resourcePrefix, stagedFiles, privateDir, executableName); + } catch (IOException privateStagingFailed) { + privateStagingFailed.addSuppressed(sharedStagingFailed); + throw new IllegalStateException( + "Failed to extract bundled CINPO simulator runtime", privateStagingFailed); } - return targetDir.resolve("jcsl"); - } catch (IOException e) { - throw new IllegalStateException("Failed to extract bundled CINPO simulator runtime", e); } } /** - * Find jcsl in the build cache using OS and architecture detection. + * Copy the bundled simulator runtime into {@code targetDir}, reusing files that are + * already staged there so a locked-but-identical binary does not have to be replaced. + * + * @return path to the staged simulator executable + */ + private static Path stageBundledSimulator( + ClassLoader loader, + String resourcePrefix, + List stagedFiles, + Path targetDir, + String executableName) throws IOException { + Files.createDirectories(targetDir); + for (String name : stagedFiles) { + URL resource = loader.getResource(resourcePrefix + name); + if (resource == null) { + continue; + } + Path target = targetDir.resolve(name); + if (isAlreadyStaged(resource, target)) { + continue; + } + try (InputStream in = resource.openStream()) { + Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING); + } + target.toFile().setExecutable(true); + } + return targetDir.resolve(executableName); + } + + /** + * Decide whether a previously staged file can be reused. The bundled runtime is a + * fixed set of Oracle binaries for one kit version, so a size match identifies the + * same file and rules out a truncated copy from an interrupted run. + */ + private static boolean isAlreadyStaged(URL resource, Path target) throws IOException { + if (!Files.isRegularFile(target)) { + return false; + } + long resourceSize = resource.openConnection().getContentLengthLong(); + return resourceSize >= 0 && resourceSize == Files.size(target); + } + + /** + * Find the simulator executable in the build cache using OS and architecture detection. * - * @return path to jcsl in build cache, or null if not found + * @return path to the simulator executable in build cache, or null if not found */ private static Path findJcslInBuildCache() { String os = Util.detectOS(); String arch = Util.detectArch(); Path buildCache = Path.of("build").resolve("cinpo-cache").resolve("jcsl"); - return buildCache.resolve(os + "-" + arch).resolve("jcsl"); + return buildCache.resolve(os + "-" + arch).resolve(Util.simulatorExecutableName()); } /** diff --git a/src/main/java/app/aoki/cinpo/runtime/jcresim/SimulatorProcess.java b/src/main/java/app/aoki/cinpo/runtime/jcresim/SimulatorProcess.java index 83e9bce..57a9d2b 100644 --- a/src/main/java/app/aoki/cinpo/runtime/jcresim/SimulatorProcess.java +++ b/src/main/java/app/aoki/cinpo/runtime/jcresim/SimulatorProcess.java @@ -1,5 +1,6 @@ package app.aoki.cinpo.runtime.jcresim; +import app.aoki.cinpo.util.Util; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -67,6 +68,14 @@ final class SimulatorProcess implements AutoCloseable { * */ void ensureReady() { + if (process != null && process.isAlive()) { + // Reconnecting to the simulator this instance started, for example after + // reset() dropped the card session. Ownership must survive that, or close() + // would leave the simulator running. + waitForSimulatorReady(); + return; + } + if (isSimulatorReachable()) { ownsProcess = false; return; @@ -128,7 +137,7 @@ ProcessBuilder createSimulatorProcessBuilder(int simulatorPort) { Path executableParent = executable.toAbsolutePath().getParent(); if (executableParent != null) { processBuilder.directory(executableParent.toFile()); - prependLdLibraryPath(processBuilder.environment(), executableParent); + prependNativeLibraryPath(processBuilder.environment(), executableParent); } processBuilder.redirectInput(ProcessBuilder.Redirect.PIPE); // Merge stderr → stdout so we capture everything jcsl emits @@ -137,13 +146,21 @@ ProcessBuilder createSimulatorProcessBuilder(int simulatorPort) { return processBuilder; } - private static void prependLdLibraryPath(Map environment, Path libraryDirectory) { + /** + * Point the simulator process at the shared libraries shipped next to its executable. + * Windows resolves DLLs through {@code PATH} (semicolon separated); ELF platforms use + * {@code LD_LIBRARY_PATH} (colon separated). + */ + static void prependNativeLibraryPath(Map environment, Path libraryDirectory) { + boolean windows = Util.isWindows(); + String variable = windows ? "PATH" : "LD_LIBRARY_PATH"; + String separator = windows ? ";" : ":"; String libPath = libraryDirectory.toAbsolutePath().toString(); - String existing = environment.get("LD_LIBRARY_PATH"); + String existing = environment.get(variable); if (existing != null && !existing.isBlank()) { - environment.put("LD_LIBRARY_PATH", libPath + ":" + existing); + environment.put(variable, libPath + separator + existing); } else { - environment.put("LD_LIBRARY_PATH", libPath); + environment.put(variable, libPath); } } diff --git a/src/main/java/app/aoki/cinpo/util/Util.java b/src/main/java/app/aoki/cinpo/util/Util.java index 35fe35d..57e1ffc 100644 --- a/src/main/java/app/aoki/cinpo/util/Util.java +++ b/src/main/java/app/aoki/cinpo/util/Util.java @@ -80,6 +80,29 @@ public static boolean isWindows() { return "windows".equals(detectOS()); } + /** + * Returns the Oracle JCDK simulator executable name for the current OS. + * Oracle names the binary {@code jcsl} on Linux/macOS and {@code jcsw.exe} on Windows. + * + * @return the simulator executable file name + */ + public static String simulatorExecutableName() { + return isWindows() ? "jcsw.exe" : "jcsl"; + } + + /** + * Returns the shared-library files that accompany the simulator executable on the + * current OS. Used to stage the runtime out of the appliance JAR. + * + * @return companion native library file names + */ + public static List simulatorRuntimeLibraryNames() { + if (isWindows()) { + return List.of("legacy.dll", "libcrypto-3.dll", "libssl-3.dll"); + } + return List.of("legacy.so", "libcrypto.so", "libcrypto.so.3", "libssl.so", "libssl.so.3"); + } + // ─── Binary read helpers ───────────────────────────────────────────── public static int readU2(byte[] data, int offset) { diff --git a/src/test/java/app/aoki/cinpo/runtime/jcresim/SimulatorProcessTest.java b/src/test/java/app/aoki/cinpo/runtime/jcresim/SimulatorProcessTest.java index f9574b9..c03cf9b 100644 --- a/src/test/java/app/aoki/cinpo/runtime/jcresim/SimulatorProcessTest.java +++ b/src/test/java/app/aoki/cinpo/runtime/jcresim/SimulatorProcessTest.java @@ -5,13 +5,19 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import app.aoki.cinpo.util.Util; import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; final class SimulatorProcessTest { + // The fixture is a /bin/sh script that stays alive; there is no equally simple + // long-running batch equivalent, so the teardown path is covered on POSIX only. + @DisabledOnOs(OS.WINDOWS) @Test void startupTimeoutStopsOwnedProcess() throws Exception { Path runtimeDir = Files.createTempDirectory("cinpo-jcsl-timeout"); @@ -35,18 +41,22 @@ void startupTimeoutStopsOwnedProcess() throws Exception { } @Test - void processBuilderPrependsSimulatorDirectoryToLdLibraryPath() throws Exception { + void processBuilderPrependsSimulatorDirectoryToNativeLibraryPath() throws Exception { Path runtimeDir = Files.createTempDirectory("cinpo-jcsl-runtime"); - Path executable = runtimeDir.resolve("jcsl"); + Path executable = runtimeDir.resolve(Util.simulatorExecutableName()); Files.writeString(executable, "#!/bin/sh\nexit 0\n"); + boolean windows = Util.isWindows(); + String variable = windows ? "PATH" : "LD_LIBRARY_PATH"; + String separator = windows ? ";" : ":"; + SimulatorProcess simulatorProcess = new SimulatorProcess(executable); ProcessBuilder builder = simulatorProcess.createSimulatorProcessBuilder(12345); Map environment = builder.environment(); - String ldLibraryPath = environment.get("LD_LIBRARY_PATH"); + String libraryPath = environment.get(variable); - assertTrue(ldLibraryPath != null && !ldLibraryPath.isBlank(), "LD_LIBRARY_PATH should be configured"); - assertEquals(runtimeDir.toAbsolutePath().toString(), ldLibraryPath.split(":", 2)[0]); + assertTrue(libraryPath != null && !libraryPath.isBlank(), variable + " should be configured"); + assertEquals(runtimeDir.toAbsolutePath().toString(), libraryPath.split(separator, 2)[0]); assertEquals(runtimeDir.toFile(), builder.directory()); } } diff --git a/template/authenticated/build.gradle b/template/authenticated/build.gradle index 8521e7a..e7e3830 100644 --- a/template/authenticated/build.gradle +++ b/template/authenticated/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'app.aoki.cinpo.gradle' version '0.3.2' + id 'app.aoki.cinpo.gradle' version '0.3.3' } java { @@ -34,7 +34,7 @@ repositories { } dependencies { - implementation 'app.aoki.cinpo:cinpo:0.3.2' + implementation 'app.aoki.cinpo:cinpo:0.3.3' implementation 'org.yaml:snakeyaml:2.2' } diff --git a/template/basic/build.gradle b/template/basic/build.gradle index c5ef2c8..bd090b1 100644 --- a/template/basic/build.gradle +++ b/template/basic/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'app.aoki.cinpo.gradle' version '0.3.2' + id 'app.aoki.cinpo.gradle' version '0.3.3' } java { @@ -34,7 +34,7 @@ repositories { } dependencies { - implementation 'app.aoki.cinpo:cinpo:0.3.2' + implementation 'app.aoki.cinpo:cinpo:0.3.3' implementation 'org.yaml:snakeyaml:2.2' implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1' implementation 'org.bouncycastle:bcpkix-jdk18on:1.78.1' diff --git a/template/probe/build.gradle b/template/probe/build.gradle index c5ef2c8..bd090b1 100644 --- a/template/probe/build.gradle +++ b/template/probe/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'app.aoki.cinpo.gradle' version '0.3.2' + id 'app.aoki.cinpo.gradle' version '0.3.3' } java { @@ -34,7 +34,7 @@ repositories { } dependencies { - implementation 'app.aoki.cinpo:cinpo:0.3.2' + implementation 'app.aoki.cinpo:cinpo:0.3.3' implementation 'org.yaml:snakeyaml:2.2' implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1' implementation 'org.bouncycastle:bcpkix-jdk18on:1.78.1' diff --git a/vendor.zip b/vendor.zip index 7f49b73..2fee0ad 100644 Binary files a/vendor.zip and b/vendor.zip differ