diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a82ef1..9825741 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## Unreleased +- `play build`: an absolute `--dir` / `play.build.dir` stays absolute (it was taken as a folder under the project and lost its leading slash); a `gradlew` without the exec bit (committed from Windows) is made executable with a note to commit the mode. The desktop release signs by key path only. + ## 0.8.59 ยท 2026-10-29 - `mcp --http`: a `tools/call` that accepts `text/event-stream` gets SSE keepalive comments every 20 s and then its result, so long calls survive proxies that cut silent responses. The public server's App Store search keeps one budget of 20 calls a minute (Apple's limit). diff --git a/cli/lib/gradle.ts b/cli/lib/gradle.ts index 9d868bc..8d14617 100644 --- a/cli/lib/gradle.ts +++ b/cli/lib/gradle.ts @@ -3,7 +3,7 @@ // properties through ORG_GRADLE_PROJECT_* variables, so passwords never appear on a command line), and read // package / versionCode / versionName back from the built AAB (its manifest is protocol buffers, aapt2 format). import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; -import { join, relative } from 'node:path'; +import { join, relative, resolve } from 'node:path'; import type { AndroidSigning } from './credentials'; import * as rt from './rt'; import { zipEntries, zipRead } from './zip'; @@ -38,7 +38,8 @@ export function pickGradle(root: string, cfg: PlayBuildConfig = {}): GradleTarge : ['capacitor.config.ts', 'capacitor.config.json', 'capacitor.config.js'].some(f => existsSync(join(root, f))) ? 'capacitor' : (() => { try { const p = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')); return !!(p.dependencies?.['react-native'] ?? p.devDependencies?.['react-native']); } catch { return false; } })() ? 'react-native' : 'native'; - const dirs = cfg.dir ? [join(root, cfg.dir)] : [join(root, 'android'), root]; + // resolve, not join: an absolute play.build.dir / --dir stays absolute (join made /Users/... a folder under the root) + const dirs = cfg.dir ? [resolve(root, cfg.dir)] : [join(root, 'android'), root]; let dir = dirs.find(isGradleDir); if (!dir) throw new Error(`no Gradle project (settings.gradle) in ${dirs.map(d => relative(root, d) || '.').join(' or ')}: set play.build.dir${framework === 'capacitor' ? ' (or npx cap add android)' : ''}`); const unity = existsSync(join(dir, 'launcher')) && existsSync(join(dir, 'unityLibrary')); diff --git a/cli/lib/web-assets.ts b/cli/lib/web-assets.ts index 638b669..83d0179 100644 --- a/cli/lib/web-assets.ts +++ b/cli/lib/web-assets.ts @@ -14,7 +14,7 @@ // The comparison (webStaleness) and the capacitor config reading (webDirOf) are pure. import { createHash } from 'node:crypto'; import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; -import { dirname, join, relative, sep } from 'node:path'; +import { dirname, join, relative, resolve, sep } from 'node:path'; import type { ShipConfig } from './config'; import { log, warn } from './log'; import { pump } from './runner'; @@ -89,7 +89,7 @@ export function capacitorCandidates(root: string): string[] { */ export function capacitorProject(root: string, hint?: string): CapProject | null { if (hint) { - for (let d = join(root, hint); ; d = dirname(d)) { + for (let d = resolve(root, hint); ; d = dirname(d)) { const rel = relative(root, d); if (rel.startsWith('..')) break; const c = capAt(root, rel.split(sep).join('/')); diff --git a/cli/play/build.ts b/cli/play/build.ts index 01e9155..cf87d69 100644 --- a/cli/play/build.ts +++ b/cli/play/build.ts @@ -14,7 +14,7 @@ // Before Gradle runs, the app module's build file must read -PversionCode / -PversionName in defaultConfig (a literal // or an env var keeps the AAB at that value; android.injected.version.* does not override it for bundleRelease): a // warning with the exact line to use. After the build, an AAB whose versionCode is not the planned one is a hard stop. -import { copyFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs'; +import { chmodSync, copyFileSync, existsSync, mkdirSync, statSync, writeFileSync } from 'node:fs'; import { join, relative } from 'node:path'; import { androidSigning } from '../lib/credentials'; import type { Ctx } from '../lib/ctx'; @@ -108,6 +108,11 @@ export async function run(ctx: Ctx): Promise { shipPhase('build', `${plan.task}, versionCode ${code || '?'}`); const started = Date.now(); mkdirSync(out, { recursive: true }); + // a gradlew committed from Windows has no exec bit: macOS / Linux then say "Executable not found: ./gradlew" + if (hasWrapper && process.platform !== 'win32') { + const w = join(t.dir, 'gradlew'); + if ((statSync(w).mode & 0o111) === 0) { chmodSync(w, statSync(w).mode | 0o755); log(TAG, `${relative(root, w)} had no exec bit (committed from Windows?): made it executable; commit it with git update-index --chmod=+x`); } + } const p = rt.spawn(argv, { cwd, stdout: 'pipe', stderr: 'pipe', env: { ...process.env, ...androidSdkEnv(sdk), ...signingEnv(signing) } }); const lines: string[] = []; const each = (line: string): void => { diff --git a/tests/gradle-dir.test.ts b/tests/gradle-dir.test.ts new file mode 100644 index 0000000..ebb5836 --- /dev/null +++ b/tests/gradle-dir.test.ts @@ -0,0 +1,16 @@ +import { expect, test } from 'bun:test'; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { pickGradle } from '../cli/lib/gradle'; + +test('an absolute play.build.dir / --dir stays absolute (it lost its leading slash under the root)', () => { + const base = mkdtempSync(join(tmpdir(), 'gdir-')); + try { + const root = join(base, 'store'), android = join(base, 'other', 'apps', 'native', 'android'); + mkdirSync(root, { recursive: true }); mkdirSync(join(android, 'app'), { recursive: true }); + writeFileSync(join(android, 'settings.gradle'), "include ':app'\n"); + expect(resolve(pickGradle(root, { dir: android }).dir)).toBe(resolve(android)); + expect(resolve(pickGradle(root, { dir: '../other/apps/native/android' }).dir)).toBe(resolve(android)); + } finally { rmSync(base, { recursive: true, force: true }); } +});