mirror of
https://github.com/CHN-beta/nixpkgs.git
synced 2026-01-12 02:40:31 +08:00
vscode-extensions-update: improve
This commit is contained in:
117
pkgs/by-name/vs/vscode-extensions-update/vscode_extensions_batch_update.py
Executable file
117
pkgs/by-name/vs/vscode-extensions-update/vscode_extensions_batch_update.py
Executable file
@@ -0,0 +1,117 @@
|
||||
#! /usr/bin/env nix-shell
|
||||
#! nix-shell -i python3 -p nix python3 python3Packages.loguru nix-search-tv vscode-extensions-update gitMinimal
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class VSCodeExtensionBatchUpdater:
|
||||
# Extensions to be skipped
|
||||
_excluded_extensions = [
|
||||
# wrong upstream constraint: 0.10.x
|
||||
"vscode-extensions.ms-vscode.theme-tomorrowkit",
|
||||
"vscode-extensions.richie5um2.snake-trail",
|
||||
# not supported
|
||||
"vscode-extensions.ms-ceintl.vscode-language-pack-cs",
|
||||
"vscode-extensions.ms-ceintl.vscode-language-pack-de",
|
||||
"vscode-extensions.ms-ceintl.vscode-language-pack-es",
|
||||
"vscode-extensions.ms-ceintl.vscode-language-pack-fr",
|
||||
"vscode-extensions.ms-ceintl.vscode-language-pack-it",
|
||||
"vscode-extensions.ms-ceintl.vscode-language-pack-ja",
|
||||
"vscode-extensions.ms-ceintl.vscode-language-pack-ko",
|
||||
"vscode-extensions.ms-ceintl.vscode-language-pack-pt-br",
|
||||
"vscode-extensions.ms-ceintl.vscode-language-pack-qps-ploc",
|
||||
"vscode-extensions.ms-ceintl.vscode-language-pack-ru",
|
||||
"vscode-extensions.ms-ceintl.vscode-language-pack-tr",
|
||||
"vscode-extensions.ms-ceintl.vscode-language-pack-zh-hans",
|
||||
"vscode-extensions.ms-ceintl.vscode-language-pack-zh-hant",
|
||||
]
|
||||
|
||||
# Unable to determine the correct file location
|
||||
_extension_file_map = {
|
||||
"vscode-extensions.hashicorp.terraform": "pkgs/applications/editors/vscode/extensions/hashicorp.terraform/default.nix",
|
||||
"vscode-extensions.betterthantomorrow.calva": "pkgs/applications/editors/vscode/extensions/betterthantomorrow.calva/default.nix",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.parser = argparse.ArgumentParser(
|
||||
description="Batch update VSCode extensions"
|
||||
)
|
||||
# By default, update command skips extensions with updateScript or need '--platforms'
|
||||
self.parser.add_argument(
|
||||
"--platforms",
|
||||
action="store_true",
|
||||
help="add '--platforms' to update command from some extensions",
|
||||
)
|
||||
self.platforms = self.parser.parse_args().platforms
|
||||
|
||||
def execute_command(
|
||||
self, command, env: dict[str, str] = None, shell: bool = False
|
||||
) -> str:
|
||||
logger.debug("Executing command: {} (shell={})", command, shell)
|
||||
return subprocess.run(
|
||||
command,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=env,
|
||||
shell=shell,
|
||||
).stdout.strip()
|
||||
|
||||
def _get_extension_list(self) -> list[str]:
|
||||
# Get extension list from nix-search-tv output
|
||||
command = "nix-search-tv print | grep '^nixpkgs/ vscode-extensions\\.' | cut -d' ' -f2-"
|
||||
output = self.execute_command(command, shell=True)
|
||||
extension_list = output.splitlines()
|
||||
logger.info("Found {} extensions: {}", len(extension_list), extension_list)
|
||||
return extension_list
|
||||
|
||||
def _has_update_script(self, extension: str) -> bool:
|
||||
try:
|
||||
result = self._get_nix_attribute(f"{extension}.updateScript")
|
||||
return "not found" not in result
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
|
||||
def _has_platform_source(self, extension: str) -> bool:
|
||||
source_url = self._get_nix_attribute(f"{extension}.src.url")
|
||||
return "targetPlatform=" in source_url
|
||||
|
||||
def _get_nix_attribute(self, attribute: str) -> str:
|
||||
return self.execute_command(["nix", "eval", "--raw", "-f", ".", attribute])
|
||||
|
||||
def _get_extension_filename(self, extension: str) -> str | None:
|
||||
return self._extension_file_map.get(extension)
|
||||
|
||||
def _update_extension(self, extension: str) -> None:
|
||||
logger.info("Updating extension: {}", extension)
|
||||
if extension in self._excluded_extensions:
|
||||
return
|
||||
try:
|
||||
if self._has_update_script(extension):
|
||||
return
|
||||
update_command = ["vscode-extensions-update", extension, "--commit"]
|
||||
if self._has_platform_source(extension):
|
||||
if self.platforms:
|
||||
update_command.append("--platforms")
|
||||
else:
|
||||
return
|
||||
filename = self._get_extension_filename(extension)
|
||||
if filename:
|
||||
update_command.extend(["--override-filename", filename])
|
||||
self.execute_command(update_command)
|
||||
logger.info("Updated extension: {}", extension)
|
||||
except subprocess.CalledProcessError:
|
||||
logger.error("Failed to update extension: {}", extension)
|
||||
self.execute_command(["git", "restore", "."])
|
||||
|
||||
def run(self) -> None:
|
||||
for extension in self._get_extension_list():
|
||||
self._update_extension(extension)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
updater = VSCodeExtensionBatchUpdater()
|
||||
updater.run()
|
||||
@@ -1,21 +1,24 @@
|
||||
#! /usr/bin/env nix-shell
|
||||
#! nix-shell -i python3 -p nix python3 nodePackages.semver vsce nix-update gitMinimal
|
||||
#! 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 loguru import logger
|
||||
|
||||
|
||||
class VSCodeExtensionUpdater:
|
||||
"""
|
||||
A class to update VSCode extension version in nixpkgs.
|
||||
A class to update VSCode extension version.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.parser = argparse.ArgumentParser(
|
||||
description="Update VSCode extension version in nixpkgs."
|
||||
description="Update VSCode extension version."
|
||||
)
|
||||
self.parser.add_argument(
|
||||
"attribute",
|
||||
@@ -42,7 +45,7 @@ class VSCodeExtensionUpdater:
|
||||
self.args = self.parser.parse_args()
|
||||
self.attribute_path = self.args.attribute
|
||||
if not self.attribute_path:
|
||||
print("Error: Attribute path is required.", file=sys.stderr)
|
||||
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()
|
||||
@@ -58,13 +61,19 @@ class VSCodeExtensionUpdater:
|
||||
self.nix_systems = self._get_nix_vscode_extension_platforms() or [
|
||||
self.nix_system
|
||||
]
|
||||
if not self.args.platforms and self.nix_system in self.nix_systems:
|
||||
self.nix_systems = [self.nix_system]
|
||||
else:
|
||||
self.nix_systems = self.nix_systems[:1]
|
||||
print(f"VSCode version: {self.target_vscode_version}")
|
||||
print(f"Extension Marketplace ID: {self.extension_marketplace_id}")
|
||||
print(f"Extension Current Version: {self.current_version}")
|
||||
if not self.args.platforms:
|
||||
self.nix_systems = (
|
||||
[self.nix_system]
|
||||
if self.nix_system in self.nix_systems
|
||||
else self.nix_systems[:1]
|
||||
)
|
||||
if self.nix_system in self.nix_systems:
|
||||
self.nix_systems.remove(self.nix_system)
|
||||
self.nix_systems.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: dict[str, str] = None
|
||||
@@ -72,6 +81,7 @@ class VSCodeExtensionUpdater:
|
||||
"""
|
||||
Executes a shell command and returns its output.
|
||||
"""
|
||||
logger.debug("Executing command: {}", commandline)
|
||||
return subprocess.run(
|
||||
commandline,
|
||||
check=True,
|
||||
@@ -92,6 +102,51 @@ class VSCodeExtensionUpdater:
|
||||
"""
|
||||
return self._get_nix_attribute("system")
|
||||
|
||||
def get_supported_nix_systems(self) -> list[str]:
|
||||
nix_config = self.execute_command(["nix", "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 _get_nix_vscode_extension_src_hash(self, system: str) -> str:
|
||||
url = self.execute_command(
|
||||
[
|
||||
"nix",
|
||||
"eval",
|
||||
"--raw",
|
||||
"-f",
|
||||
".",
|
||||
f"{self.attribute_path}.src.url",
|
||||
"--system",
|
||||
system,
|
||||
]
|
||||
)
|
||||
if "warning" not in url:
|
||||
sha256 = self.execute_command(["nix-prefetch-url", url])
|
||||
hash = self.execute_command(
|
||||
[
|
||||
"nix",
|
||||
"hash",
|
||||
"convert",
|
||||
"--to",
|
||||
"sri",
|
||||
"--hash-algo",
|
||||
"sha256",
|
||||
sha256,
|
||||
]
|
||||
)
|
||||
return hash
|
||||
|
||||
def get_target_platform(self, nix_system: str) -> str:
|
||||
"""
|
||||
Retrieves the VS Code targetPlatform variable based on the Nix system.
|
||||
@@ -108,9 +163,8 @@ class VSCodeExtensionUpdater:
|
||||
try:
|
||||
return platform_mapping[nix_system]
|
||||
except KeyError:
|
||||
print(
|
||||
f"Error: Unknown Nix system '{nix_system}'. Cannot determine targetPlatform.",
|
||||
file=sys.stderr,
|
||||
logger.error(
|
||||
f"Error: Unknown Nix system '{nix_system}'. Cannot determine targetPlatform."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -166,25 +220,10 @@ class VSCodeExtensionUpdater:
|
||||
"""
|
||||
command = ["vsce", "show", extension_id, "--json"]
|
||||
try:
|
||||
with subprocess.Popen(
|
||||
command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
) as proc:
|
||||
data = json.load(proc.stdout)
|
||||
proc.wait()
|
||||
if proc.returncode != 0:
|
||||
err = proc.stderr.read()
|
||||
raise subprocess.CalledProcessError(
|
||||
proc.returncode, command, output=err
|
||||
)
|
||||
return data
|
||||
except json.JSONDecodeError:
|
||||
print(
|
||||
"Error: Couldn’t decode JSON response from VSCode marketplace; maybe the extension is not available?",
|
||||
file=sys.stderr,
|
||||
)
|
||||
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(
|
||||
@@ -207,7 +246,7 @@ class VSCodeExtensionUpdater:
|
||||
None,
|
||||
)
|
||||
if candidate_pre_release and not self.allow_pre_release:
|
||||
print(f"Skipping PreRelease version {candidate_version}")
|
||||
logger.debug(f"Skipping PreRelease version {candidate_version}")
|
||||
continue
|
||||
engine_version_constraint = next(
|
||||
(
|
||||
@@ -218,9 +257,12 @@ class VSCodeExtensionUpdater:
|
||||
None,
|
||||
)
|
||||
if engine_version_constraint:
|
||||
print(
|
||||
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(
|
||||
[
|
||||
@@ -230,22 +272,70 @@ class VSCodeExtensionUpdater:
|
||||
engine_version_constraint,
|
||||
]
|
||||
)
|
||||
|
||||
print(f"Compatible version found: {candidate_version}")
|
||||
logger.info(f"Compatible version found: {candidate_version}")
|
||||
return candidate_version
|
||||
except (ValueError, subprocess.CalledProcessError):
|
||||
print(
|
||||
logger.debug(
|
||||
f"Version {candidate_version} is not compatible with VSCode {self.target_vscode_version} (constraint: {engine_version_constraint})."
|
||||
)
|
||||
continue
|
||||
return candidate_version
|
||||
else:
|
||||
print(
|
||||
"Error: not found compatible version.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
logger.error("Error: not found compatible version.")
|
||||
sys.exit(1)
|
||||
|
||||
def replace_version_symbol(self, version: str) -> str:
|
||||
version = re.sub(r"^\^", ">=", version)
|
||||
return 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 :]
|
||||
)
|
||||
with open(
|
||||
self.override_filename,
|
||||
"w",
|
||||
encoding="utf-8",
|
||||
) as f:
|
||||
f.write(updated_content)
|
||||
|
||||
def run_nix_update(self, new_version: str, system: str) -> None:
|
||||
"""
|
||||
Builds and executes the nix-update command.
|
||||
@@ -272,16 +362,44 @@ class VSCodeExtensionUpdater:
|
||||
self.override_filename = (
|
||||
"pkgs/applications/editors/vscode/extensions/default.nix"
|
||||
)
|
||||
update_command = [
|
||||
"nix-update",
|
||||
self.attribute_path,
|
||||
"--version",
|
||||
new_version,
|
||||
"--override-filename",
|
||||
self.override_filename,
|
||||
"--system",
|
||||
system,
|
||||
]
|
||||
if (
|
||||
new_version != "skip"
|
||||
and "pkgs/applications/editors/vscode/extensions/default.nix"
|
||||
in self.override_filename
|
||||
):
|
||||
with logger.catch(exception=(IOError, ValueError)):
|
||||
with open(
|
||||
self.override_filename,
|
||||
"r",
|
||||
encoding="utf-8",
|
||||
) as f:
|
||||
content = f.read()
|
||||
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:
|
||||
hash = self._get_nix_vscode_extension_src_hash(system)
|
||||
update_command = [
|
||||
"update-source-version",
|
||||
self.attribute_path,
|
||||
self.new_version,
|
||||
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):
|
||||
@@ -289,7 +407,7 @@ class VSCodeExtensionUpdater:
|
||||
self.extension_marketplace_id
|
||||
)
|
||||
available_versions = marketplace_data.get("versions", [])
|
||||
print(
|
||||
logger.info(
|
||||
f"Total versions found for {self.extension_marketplace_id}: {len(available_versions)}"
|
||||
)
|
||||
self.new_version = self.find_compatible_extension_version(
|
||||
@@ -300,9 +418,8 @@ class VSCodeExtensionUpdater:
|
||||
["semver", self.current_version, "-r", f"<{self.new_version}"]
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
print("Already up to date or new version is older!")
|
||||
logger.info("Already up to date or new version is older!")
|
||||
sys.exit(0)
|
||||
print(f"Selected extension version from Marketplace: {self.new_version}")
|
||||
for i, system in enumerate(self.nix_systems):
|
||||
version = self.new_version if i == 0 else "skip"
|
||||
self.run_nix_update(version, system)
|
||||
|
||||
Reference in New Issue
Block a user