Some checks failed
Periodic Merges (6h) / master → staging-nixos (push) Failing after 12m50s
Periodic Merges (6h) / master → staging-next (push) Failing after 12m54s
Periodic Merges (24h) / merge-base(master,staging) → haskell-updates (push) Failing after 11m54s
Periodic Merges (6h) / staging-next → staging (push) Failing after 12m13s
Periodic Merges (24h) / staging-next-25.05 → staging-25.05 (push) Failing after 13m24s
Periodic Merges (24h) / release-25.05 → staging-next-25.05 (push) Failing after 14m28s
441 lines
16 KiB
Python
Executable File
441 lines
16 KiB
Python
Executable File
#! /usr/bin/env nix-shell
|
|
#! nix-shell -i python3 -p nix python3 python3Packages.loguru nodePackages.semver vsce nix-update gitMinimal coreutils common-updater-scripts
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
from loguru import logger
|
|
|
|
|
|
class VSCodeExtensionUpdater:
|
|
"""
|
|
A class to update VSCode extension version.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.parser = argparse.ArgumentParser(
|
|
description="Update VSCode extension version."
|
|
)
|
|
self.parser.add_argument(
|
|
"attribute",
|
|
nargs="?",
|
|
default=os.getenv("UPDATE_NIX_ATTR_PATH"),
|
|
help="nix attribute path of the extension",
|
|
)
|
|
self.parser.add_argument(
|
|
"--override-filename", help="override-filename for nix-update"
|
|
)
|
|
self.parser.add_argument(
|
|
"--pre-release",
|
|
action="store_true",
|
|
help="allow check pre-release versions",
|
|
)
|
|
self.parser.add_argument(
|
|
"--commit", action="store_true", help="commit the updated package"
|
|
)
|
|
self.args = self.parser.parse_args()
|
|
self.attribute_path = self.args.attribute
|
|
if not self.attribute_path:
|
|
logger.error("Error: Attribute path is required.")
|
|
sys.exit(1)
|
|
self.target_vscode_version = self._get_nix_vscode_version()
|
|
self.current_version = self._get_nix_vscode_extension_version()
|
|
self.override_filename = self.args.override_filename
|
|
self.allow_pre_release = self.args.pre_release
|
|
self.commit = self.args.commit
|
|
self.extension_publisher = self._get_nix_vscode_extension_publisher()
|
|
self.extension_name = self._get_nix_vscode_extension_name()
|
|
self.extension_marketplace_id = (
|
|
f"{self.extension_publisher}.{self.extension_name}"
|
|
)
|
|
self.nix_system = self.get_nix_system()
|
|
nix_vscode_extension_platforms = self._get_nix_vscode_extension_platforms()
|
|
if not nix_vscode_extension_platforms and self._has_platform_source():
|
|
logger.error("Error: not found meta.platforms.")
|
|
sys.exit(1)
|
|
self.nix_vscode_extension_platforms = nix_vscode_extension_platforms or [
|
|
self.nix_system
|
|
]
|
|
if self.nix_system in self.nix_vscode_extension_platforms:
|
|
self.nix_vscode_extension_platforms.remove(self.nix_system)
|
|
self.nix_vscode_extension_platforms.insert(0, self.nix_system)
|
|
self.supported_nix_systems = self.get_supported_nix_systems()
|
|
logger.info(f"VSCode version: {self.target_vscode_version}")
|
|
logger.info(f"Extension Marketplace ID: {self.extension_marketplace_id}")
|
|
logger.info(f"Extension Current Version: {self.current_version}")
|
|
|
|
def execute_command(
|
|
self, commandline: list[str], env: Optional[dict[str, str]] = None
|
|
) -> str:
|
|
"""
|
|
Executes a shell command and returns its output.
|
|
"""
|
|
logger.debug("Executing command: {}", commandline)
|
|
return subprocess.run(
|
|
commandline,
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
env=env,
|
|
).stdout.strip()
|
|
|
|
def _get_nix_attribute(self, attribute_path: str) -> str:
|
|
"""
|
|
Retrieves a raw Nix attribute value.
|
|
"""
|
|
return self.execute_command([
|
|
"nix",
|
|
"--extra-experimental-features",
|
|
"nix-command",
|
|
"eval",
|
|
"--raw",
|
|
"-f",
|
|
".",
|
|
attribute_path
|
|
])
|
|
|
|
def get_nix_system(self) -> str:
|
|
"""
|
|
Retrieves system from Nix.
|
|
"""
|
|
return self._get_nix_attribute("stdenv.hostPlatform.system")
|
|
|
|
def get_supported_nix_systems(self) -> list[str]:
|
|
nix_config = self.execute_command([
|
|
"nix",
|
|
"--extra-experimental-features",
|
|
"nix-command",
|
|
"config",
|
|
"show"
|
|
])
|
|
system = None
|
|
extra_platforms = []
|
|
for line in nix_config.splitlines():
|
|
if "=" not in line:
|
|
continue
|
|
key, value = line.split("=", 1)
|
|
key = key.strip()
|
|
value = value.strip()
|
|
if key == "system":
|
|
system = value
|
|
elif key == "extra-platforms":
|
|
extra_platforms = value.strip("[]").replace('"', "").split()
|
|
return ([system] if system is not None else []) + extra_platforms
|
|
|
|
def _has_platform_source(self) -> bool:
|
|
source_url = self._get_nix_attribute(f"{self.attribute_path}.src.url")
|
|
return "targetPlatform=" in source_url
|
|
|
|
def _get_nix_vscode_extension_src_hash(self, system: str) -> str:
|
|
url = self.execute_command([
|
|
"nix",
|
|
"--extra-experimental-features",
|
|
"nix-command",
|
|
"eval",
|
|
"--raw",
|
|
"-f",
|
|
".",
|
|
f"{self.attribute_path}.src.url",
|
|
"--system",
|
|
system,
|
|
])
|
|
sha256 = self.execute_command(["nix-prefetch-url", url])
|
|
return self.execute_command([
|
|
"nix",
|
|
"--extra-experimental-features",
|
|
"nix-command",
|
|
"hash",
|
|
"convert",
|
|
"--to",
|
|
"sri",
|
|
"--hash-algo",
|
|
"sha256",
|
|
sha256,
|
|
])
|
|
|
|
def get_target_platform(self, nix_system: str) -> str:
|
|
"""
|
|
Retrieves the VS Code targetPlatform variable based on the Nix system.
|
|
"""
|
|
platform_mapping = {
|
|
"x86_64-linux": "linux-x64",
|
|
"aarch64-linux": "linux-arm64",
|
|
"armv7l-linux": "linux-armhf",
|
|
"x86_64-darwin": "darwin-x64",
|
|
"aarch64-darwin": "darwin-arm64",
|
|
"x86_64-windows": "win32-x64",
|
|
"aarch64-windows": "win32-arm64",
|
|
}
|
|
try:
|
|
return platform_mapping[nix_system]
|
|
except KeyError:
|
|
logger.error(
|
|
f"Error: Unknown Nix system '{nix_system}'. Cannot determine targetPlatform."
|
|
)
|
|
sys.exit(1)
|
|
|
|
def _get_nix_vscode_version(self) -> str:
|
|
"""
|
|
Retrieves the current VSCode version from Nix.
|
|
"""
|
|
return self._get_nix_attribute("vscode.version")
|
|
|
|
def _get_nix_vscode_extension_version(self) -> str:
|
|
"""
|
|
Retrieves the extension current version from Nix.
|
|
"""
|
|
return os.getenv("UPDATE_NIX_OLD_VERSION") or self._get_nix_attribute(
|
|
f"{self.attribute_path}.version"
|
|
)
|
|
|
|
def _get_nix_vscode_extension_platforms(self) -> list[str]:
|
|
"""
|
|
Retrieves the extension meta.platforms from Nix.
|
|
"""
|
|
try:
|
|
return json.loads(
|
|
self.execute_command([
|
|
"nix",
|
|
"--extra-experimental-features",
|
|
"nix-command",
|
|
"eval",
|
|
"--json",
|
|
"-f",
|
|
".",
|
|
f"{self.attribute_path}.meta.platforms",
|
|
])
|
|
)
|
|
except subprocess.CalledProcessError:
|
|
return []
|
|
|
|
def _get_nix_vscode_extension_publisher(self) -> str:
|
|
"""
|
|
Retrieves the extension publisher from Nix.
|
|
"""
|
|
return self._get_nix_attribute(f"{self.attribute_path}.vscodeExtPublisher")
|
|
|
|
def _get_nix_vscode_extension_name(self) -> str:
|
|
"""
|
|
Retrieves the extension name from Nix.
|
|
"""
|
|
return self._get_nix_attribute(f"{self.attribute_path}.vscodeExtName")
|
|
|
|
def get_marketplace_extension_data(self, extension_id: str) -> dict:
|
|
"""
|
|
Retrieves extension data from the VSCode Marketplace using vsce.
|
|
"""
|
|
command = ["vsce", "show", extension_id, "--json"]
|
|
try:
|
|
output = self.execute_command(command)
|
|
return json.loads(output)
|
|
except (json.JSONDecodeError, subprocess.CalledProcessError) as e:
|
|
logger.exception(e)
|
|
sys.exit(1)
|
|
|
|
def find_compatible_extension_version(
|
|
self, extension_versions: list, target_platform: str
|
|
) -> str:
|
|
"""
|
|
Finds the first compatible extension version based on Target Platform and VSCode compatibility.
|
|
"""
|
|
for version_info in extension_versions:
|
|
candidate_platform = version_info.get("targetPlatform", None)
|
|
if candidate_platform is not None and candidate_platform != target_platform:
|
|
continue
|
|
candidate_version = version_info.get("version")
|
|
candidate_pre_release = next(
|
|
(
|
|
prop.get("value")
|
|
for prop in version_info.get("properties", [])
|
|
if prop.get("key") == "Microsoft.VisualStudio.Code.PreRelease"
|
|
),
|
|
None,
|
|
)
|
|
if candidate_pre_release and not self.allow_pre_release:
|
|
logger.debug(f"Skipping PreRelease version {candidate_version}")
|
|
continue
|
|
engine_version_constraint = next(
|
|
(
|
|
prop.get("value")
|
|
for prop in version_info.get("properties", [])
|
|
if prop.get("key") == "Microsoft.VisualStudio.Code.Engine"
|
|
),
|
|
None,
|
|
)
|
|
if engine_version_constraint:
|
|
logger.debug(
|
|
f"Testing extension version: {candidate_version} with VSCode {self.target_vscode_version} (constraint: {engine_version_constraint})"
|
|
)
|
|
engine_version_constraint = self.replace_version_symbol(
|
|
engine_version_constraint
|
|
)
|
|
try:
|
|
self.execute_command([
|
|
"semver",
|
|
self.target_vscode_version,
|
|
"-r",
|
|
engine_version_constraint,
|
|
])
|
|
logger.info(f"Compatible version found: {candidate_version}")
|
|
return candidate_version
|
|
except (ValueError, subprocess.CalledProcessError):
|
|
logger.debug(
|
|
f"Version {candidate_version} is not compatible with VSCode {self.target_vscode_version} (constraint: {engine_version_constraint})."
|
|
)
|
|
continue
|
|
return candidate_version
|
|
logger.error("Error: not found compatible version.")
|
|
sys.exit(1)
|
|
|
|
def replace_version_symbol(self, version: str) -> str:
|
|
return re.sub(r"^\^", ">=", version)
|
|
|
|
def update_version_for_default_nix(self, content: str, new_version: str):
|
|
target_name = self.attribute_path.removeprefix("vscode-extensions.")
|
|
pattern = re.compile(
|
|
rf"{re.escape(target_name)}\s*=\s*buildVscodeMarketplaceExtension\s*\{{",
|
|
re.MULTILINE,
|
|
)
|
|
match = pattern.search(content)
|
|
if not match:
|
|
raise ValueError("Target block not found.")
|
|
brace_start = content.find("{", match.end() - 1)
|
|
if brace_start == -1:
|
|
raise ValueError("Opening brace not found.")
|
|
count = 0
|
|
pos = brace_start
|
|
text_len = len(content)
|
|
while pos < text_len:
|
|
if content[pos] == "{":
|
|
count += 1
|
|
elif content[pos] == "}":
|
|
count -= 1
|
|
if count == 0:
|
|
break
|
|
pos += 1
|
|
if count != 0:
|
|
raise ValueError("Braces mismatch.")
|
|
block_end = pos
|
|
block_text = content[brace_start : block_end + 1]
|
|
version_pattern = re.compile(r'(version\s*=\s*")([^"]+)(";)')
|
|
|
|
def repl(m):
|
|
match_version = m.group(2)
|
|
if self.current_version == match_version:
|
|
return f"{m.group(1)}{new_version}{m.group(3)}"
|
|
return m.group(0)
|
|
|
|
new_block_text, count_sub = version_pattern.subn(repl, block_text)
|
|
if count_sub == 0:
|
|
raise ValueError("No version field updated.")
|
|
updated_content = (
|
|
content[:brace_start] + new_block_text + content[block_end + 1 :]
|
|
)
|
|
Path(self.override_filename).write_text(updated_content, encoding="utf-8")
|
|
|
|
def run_nix_update(self, new_version: str, system: str) -> None:
|
|
"""
|
|
Builds and executes the nix-update command.
|
|
"""
|
|
if not self.override_filename:
|
|
self.override_filename = self.execute_command(
|
|
[
|
|
"nix",
|
|
"edit",
|
|
"--extra-experimental-features",
|
|
"nix-command",
|
|
"-f",
|
|
".",
|
|
self.attribute_path,
|
|
],
|
|
env={**os.environ, "EDITOR": "echo"},
|
|
)
|
|
if (
|
|
"pkgs/applications/editors/vscode/extensions/vscode-utils.nix"
|
|
in self.override_filename
|
|
) and Path(
|
|
"pkgs/applications/editors/vscode/extensions/default.nix"
|
|
).exists():
|
|
self.override_filename = (
|
|
"pkgs/applications/editors/vscode/extensions/default.nix"
|
|
)
|
|
if (
|
|
new_version != "skip"
|
|
and "pkgs/applications/editors/vscode/extensions/default.nix"
|
|
in self.override_filename
|
|
):
|
|
with logger.catch(exception=(IOError, ValueError)):
|
|
content = Path(self.override_filename).read_text(encoding="utf-8")
|
|
if content.count(self.current_version) > 1:
|
|
self.update_version_for_default_nix(content, new_version)
|
|
new_version = "skip"
|
|
if system not in self.supported_nix_systems:
|
|
src_hash = self._get_nix_vscode_extension_src_hash(system)
|
|
update_command = [
|
|
"update-source-version",
|
|
self.attribute_path,
|
|
self.new_version,
|
|
src_hash,
|
|
f"--system={system}",
|
|
"--ignore-same-version",
|
|
"--ignore-same-hash",
|
|
f"--file={self.override_filename}",
|
|
]
|
|
else:
|
|
update_command = [
|
|
"nix-update",
|
|
self.attribute_path,
|
|
"--version",
|
|
new_version,
|
|
"--override-filename",
|
|
self.override_filename,
|
|
"--system",
|
|
system,
|
|
]
|
|
self.execute_command(update_command)
|
|
|
|
def run(self):
|
|
marketplace_data = self.get_marketplace_extension_data(
|
|
self.extension_marketplace_id
|
|
)
|
|
available_versions = marketplace_data.get("versions", [])
|
|
logger.info(
|
|
f"Total versions found for {self.extension_marketplace_id}: {len(available_versions)}"
|
|
)
|
|
self.new_version = self.find_compatible_extension_version(
|
|
available_versions,
|
|
self.get_target_platform(self.nix_vscode_extension_platforms[0]),
|
|
)
|
|
try:
|
|
self.execute_command([
|
|
"semver",
|
|
self.current_version,
|
|
"-r",
|
|
f"<{self.new_version}",
|
|
])
|
|
except subprocess.CalledProcessError:
|
|
logger.info("Already up to date or new version is older!")
|
|
sys.exit(0)
|
|
for i, system in enumerate(self.nix_vscode_extension_platforms):
|
|
version = self.new_version if i == 0 else "skip"
|
|
self.run_nix_update(version, system)
|
|
if self.commit:
|
|
self.execute_command(["git", "add", self.override_filename])
|
|
self.execute_command([
|
|
"git",
|
|
"commit",
|
|
"-m",
|
|
f"{self.attribute_path}: {self.current_version} -> {self.new_version}",
|
|
])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
updater = VSCodeExtensionUpdater()
|
|
updater.run()
|