Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: check for and list valid versions and targets programmatically in phylum-init #74

Merged
merged 5 commits into from Jul 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
140 changes: 113 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,71 @@ 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] = []
prefix, suffix = "phylum-", ".zip"
for asset in assets:
name = asset.get("name", "")
if name.startswith(prefix) and name.endswith(suffix):
target = name.replace(prefix, "").replace(suffix, "")
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 +193,10 @@ 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:
releases = ", ".join(supported_versions)
raise argparse.ArgumentTypeError(f"version must be from a supported release: {releases}")

return version

Expand All @@ -156,15 +221,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 +313,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 +331,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 +344,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 +364,29 @@ def main(args=None):
"""Main entrypoint."""
args = get_args(args=args)

if args.list_releases:
print("Looking up supported releases ...")
releases = ", ".join(supported_releases())
print(f"Supported releases: {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} ...")
targets = ", ".join(supported_target_triples)
print(f"Supported targets for release {tag_name}: {targets}")
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 SystemExit(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