From 4853d79a819b594868034515387f65af31349915 Mon Sep 17 00:00:00 2001 From: baronfel Date: Wed, 10 Apr 2024 14:27:48 -0500 Subject: [PATCH 1/2] A simple version of the tool that accepts channels --- src/dotnet-install.py | 127 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 src/dotnet-install.py diff --git a/src/dotnet-install.py b/src/dotnet-install.py new file mode 100644 index 000000000..d4c309824 --- /dev/null +++ b/src/dotnet-install.py @@ -0,0 +1,127 @@ +#! /usr/bin/env python3 + +import argparse +import json +import requests +import hashlib + +class Channel: + def __init__(self, kind: str) -> None: + if kind == 'lts': + self.is_lts = True + if kind == 'sts': + self.is_sts = True + channel_parts = kind.split('.') + if channel_parts.count == 2: + self.is_channel_version = True + self.channel_version = kind + if channel_parts.count == 3: + self.is_feature_band = True; + self.channel_version = f"{channel_parts[0]}.{channel_parts[1]}" + self.feature_band = int(f"{channel_parts[2][0]}00") + + def __getattr__(self, name: str): + return self.__dict__[f"_{name}"] + + def __setattr__(self, name, value): + self.__dict__[f"_{name}"] = value + + def __str__(self) -> str: + if self.is_lts: + return 'lts' + if self.is_sts: + return 'sts' + if self.is_channel_version: + return self.channel_version + return f"{self.channel_version}.{self.feature_band.replace('0', 'x')}" + +def download_releases_index() -> json: + return requests.get("https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/releases-index.json", ).json() + +def download_releases_for_channel(channel_data: object) -> list: + return requests.get(channel_data["releases.json"]).json()['releases'] + +def pick_best_channel(channels: list, desired_channel: Channel) -> object: + if desired_channel.is_lts: + return next(chan for chan in channels if chan['release-type'] == 'lts') + elif desired_channel.is_sts: + return next(chan for chan in channels if chan['release-type'] == 'sts') + elif desired_channel.is_channel_version or desired_channel.is_feature_band: + return next(chan for chan in channels if chan['channel-version'] == desired_channel.channel_version) + +def find_matching_sdk(sdks: list, feature_band: int): + return next(sdk for sdk in sdks if int(sdk["version"].split('.')[2]) > feature_band) + +def pick_best_release(releases: list, requested_version: Channel) -> json: + if requested_version.is_lts or requested_version.is_sts or requested_version.is_channel_version: + # take the latest version for any of these + return releases[0] + elif requested_version.is_feature_band: + # find the first release that has an `sdk` in the feature band + return next(r for r in releases if find_matching_sdk(r['sdks'], requested_version.feature_band)) + +def pick_matching_sdk(sdks: list, requested_version: Channel): + if requested_version.is_lts or requested_version.is_sts or requested_version.is_channel_version: + # take the latest version for any of these + return sdks[0] + elif requested_version.is_feature_band: + # find the first release that has an `sdk` in the feature band + return find_matching_sdk(sdks, requested_version.feature_band) + +def pick_file_to_download(files: list, runtime_identifier: str): + return next(file for file in files if file['rid'] == runtime_identifier and (file['name'].endswith('tar.gz') or file['name'].endswith('zip'))) + +def download_file(url: str, hash: str): + local_filename = url.split('/')[-1] + with requests.get(url, stream=True) as r: + r.raise_for_status() + with open(local_filename, 'wb') as f: + for chunk in r.iter_content(chunk_size=8192): + f.write(chunk) + + BUF_SIZE = 65536 # lets read stuff in 64kb chunks! + + sha = hashlib.sha512() + + with open(local_filename, 'rb') as f: + while True: + data = f.read(BUF_SIZE) + if not data: + break + sha.update(data) + sha_digest = sha.hexdigest() + + if sha_digest != hash: + raise Exception("File hash didn't match") + + return local_filename + +def install_sdk(desired_channel: Channel, runtime_identifier: str): + index = download_releases_index() + if index is None: + raise Exception("Releases index could not be downloaded") + channels: list = index["releases-index"] + channel_information = pick_best_channel(channels, desired_channel) + channel_version = channel_information["channel-version"] + if channel_information is None: + raise Exception(f"No matching channel could be found for desired channel '{channel_version}'") + channel_releases = download_releases_for_channel(channel_information) + if channel_releases is None: + raise Exception(f"No releases found for channel '{channel_version}'") + release_information = pick_best_release(channel_releases, desired_channel) + if release_information is None: + raise Exception(f"No best release found in channel '{channel_version}'") + release_version = release_information['release-version'] + sdk_information = pick_matching_sdk(release_information['sdks'], desired_channel) + if sdk_information is None: + raise Exception(f"No matching SDK found in release '{release_version}'") + file_to_download = pick_file_to_download(sdk_information['files'], runtime_identifier) + if file_to_download is None: + raise Exception(f"No matching file for runtime identifier {runtime_identifier} found in release '{release_version}'") + download_file(file_to_download['url'], file_to_download['hash']) + +if __name__ == "__main__": + parser = argparse.ArgumentParser("dotnet-install.py", description="%(prog)s ia a simple command line interface for obtaining the .NET SDK and Runtime", add_help=True, exit_on_error=True) + parser.add_argument("--channel", "-c", type=Channel, help="The release grouping to download from. Can have a variety of formats: sts, lts, two-part version number (8.0) or three-part version number in major.minor.patchxx format (8.0.2xx)") + args = parser.parse_args() + install_sdk(args.channel, 'win-x64') From ad0dd9182f6c761ebbe28e71f186090fd79c22e4 Mon Sep 17 00:00:00 2001 From: baronfel Date: Wed, 10 Apr 2024 15:44:56 -0500 Subject: [PATCH 2/2] handle platform negotiation --- src/dotnet-install.py | 68 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/src/dotnet-install.py b/src/dotnet-install.py index d4c309824..12e57beac 100644 --- a/src/dotnet-install.py +++ b/src/dotnet-install.py @@ -4,6 +4,9 @@ import json import requests import hashlib +import platform +from dataclasses import dataclass +from typing import Optional class Channel: def __init__(self, kind: str) -> None: @@ -35,6 +38,45 @@ def __str__(self) -> str: return self.channel_version return f"{self.channel_version}.{self.feature_band.replace('0', 'x')}" +# TODO: platform.system() doesn't handle musl - need separate detection +def compute_platform_part(platform: str) -> str: + lower = platform.lower() + match lower: + case "osx" | "freebsd" | "rhel.6" | "linux-musl" | "linux": + return lower + case "macos": + return "osx" + case "win" | "windows": + return "win" + +def compute_arch_part(architecture: str) -> str: + match architecture: + case "arm64" | "aarch64": + return "arm64" + case "s390x" | "ppc64le" | "loongarch64": + return architecture + case _: + return "x64" + +def compute_runtime_identifier(platform: str, architecture: str) -> str: + platform_part = compute_platform_part(platform) + arch_part = compute_arch_part(architecture) + if platform_part is None or arch_part is None: + raise Exception(f"Unable to compute runtime identifier for {platform}/{architecture}") + return f"{platform_part}-{arch_part}" + + +@dataclass +class InstallRequest: + channel: Channel + runtime: Optional[str] + architecture: str + os: str + + @property + def rid(self): + return compute_runtime_identifier(self.os, self.architecture) + def download_releases_index() -> json: return requests.get("https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/releases-index.json", ).json() @@ -96,32 +138,42 @@ def download_file(url: str, hash: str): return local_filename -def install_sdk(desired_channel: Channel, runtime_identifier: str): + + +def install_sdk(request: InstallRequest): index = download_releases_index() if index is None: raise Exception("Releases index could not be downloaded") channels: list = index["releases-index"] - channel_information = pick_best_channel(channels, desired_channel) + channel_information = pick_best_channel(channels, request.channel) channel_version = channel_information["channel-version"] if channel_information is None: raise Exception(f"No matching channel could be found for desired channel '{channel_version}'") channel_releases = download_releases_for_channel(channel_information) if channel_releases is None: raise Exception(f"No releases found for channel '{channel_version}'") - release_information = pick_best_release(channel_releases, desired_channel) + release_information = pick_best_release(channel_releases, request.channel) if release_information is None: raise Exception(f"No best release found in channel '{channel_version}'") release_version = release_information['release-version'] - sdk_information = pick_matching_sdk(release_information['sdks'], desired_channel) + sdk_information = pick_matching_sdk(release_information['sdks'], request.channel) if sdk_information is None: raise Exception(f"No matching SDK found in release '{release_version}'") - file_to_download = pick_file_to_download(sdk_information['files'], runtime_identifier) + file_to_download = pick_file_to_download(sdk_information['files'], request.rid) if file_to_download is None: - raise Exception(f"No matching file for runtime identifier {runtime_identifier} found in release '{release_version}'") + raise Exception(f"No matching file for runtime identifier {request.rid} found in release '{release_version}'") download_file(file_to_download['url'], file_to_download['hash']) if __name__ == "__main__": parser = argparse.ArgumentParser("dotnet-install.py", description="%(prog)s ia a simple command line interface for obtaining the .NET SDK and Runtime", add_help=True, exit_on_error=True) parser.add_argument("--channel", "-c", type=Channel, help="The release grouping to download from. Can have a variety of formats: sts, lts, two-part version number (8.0) or three-part version number in major.minor.patchxx format (8.0.2xx)") - args = parser.parse_args() - install_sdk(args.channel, 'win-x64') + parser.add_argument("--runtime", help="Installs a shared runtime only, without the SDK", choices=["dotnet", "aspnetcore"]) + parser.add_argument("--architecture", help="Architecture of the .NET binaries to be installed, defaults to the current system.", default=platform.machine()) + parser.add_argument("--os", help="Specifies the operating system to be used when selecting the installer", default=platform.system()) + try: + args = parser.parse_args() + request = InstallRequest(args.channel, args.runtime, args.architecture, args.os) + install_sdk(request) + except: + parser.print_help() + exit(1) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy