version.v

5.1 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 := quoted_value(trimmed, key.len) {85			return value86		}87	}88	return none89}9091// quoted_value is the first quoted literal at or after `from` on a line. Only a92// literal counts: `versionName = project.version` yields nothing.93fn quoted_value(line string, from int) ?string {94	for at in from .. line.len {95		if line[at] != `"` && line[at] != `'` {96			continue97		}98		quote := line[at]99		for end in at + 1 .. line.len {100			if line[end] == quote {101				return line[at + 1..end]102			}103		}104		return none105	}106	return none107}108109// plist_version reads CFBundleShortVersionString from an Info.plist: the value110// is in the <string> element after the key, not on the key's line.111fn plist_version(path string) ?string {112	text := os.read_file(path) or { return none }113	at := text.index('<key>CFBundleShortVersionString</key>') or { return none }114	rest := text[at..]115	open := rest.index('<string>') or { return none }116	close := rest.index('</string>') or { return none }117	if close <= open + '<string>'.len {118		return none119	}120	value := rest[open + '<string>'.len..close].trim_space()121	if value == '' {122		return none123	}124	return value125}126127// gradle_version is `versionName` assigned a quoted literal, in either the128// Kotlin (`= "2.17.43"`) or the Groovy (`"2.17.43"`) syntax. Commented-out lines129// are skipped and `versionNameSuffix` is not a versionName, as the spec says.130// Koh never runs Gradle: a value from a variable or `gradle.properties` is not131// read, and the next source is tried instead.132fn gradle_version(path string) ?string {133	text := os.read_file(path) or { return none }134	for line in text.split_into_lines() {135		trimmed := line.trim_left(' \t')136		if trimmed == '' || trimmed.starts_with('//') || trimmed.starts_with('/*')137			|| trimmed.starts_with('*') {138			continue139		}140		if !trimmed.starts_with('versionName') || trimmed.len <= 'versionName'.len141			|| (trimmed['versionName'.len] != ` ` && trimmed['versionName'.len] != `=`) {142			continue143		}144		if value := quoted_value(trimmed, 'versionName'.len) {145			return value146		}147	}148	return none149}