12345678910module build1112import os131415const gradle_files = ['app/build.gradle.kts', 'app/build.gradle', 'build.gradle.kts', 'build.gradle']1617181920pub 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}46474849fn 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}606162fn 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}7071727374fn 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}90919293949596fn 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}112113114115fn 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}130131132133134135136fn 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}