blob: ff1d15e402dec0e19ceecd358add09c80b82bfba [file] [log] [blame]
#!/usr/bin/env python3
import os
import argparse
import tempfile
import sys
import zipfile
import tarfile
import re
import glob
import shutil
import json
import xml.etree.ElementTree as ET
import intellij
# A list of files excluded from the compile classpath.
HIDDEN = [
# kotlinc-lib.jar contains kotlin-stdlib and kotlin-reflect from the Kotlin compiler.
# We prefer compiling against the IntelliJ version of kotlin-stdlib
# because IntelliJ has a hack in PluginClassLoader.mustBeLoadedByPlatform()
# which circumvents the normal classloader delegation hierarchy in order to
# prioritize its own version of kotlin-stdlib at runtime.
"/plugins/Kotlin/lib/kotlinc-lib.jar",
# This annotation jar is nonexistent, despite being referenced by product-info.json.
# Probably this happens because BaseIdeaProperties.copyAdditionalFiles() moves this jar.
"/lib/annotations-java5.jar",
# Hide JUnit3 to avoid clashing with JUnit4 (b/271338952, IDEA-315065).
# This emulates IntelliJ commit 1dc8b1360c which is coming in IJ 232.
"/lib/junit.jar",
]
ALL = "all"
LINUX = "linux"
WIN = "windows"
MAC = "darwin"
MAC_ARM = "darwin_aarch64"
PLATFORMS = [LINUX, WIN, MAC, MAC_ARM]
HOME_PATHS = {
LINUX: "/linux/android-studio",
MAC: "/darwin/android-studio/Contents",
MAC_ARM: "/darwin_aarch64/android-studio/Contents",
WIN: "/windows/android-studio",
}
def read_product_info(sdk, platform):
path = "/Resources/product-info.json" if platform in [MAC, MAC_ARM] else "/product-info.json"
product_info = sdk + HOME_PATHS[platform] + path
with open(product_info) as f:
return json.load(f)
def list_sdk_jars(sdk):
sets = {}
for platform in PLATFORMS:
# Extract the runtime classpath from product-info.json.
product_info = read_product_info(sdk, platform)
(launch_config,) = product_info["launch"]
jars = ["/lib/" + jar for jar in launch_config["bootClassPathJarNames"]]
# Java plugin sdk are included as part of the platform as there are references to it.
idea_home = sdk + HOME_PATHS[platform]
jars += ["/plugins/java/lib/" + jar for jar in os.listdir(idea_home + "/plugins/java/lib/") if jar.endswith(".jar")]
jars = [jar for jar in jars if jar not in HIDDEN]
sets[platform] = set(jars)
sets[ALL] = sets[WIN] & sets[MAC] & sets[MAC_ARM] & sets[LINUX]
sets[LINUX] = sets[LINUX] - sets[ALL]
sets[WIN] = sets[WIN] - sets[ALL]
sets[MAC] = sets[MAC] - sets[ALL]
sets[MAC_ARM] = sets[MAC_ARM] - sets[ALL]
sdk_jars = {}
for platform in [ALL] + PLATFORMS:
sdk_jars[platform] = sorted(sets[platform])
return sdk_jars
def list_plugin_jars(sdk):
all = {}
for platform in PLATFORMS:
idea_home = sdk + HOME_PATHS[platform]
all[platform] = {}
for plugin in os.listdir(idea_home + "/plugins"):
if plugin == "java":
# The plugin java is added as part of the platform
continue
path = "/plugins/" + plugin + "/lib/"
jars = [path + jar for jar in os.listdir(idea_home + path) if jar.endswith(".jar")]
jars = [jar for jar in jars if jar not in HIDDEN]
all[platform][plugin] = set(jars)
plugins = sorted(set(all[MAC].keys()) | set(all[WIN].keys()) | set(all[LINUX].keys()))
plugin_jars = {}
plugin_jars[ALL] = {}
plugin_jars[MAC] = {}
plugin_jars[MAC_ARM] = {}
plugin_jars[WIN] = {}
plugin_jars[LINUX] = {}
for p in plugins:
if p in all[LINUX] and p in all[MAC] and p in all[MAC_ARM] and p in all[WIN]:
common = all[LINUX][p] & all[MAC][p] & all[MAC_ARM][p] & all[WIN][p]
else:
common = set()
plugin_jars[ALL][p] = sorted(common)
plugin_jars[MAC][p] = sorted(all[MAC][p] - common) if p in all[MAC] else []
plugin_jars[MAC_ARM][p] = sorted(all[MAC_ARM][p] - common) if p in all[MAC_ARM] else []
plugin_jars[WIN][p] = sorted(all[WIN][p] - common) if p in all[WIN] else []
plugin_jars[LINUX][p] = sorted(all[LINUX][p] - common) if p in all[LINUX] else []
return plugin_jars
def write_spec_file(workspace, sdk_rel, version, sdk_jars, plugin_jars, mac_bundle_name):
suffix = {
ALL: "",
MAC: "_darwin",
MAC_ARM: "_darwin_aarch64",
WIN: "_windows",
LINUX: "_linux",
}
sdk_versions = {}
for platform in [LINUX, WIN, MAC, MAC_ARM]:
sdk_version = intellij.extract_sdk_version(
f'{workspace}{sdk_rel}{HOME_PATHS[platform]}/lib/resources.jar')
sdk_versions[platform] = sdk_version
if len(set(sdk_versions.values())) > 1:
raise ValueError(f'Major and minor versions differ between OS platforms! {sdk_versions}')
sdk_version = sdk_versions[LINUX]
with open(workspace + sdk_rel + "/spec.bzl", "w") as file:
name = version.replace("-", "").replace(".", "_")
file.write("# Auto-generated file, do not edit manually.\n")
file.write(name + " = struct(\n" )
file.write(f' major_version = "{sdk_version.major}",\n')
file.write(f' minor_version = "{sdk_version.minor}",\n')
for platform in [ALL] + PLATFORMS:
file.write(f" jars{suffix[platform]} = [\n")
for jar in sdk_jars[platform]:
file.write(" \"" + jar + "\",\n")
file.write(" ],\n")
for platform in [ALL] + PLATFORMS:
file.write(f" plugin_jars{suffix[platform]} = {{\n")
for plugin, jars in plugin_jars[platform].items():
if jars:
file.write(" \"" + plugin + "\": [\n")
for jar in jars:
file.write(" \"" + os.path.basename(jar) + "\",\n")
file.write(" ],\n")
file.write(" },\n")
file.write(f" mac_bundle_name = \"{mac_bundle_name}\",\n")
file.write(")\n")
# When running in --existing_version mode, the mac bundle name must be extracted
# from the preexisting spec.bzl file (since the original mac bundle artifact
# has already been renamed by this point).
def extract_preexisting_mac_bundle_name(workspace, version):
with open(workspace + "/prebuilts/studio/intellij-sdk/" + version + "/spec.bzl", "r") as spec:
search = re.search(r"mac_bundle_name = \"(.*)\"", spec.read())
return search.group(1) if search else sys.exit("Failed to find existing mac bundle name")
def gen_lib(project_dir, name, jars, srcs):
xml = f'<component name="libraryTable">\n <library name="{name}">\n <CLASSES>\n'
for rel_path in jars:
xml += f' <root url="jar://$PROJECT_DIR$/{rel_path}!/" />\n'
xml += f' </CLASSES>\n <JAVADOC />\n <SOURCES>\n'
for src in srcs:
rel_path = os.path.relpath(src, project_dir)
xml += f' <root url="jar://$PROJECT_DIR$/{rel_path}!/" />\n'
xml += f' </SOURCES>\n </library>\n</component>'
filename = name.replace("-", "_")
with open(project_dir + "/.idea/libraries/" + filename + ".xml", "w") as file:
file.write(xml)
def write_xml_files(workspace, sdk, sdk_jars, plugin_jars):
project_dir = os.path.join(workspace, "tools/adt/idea")
rel_workspace = os.path.relpath(workspace, project_dir)
# Add all jars, IJ will ignore the ones that don't exist
all_jars = sdk_jars[ALL] + sorted(set(sdk_jars[MAC] + sdk_jars[MAC_ARM] + sdk_jars[WIN] + sdk_jars[LINUX]))
paths = [rel_workspace + sdk + "/$SDK_PLATFORM$" + j for j in all_jars]
gen_lib(project_dir, "studio-sdk", paths, [workspace + sdk + "/android-studio-sources.zip"])
lib_dir = project_dir + "/.idea/libraries/"
for lib in os.listdir(lib_dir):
if lib == "studio_plugin_rust.xml":
continue # Special case: the Rust plugin is built separately.
if (lib.startswith("studio_plugin_") and lib.endswith(".xml")) or lib == "intellij_updater.xml":
os.remove(lib_dir + lib)
for plugin, jars in plugin_jars[ALL].items():
add = sorted(set(plugin_jars[WIN][plugin] + plugin_jars[MAC][plugin] + plugin_jars[MAC_ARM][plugin] + plugin_jars[LINUX][plugin]))
paths = [ rel_workspace + sdk + f"/$SDK_PLATFORM$" + j for j in jars + add]
gen_lib(project_dir, "studio-plugin-" + plugin, paths, [workspace + sdk + "/android-studio-sources.zip"])
updater_jar = rel_workspace + sdk + "/updater-full.jar"
if os.path.exists(project_dir + "/" + updater_jar):
gen_lib(project_dir, "intellij-updater", [updater_jar], [workspace + sdk + "/android-studio-sources.zip"])
test_framework_jar = rel_workspace + sdk + "/$SDK_PLATFORM$/lib/testFramework.jar"
gen_lib(project_dir, "intellij-test-framework", [test_framework_jar], [workspace + sdk + "/android-studio-sources.zip"])
def update_files(workspace, version, mac_bundle_name):
sdk = "/prebuilts/studio/intellij-sdk/" + version
sdk_jars = list_sdk_jars(workspace + sdk)
plugin_jars = list_plugin_jars(workspace + sdk)
write_xml_files(workspace, sdk, sdk_jars, plugin_jars)
write_spec_file(workspace, sdk, version, sdk_jars, plugin_jars, mac_bundle_name)
def check_artifacts(dir):
files = sorted(os.listdir(dir))
if not files:
sys.exit("There are no artifacts in " + dir)
regex = re.compile("android-studio-([^.]*)\.(.*)\.([^.-]+)(-sources.zip|.mac.x64-no-jdk.zip|.mac.aarch64-no-jdk.zip|-no-jbr.tar.gz|-no-jbr.win.zip)$")
files = [file for file in files if regex.match(file) or file == "updater-full.jar"]
if not files:
sys.exit("No artifacts found in " + dir)
match = regex.match(files[0])
version_major = match.group(1)
version_minor = match.group(2)
bid = match.group(3)
expected = [
"android-studio-%s.%s.%s-no-jbr.tar.gz" % (version_major, version_minor, bid),
"android-studio-%s.%s.%s-no-jbr.win.zip" % (version_major, version_minor, bid),
"android-studio-%s.%s.%s-sources.zip" % (version_major, version_minor, bid),
"android-studio-%s.%s.%s.mac.aarch64-no-jdk.zip" % (version_major, version_minor, bid),
"android-studio-%s.%s.%s.mac.x64-no-jdk.zip" % (version_major, version_minor, bid),
"updater-full.jar",
]
if files != expected:
print("Expected:")
print(expected)
print("Got:")
print(files)
sys.exit("Unexpected artifacts in " + dir)
manifest = None
manifests = glob.glob(dir + "/manifest_*.xml")
if len(manifests) == 1:
manifest = os.path.basename(manifests[0])
return "AI-" + version_major, files[0], files[1], files[2], files[3], files[4], files[5], manifest
def download(workspace, bid):
fetch_artifact = "/google/data/ro/projects/android/fetch_artifact"
auth_flag = ""
if os.path.exists("/usr/bin/prodcertstatus"):
if os.system("prodcertstatus"):
sys.exit("You need prodaccess to download artifacts")
else:
fetch_artifact = "/usr/bin/fetch_artifact"
auth_flag = "--use_oauth2"
if not os.path.exists(fetch_artifact):
sys.exit("""You need to install fetch_artifact:
sudo glinux-add-repo android stable && \\
sudo apt update && \\
sudo apt install android-fetch-artifact""")
if not bid:
sys.exit("--bid argument needs to be set to download")
dir = tempfile.mkdtemp(prefix="studio_sdk", suffix=bid)
for artifact in ["android-studio-*-sources.zip", "android-studio-*.mac.x64-no-jdk.zip", "android-studio-*.mac.aarch64-no-jdk.zip", "android-studio-*-no-jbr.tar.gz", "android-studio-*-no-jbr.win.zip", "updater-full.jar", "manifest_%s.xml" % bid]:
os.system(
"%s %s --bid %s --target IntelliJ '%s' %s"
% (fetch_artifact, auth_flag, bid, artifact, dir))
return dir
def write_metadata(path, data):
with open(os.path.join(path, "METADATA"), "w") as file:
for k, v in data.items():
file.write(k + ": " + str(v) + "\n")
def extract(workspace, dir, delete_after, metadata):
version, linux, win, sources, mac_arm, mac, updater, manifest = check_artifacts(dir)
path = workspace + "/prebuilts/studio/intellij-sdk/" + version
if os.path.exists(path):
shutil.rmtree(path)
os.mkdir(path)
shutil.copyfile(dir + "/" + sources, path + "/android-studio-sources.zip")
shutil.copyfile(dir + "/" + updater, path + "/updater-full.jar")
print("Unzipping mac distribution...")
# Call to unzip to preserve mac symlinks
os.system("unzip -q -d \"%s\" \"%s\"" % (path + "/darwin", dir + "/" + mac))
print("Unzipping mac aarch64 distribution...")
# Call to unzip to preserve mac symlinks
os.system("unzip -q -d \"%s\" \"%s\"" % (path + "/darwin_aarch64", dir + "/" + mac_arm))
# Mac is the only one that contains the version in the directory, rename for
# consistency with other platforms and easier tooling
apps = ["/darwin/" + app for app in os.listdir(path + "/darwin") if app.startswith("Android Studio")]
if len(apps) != 1:
sys.exit("Only one directory starting with Android Studio expected for Mac")
os.rename(path + apps[0], path + "/darwin/android-studio")
os.rename(path + apps[0].replace("darwin", "darwin_aarch64"), path + "/darwin_aarch64/android-studio")
mac_bundle_name = os.path.basename(apps[0])
print("Unzipping windows distribution...")
with zipfile.ZipFile(dir + "/" + win, "r") as zip:
zip.extractall(path + "/windows")
print("Untaring linux distribution...")
with tarfile.open(dir + "/" + linux, "r") as tar:
tar.extractall(path + "/linux")
if manifest:
xml = ET.parse(dir + "/" + manifest)
for project in xml.getroot().findall("project"):
metadata[project.get("path")] = project.get("revision")
if delete_after:
shutil.rmtree(dir)
write_metadata(path, metadata)
return version, mac_bundle_name
def main(workspace, args):
metadata = {}
mac_bundle_name = None
version = args.version
path = args.path
bid = args.download
delete_path = False
if path:
metadata["path"] = path
if bid:
metadata["build_id"] = bid
path = download(workspace, bid)
delete_path = not args.debug_download
if path:
version, mac_bundle_name = extract(workspace, path, delete_path, metadata)
if args.debug_download:
print("Dowloaded artifacts kept at " + path)
else:
mac_bundle_name = extract_preexisting_mac_bundle_name(workspace, version)
update_files(workspace, version, mac_bundle_name)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--download",
default="",
dest="download",
help="The AB build to download")
parser.add_argument(
"--path",
default="",
dest="path",
help="The path of already downloaded, or locally built, artifacts")
parser.add_argument(
"--existing_version",
default="",
dest="version",
help="The version of an SDK already in prebuilts to update the project's xmls")
parser.add_argument(
"--debug_download",
action="store_true",
dest="debug_download",
help="Keeps the downloaded artifacts for debugging")
workspace = os.path.join(
os.path.dirname(os.path.realpath(__file__)), "../../../..")
args = parser.parse_args()
options = [opt for opt in [args.version, args.download, args.path] if opt]
if len(options) != 1:
print("You must specify only one option")
parser.print_usage()
else:
main(workspace, args)