#!/usr/bin/env ucode let fs = require("fs"); let common = require("core.common"); let as_string = common.as_string; let read_json_file = common.read_json_file; let read_stdin = common.read_stdin; let read_stdin_json = common.read_stdin_json; let write_json = common.write_json; function stdin_contains_ci(needle) { needle = lc(as_string(needle)); if (needle == "") return false; return index(lc(read_stdin()), needle) >= 0; } function stdin_first_line_last_field() { let data = read_stdin(); let newline = index(data, "\n"); let line = trim(newline >= 0 ? substr(data, 0, newline) : data); let fields = split(line, /[ \t\r\n]+/); if (length(fields) > 0) print(as_string(fields[length(fields) - 1]), "\n"); } let file_first_line = common.print_file_first_line; function json_file_field(path, key, fallback) { let value = read_json_file(path); if (type(value) == "object" && value[key] != null) print(as_string(value[key]), "\n"); else print(as_string(fallback), "\n"); } function object_get_default(key, fallback) { let value = read_stdin_json(); if (type(value) == "object" && value[key] != null) print(as_string(value[key]), "\n"); else print(as_string(fallback), "\n"); } function object_or_empty(value) { return type(value) == "object" ? value : {}; } function array_or_empty(value) { return type(value) == "array" ? value : []; } function releases_array_or_wrapped(value) { if (type(value) == "array") return value; if (type(value) == "object" && value != null) return [ value ]; return []; } function str_contains(haystack, needle) { return index(as_string(haystack), as_string(needle)) >= 0; } function str_startswith(value, prefix) { value = as_string(value); prefix = as_string(prefix); return substr(value, 0, length(prefix)) == prefix; } function str_endswith(value, suffix) { value = as_string(value); suffix = as_string(suffix); return length(value) >= length(suffix) && substr(value, length(value) - length(suffix)) == suffix; } function str_remove_suffix(value, suffix) { value = as_string(value); suffix = as_string(suffix); return str_endswith(value, suffix) ? substr(value, 0, length(value) - length(suffix)) : value; } function string_remove_suffix(value, suffix) { print(str_remove_suffix(value, suffix), "\n"); } function path_basename(value) { let parts = split(as_string(value), "/"); return length(parts) > 0 ? as_string(parts[length(parts) - 1]) : ""; } function arg_bool(value) { return value === true || value == "true" || value == "1" || value == 1; } function arg_number(value) { value = as_string(value); if (value == "" || match(value, /[^0-9-]/)) return 0; return int(value); } function file_json_valid(path) { return read_json_file(path) != null; } function github_response_ok() { let response = read_stdin_json(); if (response == null) return false; if (type(response) == "object") { let message = as_string(response.message || ""); if (match(message, /API rate limit/) || match(message, /rate limit exceeded/) || message == "Not Found") return false; } return true; } function release_by_tag(tag) { for (let release in array_or_empty(read_stdin_json())) { if (type(release) != "object") continue; if (release.draft === true || release.prerelease === true) continue; if (as_string(release.tag_name || "") == tag) { write_json(release); return; } } } function release_asset_name(prefix, ext) { let release = object_or_empty(read_stdin_json()); for (let asset in array_or_empty(release.assets)) { if (type(asset) != "object") continue; let name = as_string(asset.name || ""); if ((str_startswith(name, prefix + "_") || str_startswith(name, prefix + "-")) && str_endswith(name, "." + ext)) { print(name, "\n"); return; } } } function release_asset_url(name) { let release = object_or_empty(read_stdin_json()); for (let asset in array_or_empty(release.assets)) { if (type(asset) == "object" && as_string(asset.name || "") == name) { print(as_string(asset.browser_download_url || ""), "\n"); return; } } } function release_asset_name_by_suffix(suffix) { let release = object_or_empty(read_stdin_json()); for (let asset in array_or_empty(release.assets)) { let name = type(asset) == "object" ? as_string(asset.name || "") : ""; if (str_endswith(name, suffix)) { print(name, "\n"); return; } } } function release_asset_url_by_suffix_from_release(release, suffix) { suffix = as_string(suffix); for (let asset in array_or_empty(release.assets)) { if (type(asset) != "object") continue; let name = as_string(asset.name || ""); if (str_endswith(name, suffix)) return as_string(asset.browser_download_url || ""); } return ""; } function release_asset_url_by_suffix(suffix) { let release = object_or_empty(read_stdin_json()); let url = release_asset_url_by_suffix_from_release(release, suffix); if (url != "") print(url, "\n"); } function release_asset_pair(release, expected_name) { for (let asset in array_or_empty(release.assets)) { if (type(asset) != "object") continue; let name = as_string(asset.name || ""); let url = as_string(asset.browser_download_url || ""); if (name == as_string(expected_name) && url != "") return { name, url }; } return null; } function tachyon_release_plan(latest_version, asset_ext, i18n_required) { let release = object_or_empty(read_stdin_json()); if (as_string(release.tag_name || "") != as_string(latest_version)) exit(1); if (match(as_string(latest_version), /^[0-9]+[.][0-9]+[.][0-9]+$/) == null) exit(1); let backend = release_asset_pair(release, "tachyon_" + latest_version + "." + asset_ext); let app = release_asset_pair(release, "luci-app-tachyon_" + latest_version + "." + asset_ext); if (backend == null || app == null) exit(1); let i18n = { name: "", url: "" }; if (arg_bool(i18n_required)) { i18n = release_asset_pair(release, "luci-i18n-tachyon-ru_" + latest_version + "." + asset_ext); if (i18n == null) exit(1); } print(as_string(release.html_url || ""), "\t", backend.name, "\t", backend.url, "\t", app.name, "\t", app.url, "\t", i18n.name, "\t", i18n.url, "\n"); } function release_metadata_tsv() { let release = object_or_empty(read_stdin_json()); let tag = trim(as_string(release.tag_name || "")); if (substr(tag, 0, 1) == "v" || substr(tag, 0, 1) == "V") tag = substr(tag, 1); if (tag == "") return; print(tag, "\t", as_string(release.html_url || ""), "\n"); } function release_commit_sha_value(release) { release = object_or_empty(release); let sha = as_string(release.target_commitish || ""); if (sha != "" && match(sha, /^[0-9a-fA-F]{7,40}$/) != null) return sha; let body = as_string(release.body || ""); if (body != "") { // ucode's regex engine rejects non-capturing groups, so the two spellings // are matched separately rather than as one alternation. for (let pattern in [ /[Cc]ommit:?[ \t]*([0-9a-fA-F]{7,40})/, /[Ss][Hh][Aa]:?[ \t]*([0-9a-fA-F]{7,40})/, /\/commit\/([0-9a-fA-F]{7,40})/ ]) { let m = match(body, pattern); if (m && m[1]) return as_string(m[1]); } } return ""; } function commit_object_sha() { let obj = object_or_empty(read_stdin_json()); let sha = as_string(obj.sha || ""); if (sha != "" && match(sha, /^[0-9a-fA-F]{7,40}$/) != null) print(sha, "\n"); } function release_commit_sha() { let sha = release_commit_sha_value(read_stdin_json()); if (sha != "") print(sha, "\n"); } // A rebuild published under an existing tag keeps tag_name identical, so the tag // alone cannot tell two builds apart. Releases created before the workflow began // stamping "Commit: " into the body have no usable SHA either — for those // this fingerprint is the only signal that the assets were replaced. // // Preference order: the release SHA, then the publish/upload timestamps and the // size of the backend asset. Timestamps alone would miss a re-upload that keeps // published_at, and size alone collides across near-identical builds; together // they change whenever the artifacts do. function release_build_fingerprint_value(release) { release = object_or_empty(release); let sha = release_commit_sha_value(release); if (sha != "") return "sha:" + sha; let parts = []; let published = as_string(release.published_at || release.created_at || ""); if (published != "") push(parts, "pub=" + published); for (let asset in array_or_empty(release.assets)) { if (type(asset) != "object") continue; let name = as_string(asset.name || ""); if (!str_startswith(name, "tachyon_")) continue; let updated = as_string(asset.updated_at || asset.created_at || ""); if (updated != "") push(parts, "upd=" + updated); if (asset.size != null) push(parts, "size=" + as_string(asset.size)); break; } if (length(parts) == 0) return ""; return "build:" + join("|", parts); } function release_build_fingerprint() { let fingerprint = release_build_fingerprint_value(read_stdin_json()); if (fingerprint != "") print(fingerprint, "\n"); } // Decides whether the installed build differs from the published one. Exits 0 // when they differ (an update exists), 1 when they match or when there is not // enough information to tell — an unknown state must never be reported as an // available update, or the UI would offer an install that changes nothing. function tachyon_build_differs(local_sha, remote_sha, local_fingerprint, remote_fingerprint) { local_sha = trim(as_string(local_sha)); remote_sha = trim(as_string(remote_sha)); // Build.sh and the OpenWrt Makefile both stamp a short SHA, while the GitHub // release may carry the full one; compare on the shorter of the two. if (local_sha != "" && remote_sha != "") { let shorter = length(local_sha) < length(remote_sha) ? length(local_sha) : length(remote_sha); return substr(local_sha, 0, shorter) != substr(remote_sha, 0, shorter); } local_fingerprint = trim(as_string(local_fingerprint)); remote_fingerprint = trim(as_string(remote_fingerprint)); if (local_fingerprint != "" && remote_fingerprint != "") return local_fingerprint != remote_fingerprint; return false; } function openwrt_release_value(path, key) { let data = fs.readfile(path); if (data == null) return; let prefix = as_string(key) + "='"; for (let line in split(as_string(data), "\n")) { if (!str_startswith(line, prefix)) continue; let rest = substr(line, length(prefix)); let quote = rindex(rest, "'"); if (quote >= 0) print(substr(rest, 0, quote), "\n"); return; } } function openwrt_release_series(path) { let data = fs.readfile(path); if (data == null) return; let prefix = "DISTRIB_RELEASE='"; for (let line in split(as_string(data), "\n")) { if (!str_startswith(line, prefix)) continue; let value = substr(line, length(prefix)); let quote = rindex(value, "'"); if (quote >= 0) value = substr(value, 0, quote); let matched = match(value, /^([0-9]+\.[0-9]+)/); if (matched != null) print(matched[1], "\n"); return; } } function updates_arch_package_version(package_name, package_arch) { let version = str_remove_suffix(str_remove_suffix(package_name, ".ipk"), ".apk"); let prefixes = ["zapret2_", "zapret2-", "zapret_", "zapret-", "byedpi_", "byedpi-", "fptn-client-", "fptn-client_", "fptn_", "fptn-", "wdtt_", "wdtt-", "olcrtc_", "olcrtc-"]; for (let prefix in prefixes) { if (str_startswith(version, prefix)) { version = substr(version, length(prefix)); break; } } package_arch = as_string(package_arch); if (package_arch != "") { version = str_remove_suffix(version, "_" + package_arch); version = str_remove_suffix(version, "-" + package_arch); } print(version, "\n"); } function updates_bundle_version(bundle_name, prefixes) { let name = path_basename(bundle_name); for (let prefix in prefixes) { if (!str_startswith(name, prefix)) continue; let rest = substr(name, length(prefix)); let separator = index(rest, "_"); if (separator <= 0) continue; let version = substr(rest, 0, separator); if (str_startswith(version, "v")) version = substr(version, 1); print(version, "\n"); return; } print("\n"); } function updates_zapret_bundle_version(bundle_name) { updates_bundle_version(bundle_name, ["zapret_v", "zapret_"]); } function updates_zapret2_bundle_version(bundle_name) { updates_bundle_version(bundle_name, ["zapret2_v", "zapret2_", "zapret_v", "zapret_"]); } function first_version_token(value) { value = as_string(value); for (let i = 0; i < length(value); i++) { let chr = substr(value, i, 1); if (chr == " " || chr == "\t" || chr == "\r" || chr == "\n") return substr(value, 0, i); } return value; } function strip_plus_metadata(value) { value = as_string(value); let plus = index(value, "+"); return plus >= 0 ? substr(value, 0, plus) : value; } function strip_revision_suffix(value) { let matched = match(as_string(value), /^(.*)-r[0-9]+$/); return matched ? matched[1] : as_string(value); } function updates_normalize_sing_box_version(value) { value = as_string(value); if (str_startswith(value, "v")) value = substr(value, 1); print(first_version_token(strip_plus_metadata(value)), "\n"); } function updates_normalize_zapret_version(value) { value = as_string(value); if (str_startswith(value, "v")) value = substr(value, 1); value = strip_revision_suffix(value); value = strip_plus_metadata(value); print(first_version_token(value), "\n"); } function tachyon_normalized_release_version(value) { let str = trim(as_string(value)); if (substr(str, 0, 1) == "v" || substr(str, 0, 1) == "V") str = substr(str, 1); return str; } function tachyon_release_version_valid(value) { return match(as_string(value), /^[0-9]+[.][0-9]+[.][0-9]+$/) != null; } function tachyon_release_version_parts(value) { let version = tachyon_normalized_release_version(value); if (!tachyon_release_version_valid(version)) return null; let parts = split(version, "."); return [ int(parts[0]), int(parts[1]), int(parts[2]) ]; } function tachyon_release_version_compare(lhs, rhs) { let lhs_parts = tachyon_release_version_parts(lhs); let rhs_parts = tachyon_release_version_parts(rhs); if (lhs_parts == null || rhs_parts == null) return false; for (let i = 0; i < length(lhs_parts); i++) { if (lhs_parts[i] < rhs_parts[i]) { print("-1\n"); return true; } if (lhs_parts[i] > rhs_parts[i]) { print("1\n"); return true; } } print("0\n"); return true; } function fourth_whitespace_field(line) { let fields = split(trim(as_string(line)), /[ \t]+/); return length(fields) >= 4 ? as_string(fields[3]) : ""; } function updates_zip_inner_package_path(component, arch, ext) { component = as_string(component); arch = as_string(arch); ext = as_string(ext); let fallback = ""; for (let line in split(read_stdin(), "\n")) { let path = fourth_whitespace_field(line); if (path == "") continue; if (ext == "apk") { if (str_startswith(path, "apk/" + component + "-") && str_endswith(path, ".apk")) { print(path, "\n"); return; } continue; } if (ext == "ipk" && str_startswith(path, component + "_") && str_endswith(path, ".ipk")) { if (arch != "" && str_endswith(path, "_" + arch + ".ipk")) { print(path, "\n"); return; } if (fallback == "") fallback = path; } } if (fallback != "") print(fallback, "\n"); } function updates_archive_member_path(member_name) { member_name = as_string(member_name); for (let line in split(read_stdin(), "\n")) { let path = trim(as_string(line)); if (path != "" && path_basename(path) == member_name) { print(path, "\n"); return; } } } function updates_opkg_arch_list() { let arches = []; for (let line in split(read_stdin(), "\n")) { let fields = split(trim(as_string(line)), /[ \t]+/); if (length(fields) < 2 || as_string(fields[0]) != "arch") continue; let name = as_string(fields[1]); if (name == "all" || name == "noarch") continue; push(arches, { name, priority: int(length(fields) >= 3 ? as_string(fields[2]) : "") }); } for (let i = 1; i < length(arches); i++) { let item = arches[i]; let j = i - 1; while (j >= 0 && (arches[j].priority < item.priority || (arches[j].priority == item.priority && arches[j].name > item.name))) { arches[j + 1] = arches[j]; j--; } arches[j + 1] = item; } for (let arch in arches) print(arch.name, "\n"); } function append_unique(values, value) { value = as_string(value); if (value == "" || value == "all" || value == "noarch") return; for (let existing in values) if (existing == value) return; push(values, value); } function whitespace_fields(value) { value = as_string(value); let result = []; let start = -1; for (let i = 0; i < length(value); i++) { let chr = substr(value, i, 1); let whitespace = chr == " " || chr == "\t" || chr == "\r" || chr == "\n"; if (whitespace) { if (start >= 0) { push(result, substr(value, start, i - start)); start = -1; } } else if (start < 0) { start = i; } } if (start >= 0) push(result, substr(value, start)); return result; } function string_has_whitespace_field(value) { return length(whitespace_fields(value)) > 0; } function file_whitespace_list(path) { let data = fs.readfile(path); if (data == null) exit(1); print(join(" ", whitespace_fields(data)), "\n"); } function arch_candidate_variants(candidate) { let result = []; candidate = as_string(candidate); if (candidate == "") return result; append_unique(result, candidate); let plus = index(candidate, "+"); if (plus >= 0) append_unique(result, substr(candidate, 0, plus)); let suffixes = [ "_musl", "_uclibc", "_glibc", "-musl", "-uclibc", "-glibc", ".musl", ".uclibc", ".glibc" ]; for (let suffix in suffixes) if (str_endswith(candidate, suffix)) append_unique(result, substr(candidate, 0, length(candidate) - length(suffix))); return result; } function arch_candidate_bases(arch) { let result = []; arch = as_string(arch); push(result, arch); if (str_startswith(arch, "aarch64_")) { push(result, "aarch64_generic"); push(result, "aarch64_cortex-a53"); } else if (str_startswith(arch, "riscv64_")) push(result, "riscv64_generic"); else if (arch == "arm_cortex-a7_neon-vfpv4") { push(result, "arm_cortex-a7_vfpv4"); push(result, "arm_cortex-a7"); } else if (str_startswith(arch, "arm_cortex-a7_")) push(result, "arm_cortex-a7"); else if (str_startswith(arch, "arm_cortex-a9_")) push(result, "arm_cortex-a9"); else if (arch == "mipsel_24kc_24kf") push(result, "mipsel_24kc"); return result; } function updates_arch_candidates(arch_list) { let target = ""; let candidates = []; for (let arch in whitespace_fields(arch_list)) { arch = as_string(arch); if (arch == "" || arch == "all" || arch == "noarch") continue; if (target == "") target = arch; for (let base in arch_candidate_bases(arch)) for (let variant in arch_candidate_variants(base)) append_unique(candidates, variant); } if (target == "") exit(1); print(target, "\t", join(" ", candidates), "\n"); } function sing_box_extended_arch_suffix(host_arch, distrib_arch) { host_arch = as_string(host_arch); distrib_arch = as_string(distrib_arch); if (str_contains(distrib_arch, "mipsel") || str_contains(distrib_arch, "mipsle")) host_arch = "mipsel"; else if (str_contains(distrib_arch, "mips64el") || str_contains(distrib_arch, "mips64le")) host_arch = "mips64el"; if (host_arch == "aarch64") print("arm64\n"); else if (str_startswith(host_arch, "armv7")) print("armv7\n"); else if (str_startswith(host_arch, "armv6")) print("armv6\n"); else if (host_arch == "x86_64") print("amd64\n"); else if (host_arch == "i386" || host_arch == "i686") print("386\n"); else if (host_arch == "mips") print("mips-softfloat\n"); else if (host_arch == "mipsel" || host_arch == "mipsle") print("mipsle-softfloat\n"); else if (host_arch == "mips64") print("mips64\n"); else if (host_arch == "mips64el" || host_arch == "mips64le") print("mips64le\n"); else if (host_arch == "riscv64") print("riscv64\n"); else if (host_arch == "s390x") print("s390x\n"); else exit(1); } function sing_box_extended_asset_url(arch_suffix, _unused, compressed) { let release = object_or_empty(read_stdin_json()); if (as_string(compressed) != "1") exit(1); arch_suffix = as_string(arch_suffix); if (arch_suffix == "") exit(1); let url = release_asset_url_by_suffix_from_release(release, "linux-" + arch_suffix + "-compressed.tar.gz"); if (url != "") { print(url, "\n"); return; } exit(1); } function sing_box_lx_asset_url(arch_suffix) { let release = object_or_empty(read_stdin_json()); arch_suffix = as_string(arch_suffix); if (arch_suffix == "") exit(1); let url = release_asset_url_by_suffix_from_release(release, "linux-" + arch_suffix + ".tar.gz"); if (url != "") { print(url, "\n"); return; } exit(1); } function sing_box_extended_package_asset_url(distrib_arch, asset_ext) { let release = object_or_empty(read_stdin_json()); let suffix; distrib_arch = as_string(distrib_arch); asset_ext = as_string(asset_ext); if (distrib_arch == "" || asset_ext == "") exit(1); suffix = "_openwrt_" + distrib_arch + "." + asset_ext; for (let asset in array_or_empty(release.assets)) { if (type(asset) != "object") continue; let name = as_string(asset.name || ""); let url = as_string(asset.browser_download_url || ""); if (url != "" && str_startswith(name, "sing-box-extended_") && str_endswith(name, suffix)) { print(url, "\n"); return; } } exit(1); } function updates_opkg_package_installed(package_name) { package_name = as_string(package_name); for (let line in split(read_stdin(), "\n")) { let fields = split(trim(as_string(line)), /[ \t]+/); if (length(fields) >= 1 && as_string(fields[0]) == package_name) exit(0); } exit(1); } function updates_opkg_package_version(package_name) { package_name = as_string(package_name); for (let line in split(read_stdin(), "\n")) { let fields = split(trim(as_string(line)), /[ \t]+/); if (length(fields) >= 3 && as_string(fields[0]) == package_name && as_string(fields[1]) == "-") { print(as_string(fields[2]), "\n"); return; } } } function updates_apk_manifest_package_version(package_name) { package_name = as_string(package_name); for (let line in split(read_stdin(), "\n")) { let fields = split(trim(as_string(line)), /[ \t]+/); if (length(fields) >= 2 && as_string(fields[0]) == package_name) { print(as_string(fields[1]), "\n"); return; } } } function updates_apk_info_package_version(package_name) { package_name = as_string(package_name); let prefix = package_name + "-"; let first = split(read_stdin(), "\n")[0]; first = as_string(first); if (!str_startswith(first, prefix)) return; let version = substr(first, length(prefix)); version = replace(version, /[ \t].*$/, ""); if (version != "") print(version, "\n"); } function updates_apk_policy_version() { for (let line in split(read_stdin(), "\n")) { line = as_string(line); if (match(line, /^ [^ \t][^ \t]*:/) == null) continue; let fields = split(trim(line), /[ \t]+/); if (length(fields) == 0) return; print(str_remove_suffix(as_string(fields[0]), ":"), "\n"); return; } } function byedpi_asset_matches(name, arch, ext) { return (str_startswith(name, "byedpi_") || str_startswith(name, "byedpi-")) && str_endswith(name, "." + ext) && (str_contains(name, "_" + arch + "." + ext) || str_contains(name, "-" + arch + "." + ext)); } function release_asset_matches_arch(name, prefix, arch, ext) { return (str_startswith(name, prefix + "_") || str_startswith(name, prefix + "-")) && str_endswith(name, "." + ext) && (str_contains(name, "_" + arch + "." + ext) || str_contains(name, "-" + arch + "." + ext)); } function named_release_select_asset(release_prefix, asset_prefix, asset_ext, arch_candidates) { let releases = releases_array_or_wrapped(read_stdin_json()); let clean_prefix = trim(release_prefix); for (let release in releases) { if (type(release) != "object") continue; if (release.draft === true) continue; let release_name = as_string(release.name || ""); let tag_name = as_string(release.tag_name || ""); let name_match = release_name != "" && ( str_startswith(release_name, release_prefix) || str_startswith(release_name, clean_prefix + "-") || str_startswith(release_name, clean_prefix + "_") || str_startswith(release_name, clean_prefix) ); let tag_match = tag_name != "" && ( str_startswith(tag_name, release_prefix) || str_startswith(tag_name, clean_prefix + "-") || str_startswith(tag_name, clean_prefix + "_") || str_startswith(tag_name, clean_prefix) ); if (!name_match && !tag_match) continue; for (let arch in split(as_string(arch_candidates), " ")) { if (arch == "") continue; for (let asset in array_or_empty(release.assets)) { if (type(asset) != "object") continue; let name = as_string(asset.name || ""); let url = as_string(asset.browser_download_url || ""); if (url != "" && release_asset_matches_arch(name, asset_prefix, arch, asset_ext)) { print(arch, "\t", name, "\t", url, "\t", as_string(release.html_url || ""), "\t", as_string(release.tag_name || ""), "\n"); return; } } } } } function release_select_arch_suffix_asset(asset_ext, arch_candidates) { let release = object_or_empty(read_stdin_json()); for (let arch in split(as_string(arch_candidates), " ")) { if (arch == "") continue; let suffix = "_" + arch + "." + as_string(asset_ext); for (let asset in array_or_empty(release.assets)) { if (type(asset) != "object") continue; let name = as_string(asset.name || ""); let url = as_string(asset.browser_download_url || ""); if (url != "" && str_endswith(name, suffix)) { print(arch, "\t", name, "\t", url, "\t", as_string(release.html_url || ""), "\t", as_string(release.tag_name || ""), "\n"); return; } } } } function select_byedpi_asset_from_release(release, asset_ext, arch_candidates) { for (let arch in split(as_string(arch_candidates), " ")) { if (arch == "") continue; for (let asset in array_or_empty(release.assets)) { if (type(asset) != "object") continue; let name = as_string(asset.name || ""); let url = as_string(asset.browser_download_url || ""); if (url != "" && byedpi_asset_matches(name, arch, asset_ext)) { print(arch, "\t", name, "\t", url, "\t", as_string(release.html_url || ""), "\n"); return true; } } } return false; } function byedpi_select_asset(series, asset_ext, arch_candidates) { let releases = releases_array_or_wrapped(read_stdin_json()); for (let pass = 0; pass < 2; pass++) { if (pass == 0 && as_string(series) == "") continue; for (let release in releases) { if (type(release) != "object") continue; if (release.draft === true || (length(releases) > 1 && release.prerelease === true)) continue; if (pass == 0 && length(releases) > 1) { let tag = as_string(release.tag_name || ""); let name = as_string(release.name || ""); if (!str_contains(tag, series) && !str_contains(name, series)) continue; } if (select_byedpi_asset_from_release(release, asset_ext, arch_candidates)) return; } } } function fptn_asset_matches(name, series, arch, ext) { if (!str_startswith(name, "fptn-client-") && !str_startswith(name, "fptn-")) return false; if (!str_endswith(name, "." + ext)) return false; if (series != "" && !str_contains(name, series)) return false; return str_contains(name, "-" + arch + "." + ext) || str_contains(name, "_" + arch + "." + ext); } function select_fptn_asset_from_release(release, series, asset_ext, arch_candidates) { for (let arch in split(as_string(arch_candidates), " ")) { if (arch == "") continue; for (let asset in array_or_empty(release.assets)) { if (type(asset) != "object") continue; let name = as_string(asset.name || ""); let url = as_string(asset.browser_download_url || ""); if (url != "" && fptn_asset_matches(name, series, arch, asset_ext)) { print(arch, "\t", name, "\t", url, "\t", as_string(release.html_url || ""), "\n"); return true; } } } return false; } function fptn_select_asset(series, asset_ext, arch_candidates) { let releases = releases_array_or_wrapped(read_stdin_json()); for (let pass = 0; pass < 2; pass++) { let current_series = pass == 0 ? as_string(series) : ""; for (let release in releases) { if (type(release) != "object") continue; if (release.draft === true || (length(releases) > 1 && release.prerelease === true)) continue; if (select_fptn_asset_from_release(release, current_series, asset_ext, arch_candidates)) return; } } } function wdtt_select_asset(series, asset_ext, arch_candidates) { let releases = releases_array_or_wrapped(read_stdin_json()); for (let pass = 0; pass < 2; pass++) { if (pass == 0 && as_string(series) == "") continue; for (let release in releases) { if (type(release) != "object") continue; if (release.draft === true || (length(releases) > 1 && release.prerelease === true)) continue; if (pass == 0 && length(releases) > 1) { let tag = as_string(release.tag_name || ""); let name = as_string(release.name || ""); if (!str_contains(tag, series) && !str_contains(name, series)) continue; } for (let arch in split(as_string(arch_candidates), " ")) { if (arch == "") continue; for (let asset in array_or_empty(release.assets)) { if (type(asset) != "object") continue; let name = as_string(asset.name || ""); let url = as_string(asset.browser_download_url || ""); if (url != "" && release_asset_matches_arch(name, "wdtt", arch, asset_ext)) { print(arch, "\t", name, "\t", url, "\t", as_string(release.html_url || ""), "\n"); return; } } } } } } function olcrtc_select_asset(series, asset_ext, arch_candidates) { let releases = releases_array_or_wrapped(read_stdin_json()); for (let pass = 0; pass < 2; pass++) { if (pass == 0 && as_string(series) == "") continue; for (let release in releases) { if (type(release) != "object") continue; if (release.draft === true || (length(releases) > 1 && release.prerelease === true)) continue; if (pass == 0 && length(releases) > 1) { let tag = as_string(release.tag_name || ""); let name = as_string(release.name || ""); if (!str_contains(tag, series) && !str_contains(name, series)) continue; } for (let arch in split(as_string(arch_candidates), " ")) { if (arch == "") continue; for (let asset in array_or_empty(release.assets)) { if (type(asset) != "object") continue; let name = as_string(asset.name || ""); let url = as_string(asset.browser_download_url || ""); if (url != "" && release_asset_matches_arch(name, "olcrtc", arch, asset_ext)) { print(arch, "\t", name, "\t", url, "\t", as_string(release.html_url || ""), "\n"); return; } } } } } } function sing_box_extended_release_tag() { for (let release in releases_array_or_wrapped(read_stdin_json())) { if (type(release) != "object") continue; if (release.draft === true || release.prerelease === true) continue; let tag = as_string(release.tag_name || ""); let lowered = lc(tag); if (tag != "" && !str_contains(lowered, "alpha") && !str_contains(lowered, "beta") && !str_contains(lowered, "rc")) { print(tag, "\n"); return; } } } function sing_box_lx_release_tag() { for (let release in releases_array_or_wrapped(read_stdin_json())) { if (type(release) != "object") continue; if (release.draft === true || release.prerelease === true) continue; let tag = as_string(release.tag_name || ""); let lowered = lc(tag); if (tag != "" && str_contains(lowered, "-lx") && !str_contains(lowered, "alpha") && !str_contains(lowered, "beta") && !str_contains(lowered, "rc")) { print(tag, "\n"); return; } } } function text_first_chars(value, max_chars) { value = as_string(value); max_chars = int(max_chars || "0", 10) || 0; return max_chars > 0 && length(value) > max_chars ? substr(value, 0, max_chars) : value; } function file_last_nonblank_line(path, fallback, max_chars) { let data = fs.readfile(path); if (data == null || trim(as_string(data)) == "") { print(text_first_chars(fallback, max_chars), "\n"); return; } let raw_lines = split(as_string(data), /\r?\n/); let lines = []; for (let l in raw_lines) { let t = trim(l); if (t != "") push(lines, t); } if (length(lines) == 0) { print(text_first_chars(fallback, max_chars), "\n"); return; } // 1. Detect ucode runtime/syntax exception let first_line = lines[0]; let lower_first = lc(first_line); if (index(lower_first, "type error:") == 0 || index(lower_first, "syntax error:") == 0 || index(lower_first, "reference error:") == 0 || index(lower_first, "runtime error:") == 0 || index(lower_first, "unhandled exception:") == 0 || index(lower_first, "error:") == 0) { let loc = ""; if (length(lines) > 1 && (index(lines[1], "In ") == 0 || index(lines[1], "in ") == 0)) { let m = match(lines[1], /([a-zA-Z0-9_.-]+),[ \t]*line[ \t]*([0-9]+)/); if (m) loc = " (" + m[1] + ":" + m[2] + ")"; } let msg = first_line + loc; print(text_first_chars(msg, max_chars), "\n"); return; } // 2. Discard trailing pointer / caret / delimiter lines while (length(lines) > 0) { let last = lines[length(lines) - 1]; if (index(last, "Near here") >= 0 || match(last, /^[ \t\-_^]*$/) || match(last, /^[-=~_]{4,}$/)) { pop(lines); } else { break; } } if (length(lines) == 0) { print(text_first_chars(fallback, max_chars), "\n"); return; } // 3. Check if any line has an explicit error prefix for (let l in lines) { let lower_l = lc(l); if (index(lower_l, "error:") >= 0 || index(lower_l, "fatal:") >= 0 || index(lower_l, "failed:") >= 0 || index(lower_l, "failure:") >= 0) { print(text_first_chars(l, max_chars), "\n"); return; } } // 4. Fallback to the last non-empty line let result = lines[length(lines) - 1]; print(text_first_chars(result, max_chars), "\n"); } function file_flat_snippet(path, max_chars) { let data = fs.readfile(path); if (data == null) return; print(text_first_chars(replace(as_string(data), /\n/g, " "), max_chars), "\n"); } function file_tail_json_object(path) { let data = fs.readfile(path); let result = ""; if (data != null) { for (let line in split(as_string(data), "\n")) { let start = index(line, "{"); if (start >= 0) result = substr(line, start); } } if (result != "") print(result, "\n"); } function job_running_is(path, expected) { let value = read_json_file(path); let running = type(value) == "object" && value.running === true; return running == arg_bool(expected); } function updates_json_response(success, component, action, message, current_version, latest_version, changed, status, release_url) { write_json({ success: arg_bool(success), kind: "component", component: as_string(component), action: as_string(action), message: as_string(message), current_version: as_string(current_version), latest_version: as_string(latest_version), changed: arg_number(changed), status: as_string(status), release_url: as_string(release_url) }); } function updates_status_from_compare(compare_result) { compare_result = as_string(compare_result); if (compare_result == "-1") print("outdated\n"); else if (compare_result == "0") print("latest\n"); else if (compare_result == "1") print("dev\n"); else exit(1); } function updates_check_result_row(component, current_version, latest_version, status) { component = as_string(component); current_version = as_string(current_version); latest_version = as_string(latest_version); status = as_string(status); if (status == "latest") { print("Latest version is installed\t", component, " is up to date (", current_version, ")\n"); return; } if (status == "outdated") { print("Update is available\t", component, " update is available: ", current_version, " -> ", latest_version, "\n"); return; } if (status == "outdated_same_release") { print("Update is available for current release\t", component, " update is available for current release: ", current_version, "\n"); return; } if (status == "dev") { print("Installed version is newer than release\t", component, " installed version is newer than upstream release: ", current_version, " -> ", latest_version, "\n"); return; } exit(1); } function updates_job_json_response(success, job_id, message) { write_json({ success: arg_bool(success), job_id: as_string(job_id), message: as_string(message) }); } function updates_job_state_path(job_dir, job_id) { job_dir = as_string(job_dir); job_id = as_string(job_id); if (job_id == "" || job_id == "." || job_id == ".." || match(job_id, /[^A-Za-z0-9._-]/) != null) exit(1); print(job_dir, "/", job_id, ".json\n"); } function updates_running_job_state(component, action, pid, started_at) { pid = as_string(pid); write_json({ success: true, running: true, kind: "component", component: as_string(component), action: as_string(action), message: "Component action is running", pid: pid != "" ? pid : null, started_at: arg_number(started_at), updated_at: null, current_version: "", latest_version: "", changed: 0, status: "", exit_code: null }); } function job_started_at_within_grace(value, now, grace_seconds) { let started_at = arg_number(value); now = arg_number(now); grace_seconds = arg_number(grace_seconds); if (started_at <= 0 || now <= 0) return false; return now - started_at < grace_seconds; } function job_pid_valid(pid) { pid = as_string(pid); return pid != "" && match(pid, /^[0-9]+$/) != null; } function updates_job_refresh_plan(path, now, grace_seconds) { let value = read_json_file(path); if (type(value) != "object" || value.running !== true) { print("skip\n"); return; } let within_grace = job_started_at_within_grace(value.started_at, now, grace_seconds); let pid = as_string(value.pid || ""); if (!job_pid_valid(pid)) { print(within_grace ? "skip\n" : "stale\n"); return; } print("pid\t", pid, "\t", within_grace ? "0" : "1", "\n"); } function updates_set_running_job_pid(path, pid) { let value = object_or_empty(read_json_file(path)); if (value.running === true) value.pid = as_string(pid); write_json(value); } function updates_mark_stale_job_state(path) { let value = object_or_empty(read_json_file(path)); if (value.running === true) { value.success = false; value.running = false; value.message = "Component action job is stale or the worker process exited unexpectedly"; value.changed = 0; value.status = ""; value.exit_code = null; } write_json(value); } function updates_finish_job_state(path, exit_code, updated_at) { let value = read_json_file(path); if (value == null) exit(1); value.running = false; value.kind = "component"; value.exit_code = arg_number(exit_code); value.updated_at = arg_number(updated_at); write_json(value); } function updates_fallback_job_state(component, action, message, exit_code, updated_at) { write_json({ success: false, running: false, kind: "component", component: as_string(component), action: as_string(action), message: as_string(message), current_version: "", latest_version: "", changed: 0, status: "", exit_code: arg_number(exit_code), updated_at: arg_number(updated_at) }); } let mode = ARGV[0] || ""; if (mode == "file-json-valid") exit(file_json_valid(ARGV[1]) ? 0 : 1); else if (mode == "file-first-line") file_first_line(ARGV[1]); else if (mode == "json-file-field") json_file_field(ARGV[1], ARGV[2], ARGV[3]); else if (mode == "object-get-default") object_get_default(ARGV[1], ARGV[2]); else if (mode == "string-remove-suffix") string_remove_suffix(ARGV[1], ARGV[2]); else if (mode == "github-response-ok") exit(github_response_ok() ? 0 : 1); else if (mode == "release-by-tag") release_by_tag(ARGV[1]); else if (mode == "release-asset-name") release_asset_name(ARGV[1], ARGV[2]); else if (mode == "release-asset-url") release_asset_url(ARGV[1]); else if (mode == "release-asset-name-by-suffix") release_asset_name_by_suffix(ARGV[1]); else if (mode == "release-asset-url-by-suffix") release_asset_url_by_suffix(ARGV[1]); else if (mode == "tachyon-release-plan") tachyon_release_plan(ARGV[1], ARGV[2], ARGV[3]); else if (mode == "release-metadata-tsv") release_metadata_tsv(); else if (mode == "release-commit-sha") release_commit_sha(); else if (mode == "release-build-fingerprint") release_build_fingerprint(); else if (mode == "tachyon-build-differs") exit(tachyon_build_differs(ARGV[1], ARGV[2], ARGV[3], ARGV[4]) ? 0 : 1); else if (mode == "openwrt-release-value") openwrt_release_value(ARGV[1], ARGV[2]); else if (mode == "openwrt-release-series") openwrt_release_series(ARGV[1]); else if (mode == "stdin-contains-ci") exit(stdin_contains_ci(ARGV[1]) ? 0 : 1); else if (mode == "stdin-first-line-last-field") stdin_first_line_last_field(); else if (mode == "updates-arch-package-version") updates_arch_package_version(ARGV[1], ARGV[2]); else if (mode == "updates-zapret-bundle-version") updates_zapret_bundle_version(ARGV[1]); else if (mode == "updates-zapret2-bundle-version") updates_zapret2_bundle_version(ARGV[1]); else if (mode == "updates-normalize-sing-box-version") updates_normalize_sing_box_version(ARGV[1]); else if (mode == "updates-normalize-zapret-version") updates_normalize_zapret_version(ARGV[1]); else if (mode == "tachyon-release-version-valid") exit(tachyon_release_version_valid(ARGV[1]) ? 0 : 1); else if (mode == "tachyon-release-version-compare") exit(tachyon_release_version_compare(ARGV[1], ARGV[2]) ? 0 : 1); else if (mode == "updates-zip-inner-package-path") updates_zip_inner_package_path(ARGV[1], ARGV[2], ARGV[3]); else if (mode == "updates-archive-member-path") updates_archive_member_path(ARGV[1]); else if (mode == "updates-opkg-arch-list") updates_opkg_arch_list(); else if (mode == "updates-arch-candidates") updates_arch_candidates(ARGV[1]); else if (mode == "string-has-whitespace-field") exit(string_has_whitespace_field(ARGV[1]) ? 0 : 1); else if (mode == "file-whitespace-list") file_whitespace_list(ARGV[1]); else if (mode == "sing-box-extended-arch-suffix") sing_box_extended_arch_suffix(ARGV[1], ARGV[2]); else if (mode == "sing-box-extended-asset-url") sing_box_extended_asset_url(ARGV[1], ARGV[2], ARGV[3]); else if (mode == "sing-box-lx-asset-url") sing_box_lx_asset_url(ARGV[1]); else if (mode == "sing-box-extended-package-asset-url") sing_box_extended_package_asset_url(ARGV[1], ARGV[2]); else if (mode == "updates-opkg-package-installed") updates_opkg_package_installed(ARGV[1]); else if (mode == "updates-opkg-package-version") updates_opkg_package_version(ARGV[1]); else if (mode == "updates-apk-manifest-package-version") updates_apk_manifest_package_version(ARGV[1]); else if (mode == "updates-apk-info-package-version") updates_apk_info_package_version(ARGV[1]); else if (mode == "updates-apk-policy-version") updates_apk_policy_version(); else if (mode == "named-release-select-asset") named_release_select_asset(ARGV[1], ARGV[2], ARGV[3], ARGV[4]); else if (mode == "release-select-arch-suffix-asset") release_select_arch_suffix_asset(ARGV[1], ARGV[2]); else if (mode == "byedpi-select-asset") byedpi_select_asset(ARGV[1], ARGV[2], ARGV[3]); else if (mode == "wdtt-select-asset") wdtt_select_asset(ARGV[1], ARGV[2], ARGV[3]); else if (mode == "olcrtc-select-asset") olcrtc_select_asset(ARGV[1], ARGV[2], ARGV[3]); else if (mode == "fptn-select-asset") fptn_select_asset(ARGV[1], ARGV[2], ARGV[3]); else if (mode == "sing-box-extended-release-tag") sing_box_extended_release_tag(); else if (mode == "sing-box-lx-release-tag") sing_box_lx_release_tag(); else if (mode == "file-last-nonblank-line") file_last_nonblank_line(ARGV[1], ARGV[2], ARGV[3]); else if (mode == "file-flat-snippet") file_flat_snippet(ARGV[1], ARGV[2]); else if (mode == "file-tail-json-object") file_tail_json_object(ARGV[1]); else if (mode == "job-running-is") exit(job_running_is(ARGV[1], ARGV[2]) ? 0 : 1); else if (mode == "updates-json-response") updates_json_response(ARGV[1], ARGV[2], ARGV[3], ARGV[4], ARGV[5], ARGV[6], ARGV[7], ARGV[8], ARGV[9]); else if (mode == "updates-status-from-compare") updates_status_from_compare(ARGV[1]); else if (mode == "updates-check-result-row") updates_check_result_row(ARGV[1], ARGV[2], ARGV[3], ARGV[4]); else if (mode == "updates-job-json-response") updates_job_json_response(ARGV[1], ARGV[2], ARGV[3]); else if (mode == "updates-job-state-path") updates_job_state_path(ARGV[1], ARGV[2]); else if (mode == "updates-running-job-state") updates_running_job_state(ARGV[1], ARGV[2], ARGV[3], ARGV[4]); else if (mode == "updates-job-refresh-plan") updates_job_refresh_plan(ARGV[1], ARGV[2], ARGV[3]); else if (mode == "updates-set-running-job-pid") updates_set_running_job_pid(ARGV[1], ARGV[2]); else if (mode == "updates-mark-stale-job-state") updates_mark_stale_job_state(ARGV[1]); else if (mode == "updates-finish-job-state") updates_finish_job_state(ARGV[1], ARGV[2], ARGV[3]); else if (mode == "updates-fallback-job-state") updates_fallback_job_state(ARGV[1], ARGV[2], ARGV[3], ARGV[4], ARGV[5]); else if (mode == "commit-object-sha") commit_object_sha(); else { warn("Usage: components/updater.uc ...\n"); exit(1); }