Add archive and release scripts for Google smali fork

The archive script will archive a version built from a specified
hash and archive it in GCS under r8-releases/smali.

The release script will publish the archive from GCS to Google
Maven.

Bug: b/262205084
Change-Id: I48d38d5ef2fd760e3fab24c0e60fbfaa6c5ee489
diff --git a/tools/archive_smali.py b/tools/archive_smali.py
new file mode 100755
index 0000000..fdfb75c
--- /dev/null
+++ b/tools/archive_smali.py
@@ -0,0 +1,148 @@
+#!/usr/bin/env python3
+# Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+import argparse
+import os
+import re
+try:
+  import resource
+except ImportError:
+  # Not a Unix system. Do what Gandalf tells you not to.
+  pass
+import shutil
+import subprocess
+import sys
+
+import utils
+
+ARCHIVE_BUCKET = 'r8-releases'
+REPO = 'https://github.com/google/smali'
+NO_DRYRUN_OUTPUT = object()
+
+def checkout(temp):
+  subprocess.check_call(['git', 'clone', REPO, temp])
+  return temp
+
+
+def parse_options():
+  result = argparse.ArgumentParser(description='Release Smali')
+  result.add_argument('--archive-hash',
+                      required=True,
+                      metavar=('<main hash>'),
+                      help='The hash to use for archiving a smali build')
+  result.add_argument('--version',
+                      required=True,
+                      metavar=('<version>'),
+                      help='The version of smali to archive.')
+  result.add_argument('--dry-run', '--dry_run',
+      nargs='?',
+      help='Build only, no upload.',
+      metavar='<output directory>',
+      default=None,
+      const=NO_DRYRUN_OUTPUT)
+  result.add_argument('--checkout',
+      help='Use existing checkout.')
+  return result.parse_args()
+
+
+def set_rlimit_to_max():
+  (soft, hard) = resource.getrlimit(resource.RLIMIT_NOFILE)
+  resource.setrlimit(resource.RLIMIT_NOFILE, (hard, hard))
+
+
+def Main():
+  options = parse_options()
+  if not utils.is_bot() and not options.dry_run:
+    raise Exception('You are not a bot, don\'t archive builds. '
+      + 'Use --dry-run to test locally')
+  if options.checkout and not options.dry_run:
+    raise Exception('Using local checkout is only allowed with --dry-run')
+  if not options.checkout and (not options.archive_hash or not options.version):
+    raise Exception('Both --archive-hash and --version are required')
+
+  if utils.is_bot() and not utils.IsWindows():
+    set_rlimit_to_max()
+
+  with utils.TempDir() as temp:
+    # Resolve dry run location to support relative directories.
+    dry_run_output = None
+    if options.dry_run and options.dry_run != NO_DRYRUN_OUTPUT:
+      if not os.path.isdir(options.dry_run):
+        os.mkdir(options.dry_run)
+      dry_run_output = os.path.abspath(options.dry_run)
+
+    checkout_dir = options.checkout if options.checkout else checkout(temp)
+    with utils.ChangedWorkingDirectory(checkout_dir):
+      assert options.archive_hash
+      subprocess.check_call(['git', 'checkout', options.archive_hash])
+
+      # Find version from `build.gradle`.
+      for line in open(os.path.join('build.gradle'), 'r'):
+        result = re.match(
+            r'^version = \'(\d+)\.(\d+)\.(\d+)\'', line)
+        if result:
+          break
+      version = '%s.%s.%s' % (result.group(1), result.group(2), result.group(3))
+      if version != options.version:
+        raise Exception(
+            'Commit % has version %s, expected version %s'
+            % (options.archive_hash, version, options.version))
+      print('Building version: %s' % version)
+
+      # Build release to local Maven repository.
+      m2 = os.path.join(temp, 'm2')
+      os.mkdir(m2)
+      subprocess.check_call(
+          ['./gradlew', '-Dmaven.repo.local=%s' % m2  , 'release', 'publishToMavenLocal'])
+      base = os.path.join('com', 'android', 'tools', 'smali')
+
+      # Check that the local maven repository only has the single version directory in
+      # each artifact directory.
+      for name in ['smali-util', 'smali-dexlib2', 'smali', 'smali-baksmali']:
+        dirnames = next(os.walk(os.path.join(m2, base, name)), (None, None, []))[1]
+        if not dirnames or len(dirnames) != 1 or dirnames[0] != version:
+          raise Exception('Found unexpected directory %s in %s' % (dirnames, name))
+
+      # Build an archive with the relevant content of the local maven repository.
+      m2_filtered = os.path.join(temp, 'm2_filtered')
+      shutil.copytree(m2, m2_filtered, ignore=shutil.ignore_patterns('maven-metadata-local.xml'))
+      maven_release_archive = shutil.make_archive(
+          'smali-maven-release-%s' % version, 'zip', m2_filtered, base)
+
+      # Collect names of the fat jars.
+      fat_jars = list(map(
+        lambda prefix: '%s-%s-fat.jar' % (prefix, version),
+        ['smali/build/libs/smali', 'baksmali/build/libs/baksmali']))
+
+      # Copy artifacts.
+      files = [maven_release_archive]
+      files.extend(fat_jars)
+      if options.dry_run:
+        if dry_run_output:
+          print('Dry run, not actually uploading. Copying to %s:' % dry_run_output)
+          for file in files:
+            destination = os.path.join(dry_run_output, os.path.basename(file))
+            shutil.copyfile(file, destination)
+            print("  %s" % destination)
+        else:
+          print('Dry run, not actually uploading. Generated files:')
+          for file in files:
+            print("  %s" % os.path.basename(file))
+      else:
+        destination_prefix = 'gs://%s/smali/%s' % (ARCHIVE_BUCKET, version)
+        if utils.cloud_storage_exists(destination_prefix):
+          raise Exception('Target archive directory %s already exists' % destination_prefix)
+        for file in files:
+          destination = '%s/%s' % (destination_prefix, os.path.basename(file))
+          if utils.cloud_storage_exists(destination):
+            raise Exception('Target %s already exists' % destination)
+          utils.upload_file_to_cloud_storage(file, destination)
+        public_url = 'https://storage.googleapis.com/%s/smali/%s' % (ARCHIVE_BUCKET, version)
+        print('Artifacts available at: %s' % public_url)
+
+  print("Done!")
+
+if __name__ == '__main__':
+  sys.exit(Main())
diff --git a/tools/gmaven.py b/tools/gmaven.py
new file mode 100644
index 0000000..1ad96c0
--- /dev/null
+++ b/tools/gmaven.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python3
+# Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+import re
+import subprocess
+
+GMAVEN_PUBLISHER = '/google/bin/releases/android-devtools/gmaven/publisher/gmaven-publisher'
+GMAVEN_PUBLISH_STAGE_RELEASE_ID_PATTERN = re.compile('Release ID = ([0-9a-f\-]+)')
+
+
+def publisher_stage(gfiles, dry_run = False):
+  if dry_run:
+    print('Dry-run, would have staged %s' % gfiles)
+    return 'dry-run-release-id'
+
+  print("Staging: %s" % ', '.join(gfiles))
+  print("")
+
+  cmd = [GMAVEN_PUBLISHER, 'stage', '--gfile', ','.join(gfiles)]
+  output = subprocess.check_output(cmd)
+
+  # Expect output to contain:
+  # [INFO] 06/19/2020 09:35:12 CEST: >>>>>>>>>> Staged
+  # [INFO] 06/19/2020 09:35:12 CEST: Release ID = 9171d015-18f6-4a90-9984-1c362589dc1b
+  # [INFO] 06/19/2020 09:35:12 CEST: Stage Path = /bigstore/studio_staging/maven2/sgjesse/9171d015-18f6-4a90-9984-1c362589dc1b
+
+  matches = GMAVEN_PUBLISH_STAGE_RELEASE_ID_PATTERN.findall(output.decode("utf-8"))
+  if matches == None or len(matches) > 1:
+    print("Could not determine the release ID from the gmaven_publisher " +
+          "output. Expected a line with 'Release ID = <release id>'.")
+    print("Output was:")
+    print(output)
+    sys.exit(1)
+
+  print(output)
+
+  release_id = matches[0]
+  return release_id
+
+
+def publisher_stage_redir_test_info(release_id, artifact, dst):
+
+  redir_command = ("/google/data/ro/teams/android-devtools-infra/tools/redir "
+                 + "--alsologtostderr "
+                 + "--gcs_bucket_path=/bigstore/gmaven-staging/${USER}/%s "
+                 + "--port=1480") % release_id
+
+  get_command = ("mvn org.apache.maven.plugins:maven-dependency-plugin:2.4:get "
+                + "-Dmaven.repo.local=/tmp/maven_repo_local "
+                + "-DremoteRepositories=http://localhost:1480 "
+                + "-Dartifact=%s "
+                + "-Ddest=%s") % (artifact, dst)
+
+  print("""To test the staged content with 'redir' run:
+
+%s
+
+Then add the following repository to settings.gradle to search the 'redir'
+repository:
+
+dependencyResolutionManagement {
+  repositories {
+    maven {
+      url 'http://localhost:1480'
+      allowInsecureProtocol true
+    }
+  }
+}
+
+and add the following repository to gradle.build for for the staged version:
+
+dependencies {
+  implementation('%s') {
+    changing = true
+  }
+}
+
+Use this commands to get artifact from 'redir':
+
+rm -rf /tmp/maven_repo_local
+%s
+""" % (redir_command, artifact, get_command))
+
+
+def publisher_publish(release_id, dry_run = False):
+  if dry_run:
+    print('Dry-run, would have published %s' % release_id)
+    return
+
+  cmd = [GMAVEN_PUBLISHER, 'publish', release_id]
+  output = subprocess.check_output(cmd)
diff --git a/tools/release_smali.py b/tools/release_smali.py
new file mode 100755
index 0000000..12a9389
--- /dev/null
+++ b/tools/release_smali.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python3
+# Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+import argparse
+import sys
+
+import gmaven
+
+ARCHIVE_BUCKET = 'r8-releases'
+REPO = 'https://github.com/google/smali'
+
+def parse_options():
+  result = argparse.ArgumentParser(description='Release Smali')
+  result.add_argument('--version',
+                      required=True,
+                      metavar=('<version>'),
+                      help='The version of smali to release.')
+  result.add_argument('--dry-run',
+                      default=False,
+                      action='store_true',
+                      help='Only perform non-commiting tasks and print others.')
+  return result.parse_args()
+
+
+def Main():
+  options = parse_options()
+  gfile = ('/bigstore/r8-releases/smali/%s/smali-maven-release-%s.zip'
+       % (options.version, options.version))
+  release_id = gmaven.publisher_stage([gfile], options.dry_run)
+
+  print('Staged Release ID %s.\n' % release_id)
+  gmaven.publisher_stage_redir_test_info(
+      release_id, 'com.android.tools.smali:smali:%s' % options.version, 'smali.jar')
+
+  print()
+  answer = input('Continue with publishing [y/N]:')
+
+  if answer != 'y':
+    print('Aborting release to Google maven')
+    sys.exit(1)
+
+  gmaven.publisher_publish(release_id, options.dry_run)
+
+  print()
+  print('Published. Use the email workflow for approval.')
+
+if __name__ == '__main__':
+  sys.exit(Main())