Skip to content

Commit

Permalink
feat: check for and list valid versions and targets programmatically …
Browse files Browse the repository at this point in the history
…in `phylum-init`

* Check for valid versions by using the GitHub API to compare against actual releases
  * Supported target triples programmatically generated based on the results
  * Users will be told when the version provided as an option is not valid
  * Removed `SUPPORTED_TARGET_TRIPLES` static collection
* Add `--list-releases` and `--list-targets` options
  * These are mutually exclusive options
  * Listing Rust target triples are for a specific release version
* Format and refactor throughout
  * Stop using `.get` method on `importlib_metadata.metadata()` results
    * See python/importlib_metadata#384
  * Add constant for `MIN_SUPPORTED_CLI_VERSION`
  • Loading branch information
maxrake committed Jul 12, 2022
1 parent 1d31ca1 commit 6fa937a
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 49 deletions.
10 changes: 5 additions & 5 deletions src/phylum/__init__.py
@@ -1,5 +1,5 @@
"""Top-level package for phylum."""
# TODO: Use only the standard library form (importlib.metadata) only after Python 3.7 support is dropped
# TODO: Use only the standard library form (importlib.metadata) after Python 3.7 support is dropped
# https://github.com/phylum-dev/phylum-ci/issues/18
try:
import importlib.metadata as importlib_metadata
Expand All @@ -10,8 +10,8 @@
PKG_METADATA = importlib_metadata.metadata(__name__)

__version__ = importlib_metadata.version(__name__)
__author__ = PKG_METADATA.get("Author")
__email__ = PKG_METADATA.get("Author-email")
__author__ = PKG_METADATA["Author"]
__email__ = PKG_METADATA["Author-email"]

PKG_NAME = PKG_METADATA.get("Name")
PKG_SUMMARY = PKG_METADATA.get("Summary")
PKG_NAME = PKG_METADATA["Name"]
PKG_SUMMARY = PKG_METADATA["Summary"]
3 changes: 1 addition & 2 deletions src/phylum/ci/cli.py
Expand Up @@ -15,7 +15,7 @@
from phylum.ci.ci_none import CINone
from phylum.ci.ci_precommit import CIPreCommit
from phylum.ci.common import ReturnCode
from phylum.constants import SUPPORTED_TARGET_TRIPLES, TOKEN_ENVVAR_NAME
from phylum.constants import TOKEN_ENVVAR_NAME
from phylum.init.cli import get_target_triple, version_check


