diff --git a/pkgs/applications/editors/vscode/extensions/update_installed_exts.py b/pkgs/applications/editors/vscode/extensions/update_installed_exts.py new file mode 100755 index 0000000000000..a0cbafd3b5614 --- /dev/null +++ b/pkgs/applications/editors/vscode/extensions/update_installed_exts.py @@ -0,0 +1,130 @@ +#! /usr/bin/env nix-shell +#! nix-shell -i python3 -p cacert nix python3 python3Packages.requests + + +""" +can be added to your configuration with the following command and snippet: +$ ./pkgs/applications/editors/vscode/extensions/update_installed_exts.py > extensions.nix + +(vscode-with-extensions.override { + vscodeExtensions = map ( + extension: + vscode-utils.buildVscodeMarketplaceExtension { + mktplcRef = { + inherit (extension) + name + publisher + version + hash + ; + }; + } + ) (import ./extensions.nix).extensions; +}) +""" + +import atexit +import json +import shutil +import signal +import subprocess +import sys +import tempfile +import zipfile +from pathlib import Path + +import requests + +tmp_dirs = [] + + +def cleanup_all(): + for d in list(tmp_dirs): + shutil.rmtree(d, ignore_errors=True) + tmp_dirs.remove(d) + + +atexit.register(cleanup_all) + + +def fail(msg): + print(msg, file=sys.stderr) + sys.exit(1) + + +session = requests.Session() +adapter = requests.adapters.HTTPAdapter(max_retries=3) +session.mount("https://", adapter) + + +def download_and_process(owner, name): + n = f"{owner}.{name}" + url = ( + f"https://{owner}.gallery.vsassets.io/_apis/public/" + f"gallery/publisher/{owner}/extension/{name}/latest/" + "assetbyname/Microsoft.VisualStudio.Services.VSIXPackage" + ) + + td = tempfile.mkdtemp(prefix="vscode_exts_") + tmp_dirs.append(td) + try: + zpath = Path(td) / f"{n}.zip" + r = session.get(url, stream=True, timeout=10) + if r.status_code == 404: + return "" + r.raise_for_status() + with zpath.open("wb") as f: + for chunk in r.iter_content(8192): + f.write(chunk) + + with zipfile.ZipFile(zpath) as z: + pkg = z.read("extension/package.json") + ver = json.loads(pkg)["version"] + + res = subprocess.run( + ["nix", "hash", "file", str(zpath)], + check=False, + capture_output=True, + text=True, + ) + if res.returncode: + fail(res.stderr.strip()) + h = res.stdout.strip() + + return f' {{ name = "{name}"; publisher = "{owner}"; version = "{ver}"; hash = "{h}"; }}' + finally: + if td in tmp_dirs: + shutil.rmtree(td, ignore_errors=True) + tmp_dirs.remove(td) + + +if __name__ == "__main__": + code = ( + sys.argv[1] + if len(sys.argv) > 1 + else shutil.which("code") or shutil.which("codium") + ) + if not code: + fail("VSCode executable not found") + + def on_int(signum, frame): + print("Interrupted, exiting", file=sys.stderr) + sys.exit(1) + + signal.signal(signal.SIGINT, on_int) + + try: + exts = subprocess.check_output( + [code, "--list-extensions"], text=True + ).splitlines() + except Exception as e: + fail(str(e)) + + print("{ extensions = [") + for ext in exts: + if "." in ext: + owner, name = ext.split(".", 1) + entry = download_and_process(owner, name) + if entry: + print(entry) + print("];\n}") diff --git a/pkgs/applications/editors/vscode/extensions/update_installed_exts.sh b/pkgs/applications/editors/vscode/extensions/update_installed_exts.sh deleted file mode 100755 index dd2e8ef247895..0000000000000 --- a/pkgs/applications/editors/vscode/extensions/update_installed_exts.sh +++ /dev/null @@ -1,90 +0,0 @@ -#! /usr/bin/env nix-shell -#! nix-shell -i bash -p cacert curl jq unzip -# shellcheck shell=bash -set -eu -o pipefail - -# can be added to your configuration with the following command and snippet: -# $ ./pkgs/applications/editors/vscode/extensions/update_installed_exts.sh > extensions.nix -# -# packages = with pkgs; -# (vscode-with-extensions.override { -# vscodeExtensions = map -# (extension: vscode-utils.buildVscodeMarketplaceExtension { -# mktplcRef = { -# inherit (extension) name publisher version sha256; -# }; -# }) -# (import ./extensions.nix).extensions; -# }) -# ] - -# Helper to just fail with a message and non-zero exit code. -function fail() { - echo "$1" >&2 - exit 1 -} - -# Helper to clean up after ourselves if we're killed by SIGINT. -function clean_up() { - TDIR="${TMPDIR:-/tmp}" - echo "Script killed, cleaning up tmpdirs: $TDIR/vscode_exts_*" >&2 - rm -Rf "$TDIR/vscode_exts_*" -} - -function get_vsixpkg() { - N="$1.$2" - - # Create a tempdir for the extension download. - EXTTMP=$(mktemp -d -t vscode_exts_XXXXXXXX) - - URL="https://$1.gallery.vsassets.io/_apis/public/gallery/publisher/$1/extension/$2/latest/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage" - - # Quietly but delicately curl down the file, blowing up at the first sign of trouble. - curl --silent --show-error --retry 3 --fail -X GET -o "$EXTTMP/$N.zip" "$URL" - # Unpack the file we need to stdout then pull out the version - VER=$(jq -r '.version' <(unzip -qc "$EXTTMP/$N.zip" "extension/package.json")) - # Calculate the hash - HASH=$(nix-hash --flat --sri --type sha256 "$EXTTMP/$N.zip") - - # Clean up. - rm -Rf "$EXTTMP" - # I don't like 'rm -Rf' lurking in my scripts but this seems appropriate. - - cat <<-EOF - { - name = "$2"; - publisher = "$1"; - version = "$VER"; - hash = "$HASH"; - } -EOF -} - -# See if we can find our `code` binary somewhere. -if [ $# -ne 0 ]; then - CODE=$1 -else - CODE=$(command -v code || command -v codium) -fi - -if [ -z "$CODE" ]; then - # Not much point continuing. - fail "VSCode executable not found" -fi - -# Try to be a good citizen and clean up after ourselves if we're killed. -trap clean_up SIGINT - -# Begin the printing of the nix expression that will house the list of extensions. -printf '{ extensions = [\n' - -# Note that we are only looking to update extensions that are already installed. -for i in $($CODE --list-extensions) -do - OWNER=$(echo "$i" | cut -d. -f1) - EXT=$(echo "$i" | cut -d. -f2) - - get_vsixpkg "$OWNER" "$EXT" -done -# Close off the nix expression. -printf '];\n}'