// The version a project declares, by Koh's own rule (`src/build/version.v` in Koh 3): the first // source that answers wins, so a `VERSION` file overrides everything, and nothing is evaluated: a // value built from a variable or a call is skipped and the next source tried. A source that answers // with something that is not a version means no version, as Koh then refuses to publish. const GRADLE_FILES = [ "app/build.gradle.kts", "app/build.gradle", "build.gradle.kts", "build.gradle", ]; const VERSION = /^[A-Za-z0-9.-]{1,32}$/; const ASCII_SPACE = /^[ \n\t\v\f\r]+|[ \n\t\v\f\r]+$/g; /** The declared version, reading files at one save through `read` (null for a missing file). */ export function declaredVersion(read: (path: string) => string | null): string | null { const sources: (() => string | null)[] = [ () => read("VERSION")?.replace(ASCII_SPACE, "") || null, () => lineVersion(read("build.zig.zon"), ".version"), () => lineVersion(read("v.mod"), "version"), () => lineVersion(read("package.json"), '"version"'), () => lineVersion(read("Cargo.toml"), "version"), () => plistVersion(read("Info.plist")), ...GRADLE_FILES.map((path) => () => gradleVersion(read(path))), ]; for (const source of sources) { const value = source(); if (value !== null) return VERSION.test(value) ? value : null; } return null; } // The value of the first line the key starts that assigns a quoted literal. function lineVersion(text: string | null, key: string): string | null { for (const line of text?.split(/\r\n|[\r\n]/) ?? []) { const trimmed = line.replace(/^[ \t]+/, ""); if (trimmed === "" || trimmed.startsWith("//") || trimmed.startsWith("#")) continue; if (!trimmed.startsWith(key)) continue; const value = literalAfter(trimmed, key.length); if (value !== null) return value; } return null; } // The quoted literal right after the key, an optional `=` or `:` and any spaces. function literalAfter(line: string, from: number): string | null { let at = from; while (at < line.length && " \t=:".includes(line[at] as string)) at++; const quote = line[at]; if (quote !== '"' && quote !== "'") return null; const end = line.indexOf(quote, at + 1); return end < 0 ? null : line.slice(at + 1, end); } // CFBundleShortVersionString: its value is in the after the key, not on the key's line. function plistVersion(text: string | null): string | null { const at = text?.indexOf("CFBundleShortVersionString") ?? -1; if (!text || at < 0) return null; const rest = text.slice(at); const open = rest.indexOf(""); const close = rest.indexOf(""); if (open < 0 || close <= open + "".length) return null; return rest.slice(open + "".length, close).replace(ASCII_SPACE, "") || null; } // `versionName` assigned a quoted literal, Kotlin (`= "1.2"`) or Groovy (`"1.2"`); comments and // `versionNameSuffix` are not it. function gradleVersion(text: string | null): string | null { for (const line of text?.split(/\r\n|[\r\n]/) ?? []) { const trimmed = line.replace(/^[ \t]+/, ""); if (trimmed === "" || /^(\/\/|\/\*|\*)/.test(trimmed)) continue; if (!/^versionName[ =]/.test(trimmed)) continue; const value = literalAfter(trimmed, "versionName".length); if (value !== null) return value; } return null; }