Expand Down Expand Up @@ -188,7 +188,6 @@ def get_args(args: Optional[Sequence[str]] = None) -> Tuple[argparse.Namespace,
cli_group.add_argument(
"-t",
"--target",
choices=SUPPORTED_TARGET_TRIPLES,
default=get_target_triple(),
help="The target platform type where the CLI will be installed.",
)
Expand Down
19 changes: 4 additions & 15 deletions src/phylum/constants.py
@@ -1,20 +1,8 @@
"""Provide constants for use throughout the package."""

# These are the currently supported Rust target triples
#
# Targets are identified by their "target triple" which is the string to inform the compiler what kind of output
# should be produced. A target triple consists of three strings separated by a hyphen, with a possible fourth string
# at the end preceded by a hyphen. The first is the architecture, the second is the "vendor", the third is the OS
# type, and the optional fourth is environment type.
#
# References:
# * https://doc.rust-lang.org/nightly/rustc/platform-support.html
# * https://rust-lang.github.io/rfcs/0131-target-specification.html
SUPPORTED_TARGET_TRIPLES = (
"aarch64-apple-darwin",
"x86_64-apple-darwin",
"x86_64-unknown-linux-musl",
)
# The release layout structure changed starting with v2.0.0 and support is only for the new layout
MIN_SUPPORTED_CLI_VERSION = "v2.0.0"

# Keys are lowercase machine hardware names as returned from `uname -m`.
# Values are the mapped rustc architecture.
SUPPORTED_ARCHES = {
Expand All @@ -23,6 +11,7 @@
"x86_64": "x86_64",
"amd64": "x86_64",
}

# Keys are lowercase operating system name as returned from `uname -s`.
# Values are the mapped rustc platform, which is the vendor-os_type[-environment_type].
SUPPORTED_PLATFORMS = {
Expand Down
135 changes: 108 additions & 27 deletions src/phylum/init/cli.py
Expand Up @@ -8,18 +8,19 @@
import sys
import tempfile
import zipfile
from functools import lru_cache
from pathlib import Path
from typing import Optional, Tuple
from typing import List, Optional, Tuple

import requests
from packaging.utils import canonicalize_version
from packaging.version import InvalidVersion, Version
from phylum import __version__
from phylum.constants import (
MIN_SUPPORTED_CLI_VERSION,
REQ_TIMEOUT,
SUPPORTED_ARCHES,
SUPPORTED_PLATFORMS,
SUPPORTED_TARGET_TRIPLES,
TOKEN_ENVVAR_NAME,
)
from phylum.init import SCRIPT_NAME
Expand Down Expand Up @@ -106,7 +107,8 @@ def get_latest_version():
# API Reference: https://docs.github.com/en/rest/releases/releases#get-the-latest-release
github_api_url = "https://api.github.com/repos/phylum-dev/cli/releases/latest"

req = requests.get(github_api_url, timeout=REQ_TIMEOUT)
headers = {"Accept": "application/vnd.github+json"}
req = requests.get(github_api_url, headers=headers, timeout=REQ_TIMEOUT)
req.raise_for_status()
req_json = req.json()

Expand All @@ -117,6 +119,69 @@ def get_latest_version():
return latest_version


@lru_cache(maxsize=1)
def supported_releases() -> List[str]:
"""Get the most recent supported releases programmatically and return them."""
# API Reference: https://docs.github.com/en/rest/releases/releases#list-releases
github_api_url = "https://api.github.com/repos/phylum-dev/cli/releases"

headers = {"Accept": "application/vnd.github+json"}
query_params = {"per_page": 100}
req = requests.get(github_api_url, headers=headers, params=query_params, timeout=REQ_TIMEOUT)
req.raise_for_status()
req_json = req.json()

# The "name" entry stores the GitHub Release name, which could be set to something other than the version.
# Using the "tag_name" entry is better since the tags are much more tightly coupled with the release version.
releases = [rel.get("tag_name") for rel in req_json if is_supported_version(rel.get("tag_name", "0.0.0"))]

return releases


def is_supported_version(version: str) -> bool:
"""Predicate for determining if a given version is supported."""
try:
provided_version = Version(canonicalize_version(version))
min_supported_version = Version(MIN_SUPPORTED_CLI_VERSION)
except InvalidVersion as err:
raise ValueError("An invalid version was provided") from err

return provided_version >= min_supported_version


def supported_targets(release_tag: str) -> List[str]:
"""Get the supported Rust target triples programmatically for a given release tag and return them.
Targets are identified by their "target triple" which is the string to inform the compiler what kind of output
should be produced. A target triple consists of three strings separated by a hyphen, with a possible fourth string
at the end preceded by a hyphen. The first is the architecture, the second is the "vendor", the third is the OS
type, and the optional fourth is environment type.
References:
* https://doc.rust-lang.org/nightly/rustc/platform-support.html
* https://rust-lang.github.io/rfcs/0131-target-specification.html
"""
if release_tag not in supported_releases():
raise SystemExit(f" [!] Unsupported version: {release_tag}")

# API Reference: https://docs.github.com/en/rest/releases/releases#get-a-release-by-tag-name
github_api_url = f"https://api.github.com/repos/phylum-dev/cli/releases/tags/{release_tag}"

headers = {"Accept": "application/vnd.github+json"}
req = requests.get(github_api_url, headers=headers, timeout=REQ_TIMEOUT)
req.raise_for_status()
req_json = req.json()

assets = req_json.get("assets", [])
targets: List[str] = []
for asset in assets:
name = asset.get("name", "")
target = name.replace("phylum-", "").replace(".zip.minisig", "").replace(".zip", "")
targets.append(target)

return list(set(targets))


def version_check(version):
"""Check a given version for validity and return a normalized form of it."""
if version == "latest":
Expand All @@ -126,12 +191,9 @@ def version_check(version):
if not version.startswith("v"):
version = f"v{version}"

try:
# The release layout structure changed starting with v2.0.0 and support here is only for the new layout
if Version("v2.0.0") > Version(canonicalize_version(version)):
raise argparse.ArgumentTypeError("version must be at least v2.0.0")
except InvalidVersion as err:
raise argparse.ArgumentTypeError("an invalid version was provided") from err
supported_versions = supported_releases()
if version not in supported_versions:
raise argparse.ArgumentTypeError(f"version must be from a supported release: {', '.join(supported_versions)}")

return version

Expand All @@ -156,15 +218,11 @@ def save_file_from_url(url, path):
print("Done")


def get_archive_url(version, archive_name):
"""Craft an archive download URL from a given version and archive name.
Despite the name, the `version` is really what the GitHub API for releases calls the `tag_name`.
Reference: https://docs.github.com/en/rest/releases/releases#get-a-release-by-tag-name
"""
def get_archive_url(tag_name, archive_name):
"""Craft an archive download URL from a given tag name and archive name."""
# Reference: https://docs.github.com/en/rest/releases/releases#get-a-release-by-tag-name
github_base_uri = "https://github.com/phylum-dev/cli/releases"
archive_url = f"{github_base_uri}/download/{version}/{archive_name}"

archive_url = f"{github_base_uri}/download/{tag_name}/{archive_name}"
return archive_url


Expand Down Expand Up @@ -252,6 +310,12 @@ def get_args(args=None):
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)

parser.add_argument(
"-V",
"--version",
action="version",
version=f"{SCRIPT_NAME} {__version__}",
)
parser.add_argument(
"-r",
"--phylum-release",
Expand All @@ -264,7 +328,6 @@ def get_args(args=None):
parser.add_argument(
"-t",
"--target",
choices=SUPPORTED_TARGET_TRIPLES,
default=get_target_triple(),
help="The target platform type where the CLI will be installed.",
)
Expand All @@ -278,11 +341,17 @@ def get_args(args=None):
already set in the Phylum config file or (2) to manually populate the token with a `phylum auth login` or
`phylum auth register` command after install.""",
)
parser.add_argument(
"-V",
"--version",
action="version",
version=f"{SCRIPT_NAME} {__version__}",

list_group = parser.add_mutually_exclusive_group()
list_group.add_argument(
"--list-releases",
action="store_true",
help="List the Phylum CLI releases available to install.",
)
list_group.add_argument(
"--list-targets",
action="store_true",
help="List the target platform types available for installing a given Phylum CLI release.",
)

return parser.parse_args(args=args)
Expand All @@ -292,15 +361,27 @@ def main(args=None):
"""Main entrypoint."""
args = get_args(args=args)

if args.list_releases:
print("Looking up supported releases ...")
print(f"Supported releases: {', '.join(supported_releases())}")
return 0

tag_name = args.version
supported_target_triples = supported_targets(tag_name)
if args.list_targets:
print(f"Looking up supported targets for release {tag_name} ...")
print(f"Supported targets for release {tag_name}: {', '.join(supported_target_triples)}")
return 0

target_triple = args.target
if target_triple not in SUPPORTED_TARGET_TRIPLES:
raise ValueError(f"The identified target triple `{target_triple}` is not currently supported")
if target_triple not in supported_target_triples:
raise ValueError(f"The identified target triple `{target_triple}` is not supported for release {tag_name}")

archive_name = f"phylum-{target_triple}.zip"
minisig_name = f"{archive_name}.minisig"
archive_url = get_archive_url(args.version, archive_name)
archive_url = get_archive_url(tag_name, archive_name)
minisig_url = f"{archive_url}.minisig"
phylum_bin_path = get_expected_phylum_bin_path(args.version)
phylum_bin_path = get_expected_phylum_bin_path(tag_name)

with tempfile.TemporaryDirectory() as temp_dir:
temp_dir_path = pathlib.Path(temp_dir)
Expand Down

0 comments on commit 6fa937a

Please sign in to comment.