rules_jvm_export/jvm_export/support/deploy.py (176 lines of code) (raw):
#!$PYTHON_PATH
# Originally from https://github.com/vaticle/bazel-distribution/blob/master/maven/templates/deploy.py
# SPDX-License-Identifier: Apache-2.0
from __future__ import print_function
from xml.etree import ElementTree
import argparse
import hashlib
import os
import re
import shutil
import subprocess as sp
import sys
import tempfile
from pathlib import Path
from posixpath import join as urljoin
from urllib.parse import urlparse
USAGE_STR = """
bazel run <target>.publish -- release
"""
MAVEN_REPOS = {
"snapshot": "{snapshot}",
"release": "{release}",
"local": "file:~/.m2/repository/",
}
ARTIFACTS = $ARTIFACTS
POM_FILE_PATH = "$POM_PATH"
SRCJAR_PATH = "$SRCJAR_PATH"
def main():
args = _parse_args()
repo_type = "local" if args.local else args.command
maven_url = _to_url(args.publish_to) if args.publish_to else MAVEN_REPOS[repo_type]
pom = ElementTree.parse(POM_FILE_PATH).getroot()
group_id = _parse_pom(pom, "ns0:groupId")
artifact_id = _parse_pom(pom, "ns0:artifactId")
version = _parse_pom(pom, "ns0:version")
version_snapshot_regex = """^[0-9|a-f|A-F]{40}$|.*-SNAPSHOT$"""
version_release_regex = """^[0-9]+.[0-9]+.[0-9]+(-[a-zA-Z0-9]+)*$"""
if (
repo_type == "snapshot"
and len(re.findall(version_snapshot_regex, version)) == 0
):
raise ValueError(
"Invalid version: {}. An artifact uploaded to a {} repository "
"must have a version which complies to this regex: {}".format(
version, repo_type, version_snapshot_regex
)
)
if repo_type == "release" and len(re.findall(version_release_regex, version)) == 0:
raise ValueError(
"Invalid version: {}. An artifact uploaded to a {} repository "
"must have a version which complies to this regex: {}".format(
version, repo_type, version_release_regex
)
)
curl_opts = _curl_options(maven_url, args.netrc)
filename_base = "{coordinates}/{artifact}/{version}/{artifact}-{version}".format(
coordinates=group_id.replace(".", "/"), version=version, artifact=artifact_id
)
for classifier, artifact_path in ARTIFACTS.items():
remote_path = f"{filename_base}-{classifier}.jar" if classifier else f"{filename_base}.jar"
upload_with_sig(maven_url, curl_opts, args.gpg, artifact_path, remote_path)
upload_with_sig(maven_url, curl_opts, args.gpg, POM_FILE_PATH, filename_base + ".pom")
def upload_with_sig(maven_url, curl_opts, gpg, local_path, remote_path):
upload(maven_url, curl_opts, local_path, remote_path)
if gpg:
upload(maven_url, curl_opts, sign(local_path), remote_path + ".asc")
with tempfile.NamedTemporaryFile(mode="wt", delete=True) as md5_temp:
md5_temp.write(md5(local_path))
md5_temp.flush()
upload(maven_url, curl_opts, md5_temp.name, remote_path + ".md5")
with tempfile.NamedTemporaryFile(mode="wt", delete=True) as sha1_temp:
sha1_temp.write(sha1(local_path))
sha1_temp.flush()
upload(maven_url, curl_opts, sha1_temp.name, remote_path + ".sha1")
def _parse_pom(pom, elem_name):
namespaces = {"ns0": "http://maven.apache.org/POM/4.0.0"}
elem = pom.find(elem_name, namespaces)
if elem is None or len(elem.text) == 0:
raise Exception(f"Could not get {elem_name} from pom.xml")
return elem.text
def sha1(fn):
with open(fn, "rb") as f:
return hashlib.sha1(f.read()).hexdigest()
def md5(fn):
with open(fn, "rb") as f:
return hashlib.md5(f.read()).hexdigest()
def upload(url, curl_opts, local_fn, remote_fn):
print(f" publishing {remote_fn}")
u = urlparse(url)
if u.scheme == "file":
destination = Path(u.path).expanduser() / remote_fn
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(src=local_fn, dst=destination)
else:
failed = False
try:
upload_status_code = (
sp.check_output(
[
"curl",
"--silent",
"--output",
"/dev/stderr",
"--write-out",
"%{http_code}",
*curl_opts,
"--upload-file",
local_fn,
urljoin(url, remote_fn),
]
)
.decode()
.strip()
)
except Exception:
# Catch the exception to avoid credential getting logged
failed = True
if failed:
raise Exception("upload of {} to {} failed".format(local_fn, urljoin(url, remote_fn)))
if upload_status_code not in {"200", "201"}:
raise Exception(
"upload of {} failed, got HTTP status code {}".format(
local_fn, upload_status_code
)
)
def sign(fn):
# TODO(vmax): current limitation of this functionality
# is that gpg key should already be present in keyring
# and should not require passphrase
asc_file = tempfile.mktemp()
sp.check_call(["gpg", "--detach-sign", "--armor", "--output", asc_file, fn])
return asc_file
def _curl_options(maven_url, netrc):
if urlparse(maven_url).scheme == "file":
return None
elif netrc:
return ["--netrc"]
else:
username, password = os.getenv("SONATYPE_USERNAME"), os.getenv(
"SONATYPE_PASSWORD"
)
if not username:
raise ValueError(
"Error: username should be passed via $SONATYPE_USERNAME env variable"
)
if not password:
raise ValueError(
"Error: password should be passed via $SONATYPE_PASSWORD env variable"
)
return ["-u", f"{username}:{password}"]
def _to_url(publish_to):
if (publish_to.startswith("https:") or
publish_to.startswith("http:") or
publish_to.startswith("file:")):
return publish_to
return "file:" + publish_to
def _parse_args():
class Formatter(
argparse.ArgumentDefaultsHelpFormatter, argparse.RawTextHelpFormatter
):
pass
parser = argparse.ArgumentParser(description=USAGE_STR, formatter_class=Formatter)
parser.add_argument("command", choices=["release", "snapshot"])
parser.add_argument(
"--gpg", action="store_true", default=False, help="Sign artifact using gpg"
)
parser.add_argument(
"--netrc",
action="store_true",
default=False,
help="Use .netrc for authentication",
)
parser.add_argument(
"--local",
action="store_true",
default=False,
help="Publish to local M2 repo (~/.m2/repository/).",
)
parser.add_argument(
"--publish_to",
help="Specify repository to publish to.",
)
return parser.parse_args()
if __name__ == "__main__":
main()