version.v

5.3 KB · raw

1//! The version a project declares, read from its own files.2//!3//! The first source that answers wins, so a `VERSION` file overrides everything4//! and a project that already declares its version in `v.mod`, `package.json`,5//! `Cargo.toml`, `Info.plist` or Gradle needs no second copy to keep in step.6//! Nothing is evaluated: a value built from a variable or a function call is7//! skipped and the next source is tried, so Koh never runs a build system to8//! ask what version something is.910module build1112import os1314// The spec's search order for Gradle's `versionName`.15const gradle_files = ['app/build.gradle.kts', 'app/build.gradle', 'build.gradle.kts', 'build.gradle']1617// project_version is the version the project declares. Finding nothing is an18// error: publishing refuses rather than inventing 0.0.0, and a build cannot fill19// `{version}` without one.20pub fn project_version(root string) !string {21	if value := file_version(os.join_path(root, 'VERSION')) {22		return checked(value, 'VERSION')23	}24	if value := line_version(os.join_path(root, 'build.zig.zon'), '.version') {25		return checked(value, 'build.zig.zon')26	}27	if value := line_version(os.join_path(root, 'v.mod'), 'version') {28		return checked(value, 'v.mod')29	}30	if value := line_version(os.join_path(root, 'package.json'), '"version"') {31		return checked(value, 'package.json')32	}33	if value := line_version(os.join_path(root, 'Cargo.toml'), 'version') {34		return checked(value, 'Cargo.toml')35	}36	if value := plist_version(os.join_path(root, 'Info.plist')) {37		return checked(value, 'Info.plist')38	}39	for file in gradle_files {40		if value := gradle_version(os.join_path(root, file)) {41			return checked(value, file)42		}43	}44	return error('no version in ${root} — add a VERSION file holding the version to publish')45}4647// checked refuses a value that is not a version. It becomes a release file48// name, and 1–32 letters, digits, dots and dashes is what Koh 2 allowed.49fn checked(value string, source string) !string {50	if value.len == 0 || value.len > 32 || !value.bytes().all(is_version_byte(it)) {51		return error('${source} says the version is "${value}", which is not 1–32 letters, digits, dots and dashes')52	}53	return value54}5556fn is_version_byte(character u8) bool {57	return (character >= `a` && character <= `z`) || (character >= `A` && character <= `Z`)58		|| (character >= `0` && character <= `9`) || character == `.` || character == `-`59}6061// file_version is the VERSION rule: the whole file, trimmed.62fn file_version(path string) ?string {63	text := os.read_file(path) or { return none }64	value := text.trim_space()65	if value == '' {66		return none67	}68	return value69}7071// line_version takes the value of the first line whose key starts it, in either72// quote. A line that builds its value from a variable has no literal, so this73// source yields nothing and the next one is tried.74fn line_version(path string, key string) ?string {75	text := os.read_file(path) or { return none }76	for line in text.split_into_lines() {77		trimmed := line.trim_left(' \t')78		if trimmed == '' || trimmed.starts_with('//') || trimmed.starts_with('#') {79			continue80		}81		if !trimmed.starts_with(key) {82			continue83		}84		if value := literal_after(trimmed, key.len) {85			return value86		}87	}88	return none89}9091// literal_after is the quoted literal that starts right after the key, an92// optional `=` and any spaces. That is what "assigned a quoted string literal"93// means: a call such as `gradleProperty("VERSION")` or a name such as94// `project.version` starts with something else, so it yields nothing and the95// next source is tried.96fn literal_after(line string, from int) ?string {97	mut at := from98	for at < line.len && line[at] in [` `, `\t`, `=`, `:`] {99		at++100	}101	if at >= line.len || (line[at] != `"` && line[at] != `'`) {102		return none103	}104	quote := line[at]105	for end in at + 1 .. line.len {106		if line[end] == quote {107			return line[at + 1..end]108		}109	}110	return none111}112113// plist_version reads CFBundleShortVersionString from an Info.plist: the value114// is in the <string> element after the key, not on the key's line.115fn plist_version(path string) ?string {116	text := os.read_file(path) or { return none }117	at := text.index('<key>CFBundleShortVersionString</key>') or { return none }118	rest := text[at..]119	open := rest.index('<string>') or { return none }120	close := rest.index('</string>') or { return none }121	if close <= open + '<string>'.len {122		return none123	}124	value := rest[open + '<string>'.len..close].trim_space()125	if value == '' {126		return none127	}128	return value129}130131// gradle_version is `versionName` assigned a quoted literal, in either the132// Kotlin (`= "2.17.43"`) or the Groovy (`"2.17.43"`) syntax. Commented-out lines133// are skipped and `versionNameSuffix` is not a versionName, as the spec says.134// Koh never runs Gradle: a value from a variable or `gradle.properties` is not135// read, and the next source is tried instead.136fn gradle_version(path string) ?string {137	text := os.read_file(path) or { return none }138	for line in text.split_into_lines() {139		trimmed := line.trim_left(' \t')140		if trimmed == '' || trimmed.starts_with('//') || trimmed.starts_with('/*')141			|| trimmed.starts_with('*') {142			continue143		}144		if !trimmed.starts_with('versionName') || trimmed.len <= 'versionName'.len145			|| (trimmed['versionName'.len] != ` ` && trimmed['versionName'.len] != `=`) {146			continue147		}148		if value := literal_after(trimmed, 'versionName'.len) {149			return value150		}151	}152	return none153}