From 39b9f0e956b7967d118873ee2e365fe6a02a029b Mon Sep 17 00:00:00 2001 From: Diego Alonso Marquez Palacios Date: Tue, 31 Oct 2023 19:19:41 -0400 Subject: [PATCH] feat: `generate_library.sh` with postprocessing (#1951) * feat: add * generate gapic and proto folder * refactor utilities * add an action to verify * checkout googleapis-gen * setup repo name * add commit hash of googleapis-gen * change secret * change token * change to git clone * change user name * add input list * include resources folder in main * remove grpc version in `*ServiceGrpc.java` * change destination path * compare generation result with googleapis-gen * fix type in diff command * checkout repo using checkout action * checkout repos as nested repo * sparse checkout googleapis * Revert "sparse checkout googleapis" This reverts commit 3d612f8e4949251d7d97070f507c31ff4170d85a. * change library * change step name * add a readme * make grpc version optional * make protobuf version optional * checkout master branch, rather than a commit hash * allow snapshot version of generator * download snapshot of generator parent pom * update README * download generator and grpc using mvn * change error message * add maven central mirror * add comments in utilities * add comments * add an integration test * fail fast if no file is found * do not delete google/ * get protobuf version from WORKSPACE * add instructions on download `google/` from googleapis * add comments * update description of `destination_path` * update comments * download dependencies using `curl` * increase download time * remove comment * add samples directory in readme * remove prerequisite about `proto_path` * add explanation in prerequisite * add example to generate showcase * add a comment * wip adaptations * add owlbot.py template * run owlbot docker image * fix consolidate config * move owlbot call to its own function * move postprocessing logic * prepare integration test for gh workflow * fix local dev script * post-merge fixes * fix test script and IT * fix parent poms * start fixing samples problem * fix samples folder transfer * cleanup, prepare IT workflow * cleanup ii, sparse clone monorepo * delete preserve script * clean unnecessary lines * infer owlbot sha * add template file * remove newline from owlbot template * chore: newline correction * use stderr for error messages * fix script documentation * function comments * quoting variables * format constant * fix sparse checkout of monorepo * include location to googleapis sparse clone * remove unnecessary parent pom setting * remove consolidate_config.sh * exclude changelog and owlbot copy files from diff check * fixes after merge * include .github in monorepo sparse clone * restore `set_parent_pom.sh` * restore `consolidate_config.sh` * correct parameter resolution * use separate variable for version * postprocessing to use separate versions * remove old IT file * post-merge fixes * enable post-processing by default * post-merge fixes * post-merge fixes * post merge fixes * add script to compare poms * post-merge fixes * post-merge fixes ii * fix pom comparison * include pre-existing poms before running owlbot * change owlbot-staging suffix folder to run owlbot.py * fix newline removal in owlbot.py * split git diff command * enable tests for HW libraries * generate all hw libs except bigtable * all libraries passing * fix unit tests * repo metadata json logic cleanup * remove new library scripts * fix googleapis-gen tests * fix post-processing it * magic empty commit * correct conflict string * use os agnostic string replacement * comments and cleanup on postprocessing * cleanup of IT * temp: use custom gapic library name * use owl-bot-copy * remove api_version logic * remove custom_gapic_name in favor of owl-bot-copy * remove unnecessary new library flag * fix folder name test * remove unnecessary util function * remove unnecessary utils script dir var * rename postprocessing folder, apply_current_versions comment * fix postprocessing comments * correct popd folder name to its variable name * unnecessary sed command * skip generation if more versions coming * do not stage previous versions in owl-bot-staging * do not use custom repo metadatas * reset workspace folder * remove unnecessary owlbot yaml copy * modify readme * expand README instructions * examples for both pre and post processing * exclude new library owlbot.py template * do not process HW libraries * success message, folder navigation fix * set git author * add docker to workflow * lint fix * custom docker step for macos * do not postprocess showcase * os-dependent pom comparison * add python to workflow * explicit python version * add debugging output for compare_poms * correct xargs for macos * remove debug checkpoints * clean compare_poms.py * concise else logic * infer destination_path * add generation times * remove unused transport and include_samples from postprocessing * use versions.txt at root of owlbot postprocessor fs * modify success message * remove unused version processing script * remove owlbot_sha and repo_metadata args * use built-in docker images * manual install of docker ii * manual install of docker iii * manual install of docker iv * manual install of docker v * manual install of docker vi * manual install of docker vii * manual install of docker viii * manual install of docker ix * versions.txt as an argument * fix exit code in time tracking * fix readme * remove unused options * fix macos docker install * do not use cask to install docker * test custom user id in docker run * correct time tracking entry * change postprocessing file structture * move helper postprocess funcs to utilities.sh * add unit tests for postprocess utils * remove repository_path * fix workspace creation logic * fix readme * transfer from workspace to destination path * include folder structure for p.p. libs in readme * omit pre-processed folders * omit package-info.java * fix documentation argument order * fix preparation of copy-code source folder * add unit test for copy_directory_if_exists * fix wrong args to cp * change test monorepo folder names --------- Co-authored-by: JoeWang1127 --- .../workflows/verify_library_generation.yaml | 27 ++- library_generation/README.md | 68 ++++++- library_generation/generate_library.sh | 59 +++++- library_generation/postprocess_library.sh | 101 ++++++++++ library_generation/test/compare_poms.py | 116 ++++++++++++ .../test/generate_library_integration_test.sh | 174 ++++++++++++++---- .../test/generate_library_unit_tests.sh | 56 ++++++ .../test/resources/proto_path_list.txt | 39 ++-- .../test-monorepo/.github/.OwlBot.lock.yaml | 17 ++ .../test-service/.repo-metadata.json | 18 ++ library_generation/test/test_utilities.sh | 40 ++++ library_generation/utilities.sh | 54 +++++- showcase/scripts/generate_showcase.sh | 1 + 13 files changed, 704 insertions(+), 66 deletions(-) create mode 100755 library_generation/postprocess_library.sh create mode 100644 library_generation/test/compare_poms.py create mode 100644 library_generation/test/resources/test-monorepo/.github/.OwlBot.lock.yaml create mode 100644 library_generation/test/resources/test-monorepo/test-service/.repo-metadata.json diff --git a/.github/workflows/verify_library_generation.yaml b/.github/workflows/verify_library_generation.yaml index 5a361828d7..521f586c3d 100644 --- a/.github/workflows/verify_library_generation.yaml +++ b/.github/workflows/verify_library_generation.yaml @@ -14,6 +14,7 @@ jobs: matrix: java: [ 8 ] os: [ ubuntu-22.04, macos-12 ] + post_processing: [ 'true', 'false' ] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 @@ -22,11 +23,35 @@ jobs: java-version: ${{ matrix.java }} distribution: temurin cache: maven + - uses: actions/setup-python@v4 + with: + python-version: '3.11' + - name: install docker (ubuntu) + if: matrix.os == 'ubuntu-22.04' + run: | + set -x + # install docker + sudo apt install containerd -y + sudo apt install -y docker.io docker-compose + + # launch docker + sudo systemctl start docker + - name: install docker (macos) + if: matrix.os == 'macos-12' + run: | + brew update --preinstall + brew install docker docker-compose qemu + brew upgrade qemu + colima start + docker run --user $(id -u):$(id -g) --rm hello-world - name: Run integration tests run: | set -x + git config --global user.email "github-workflow@github.com" + git config --global user.name "Github Workflow" library_generation/test/generate_library_integration_test.sh \ - --googleapis_gen_url https://cloud-java-bot:${{ secrets.CLOUD_JAVA_BOT_GITHUB_TOKEN }}@github.com/googleapis/googleapis-gen.git + --googleapis_gen_url https://cloud-java-bot:${{ secrets.CLOUD_JAVA_BOT_GITHUB_TOKEN }}@github.com/googleapis/googleapis-gen.git \ + --enable_postprocessing "${{ matrix.post_processing }}" unit_tests: strategy: matrix: diff --git a/library_generation/README.md b/library_generation/README.md index 7a2fe32e3e..c2e9826b5b 100644 --- a/library_generation/README.md +++ b/library_generation/README.md @@ -21,6 +21,13 @@ In order to generate a GAPIC library, you need to pull `google/` from [googleapi and put it into `output` since protos in `google/` are likely referenced by protos from which the library are generated. +In order to generate a post-processed GAPIC library, you need to pull the +original repository (i.e. google-cloud-java) and pass the monorepo as +`destination_path` (e.g. `google-cloud-java/java-asset`). +This repository will be the source of truth for pre-existing +pom.xml files, owlbot.py and .OwlBot.yaml files. See the option belows for +custom postprocessed generations (e.g. custom `versions.txt` file). + ## Parameters to run `generate_library.sh` You need to run the script with the following parameters. @@ -40,7 +47,7 @@ Use `-d` or `--destination_path` to specify the value. Note that you do not need to create `$destination_path` beforehand. -The directory structure of the generated library is +The directory structure of the generated library _withtout_ postprocessing is ``` $destination_path |_gapic-* @@ -65,7 +72,35 @@ $destination_path ``` You can't build the library as-is since it does not have `pom.xml` or `build.gradle`. To use the library, copy the generated files to the corresponding directory -of a library repository, e.g., `google-cloud-java`. +of a library repository, e.g., `google-cloud-java` or use the +`enable_postprocessing` flag on top of a pre-existing generated library to +produce the necessary pom files. + +For `asset/v1` the directory structure of the generated library _with_ postprocessing is +``` + +├── google-cloud-asset +│   └── src +│   ├── main +│   │   ├── java +│   │   └── resources +│   └── test +│   └── java +├── google-cloud-asset-bom +├── grpc-google-cloud-asset-v* +│   └── src +│   └── main +│   └── java +├── proto-google-cloud-asset-v* +│   └── src +│   └── main +│   ├── java +│   └── proto +└── samples + └── snippets + └── generated + +``` ### gapic_generator_version You can find the released version of gapic-generator-java in [maven central](https://repo1.maven.org/maven2/com/google/api/gapic-generator-java/). @@ -150,8 +185,33 @@ Use `--include_samples` to specify the value. Choose the protoc binary type from https://github.com/protocolbuffers/protobuf/releases. Default is "linux-x86_64". -## An example to generate a client library +### enable_postprocessing (optional) +Whether to enable the post-processing steps (usage of owlbot) in the generation +of this library +Default is "true". + +### versions_file (optional) +It must point to a versions.txt file containing the versions the post-processed +poms will have. It is required when `enable_postprocessing` is `"true"` + + +## An example to generate a non post-processed client library +```bash +library_generation/generate_library.sh \ +-p google/cloud/confidentialcomputing/v1 \ +-d google-cloud-confidentialcomputing-v1-java \ +--gapic_generator_version 2.24.0 \ +--protobuf_version 23.2 \ +--grpc_version 1.55.1 \ +--gapic_additional_protos "google/cloud/common_resources.proto google/cloud/location/locations.proto" \ +--transport grpc+rest \ +--rest_numeric_enums true \ +--enable_postprocessing false \ +--include_samples true ``` + +## An example to generate a library with postprocessing +```bash library_generation/generate_library.sh \ -p google/cloud/confidentialcomputing/v1 \ -d google-cloud-confidentialcomputing-v1-java \ @@ -161,5 +221,7 @@ library_generation/generate_library.sh \ --gapic_additional_protos "google/cloud/common_resources.proto google/cloud/location/locations.proto" \ --transport grpc+rest \ --rest_numeric_enums true \ +--enable_postprocessing true \ +--versions_file "path/to/versions.txt" \ --include_samples true ``` diff --git a/library_generation/generate_library.sh b/library_generation/generate_library.sh index 04e3fbe860..7df2b226e9 100755 --- a/library_generation/generate_library.sh +++ b/library_generation/generate_library.sh @@ -1,7 +1,6 @@ #!/usr/bin/env bash set -eo pipefail -set -x # parse input parameters while [[ $# -gt 0 ]]; do @@ -61,10 +60,18 @@ case $key in include_samples="$2" shift ;; + --enable_postprocessing) + enable_postprocessing="$2" + shift + ;; --os_architecture) os_architecture="$2" shift ;; + --versions_file) + versions_file="$2" + shift + ;; *) echo "Invalid option: [$1]" exit 1 @@ -74,6 +81,7 @@ shift # past argument or value done script_dir=$(dirname "$(readlink -f "$0")") +# source utility functions source "${script_dir}"/utilities.sh output_folder="$(get_output_folder)" @@ -117,17 +125,20 @@ if [ -z "${include_samples}" ]; then include_samples="true" fi +if [ -z "$enable_postprocessing" ]; then + enable_postprocessing="true" +fi + if [ -z "${os_architecture}" ]; then os_architecture=$(detect_os_architecture) fi - mkdir -p "${output_folder}/${destination_path}" ##################### Section 0 ##################### # prepare tooling ##################################################### # the order of services entries in gapic_metadata.json is relevant to the -# order of proto file, sort the proto files with respect to their name to +# order of proto file, sort the proto files with respect to their bytes to # get a fixed order. folder_name=$(extract_folder_name "${destination_path}") pushd "${output_folder}" @@ -137,7 +148,7 @@ case "${proto_path}" in find_depth="-maxdepth 1" ;; esac -proto_files=$(find "${proto_path}" ${find_depth} -type f -name "*.proto" | sort) +proto_files=$(find "${proto_path}" ${find_depth} -type f -name "*.proto" | LC_COLLATE=C sort) # include or exclude certain protos in grpc plugin and gapic generator java. case "${proto_path}" in "google/cloud") @@ -280,5 +291,41 @@ popd # output_folder ##################################################### pushd "${output_folder}/${destination_path}" rm -rf java_gapic_srcjar java_gapic_srcjar_raw.srcjar.zip java_grpc.jar java_proto.jar temp-codegen.srcjar -popd -set +x +popd # destination path +##################### Section 5 ##################### +# post-processing +##################################################### +if [ "${enable_postprocessing}" != "true" ]; +then + echo "post processing is disabled" + exit 0 +fi +if [ -z "${versions_file}" ];then + echo "no versions.txt argument provided. Please provide one in order to enable post-processing" + exit 1 +fi +workspace="${output_folder}/workspace" +if [ -d "${workspace}" ]; then + rm -rdf "${workspace}" +fi + +mkdir -p "${workspace}" + +bash -x "${script_dir}/postprocess_library.sh" "${workspace}" \ + "${script_dir}" \ + "${destination_path}" \ + "${proto_path}" \ + "${versions_file}" \ + "${output_folder}" + +# for post-procesed libraries, remove pre-processed folders +pushd "${output_folder}/${destination_path}" +rm -rdf "proto-${folder_name}" +rm -rdf "grpc-${folder_name}" +rm -rdf "gapic-${folder_name}" +if [ "${include_samples}" == "false" ]; then + rm -rdf "samples" +fi +popd # output_folder +# move contents of the post-processed library into destination_path +cp -r ${workspace}/* "${output_folder}/${destination_path}" diff --git a/library_generation/postprocess_library.sh b/library_generation/postprocess_library.sh new file mode 100755 index 0000000000..880139db73 --- /dev/null +++ b/library_generation/postprocess_library.sh @@ -0,0 +1,101 @@ +#!/bin/bash +# +# Main functions to interact with owlbot post-processor and postprocessing +# scripts + + +# Runs the owlbot post-processor docker image. The resulting post-processed +# library gets stored in `${output_folder}/workspace` +# Arguments +# 1 - workspace: the location of the grpc,proto and gapic libraries to be +# processed +# 2 - scripts_root: location of the generation scripts +# 3 - destination_path: used to transfer the raw grpc, proto and gapic libraries +# 4 - proto_path: googleapis path of the library. This is used to prepare the +# folder structure to run `owlbot-cli copy-code` +# 5 - versions_file: path to file containing versions to be applied to the poms +# 6 - output_folder: main workspace of the generation process + +workspace=$1 +scripts_root=$2 +destination_path=$3 +proto_path=$4 +versions_file=$5 +output_folder=$6 + +source "${scripts_root}"/utilities.sh + +repository_root=$(echo "${destination_path}" | cut -d/ -f1) +repo_metadata_json_path=$(get_repo_metadata_json "${destination_path}" "${output_folder}") +owlbot_sha=$(get_owlbot_sha "${output_folder}" "${repository_root}") + +# read or infer owlbot sha + +cp "${repo_metadata_json_path}" "${workspace}"/.repo-metadata.json + +# call owl-bot-copy +owlbot_staging_folder="${workspace}/owl-bot-staging" +mkdir -p "${owlbot_staging_folder}" +owlbot_postprocessor_image="gcr.io/cloud-devrel-public-resources/owlbot-java@sha256:${owlbot_sha}" + + + +# copy existing pom, owlbot and version files if the source of truth repo is present +# pre-processed folders are ommited +if [[ -d "${output_folder}/${destination_path}" ]]; then + rsync -avm \ + --include='*/' \ + --include='*.xml' \ + --include='owlbot.py' \ + --include='.OwlBot.yaml' \ + --exclude='*' \ + "${output_folder}/${destination_path}/" \ + "${workspace}" +fi + +echo 'Running owl-bot-copy' +pre_processed_libs_folder="${output_folder}/pre-processed" +# By default (thanks to generation templates), .OwlBot.yaml `deep-copy` section +# references a wildcard pattern matching a folder +# ending with `-java` at the leaf of proto_path. +mkdir -p "${pre_processed_libs_folder}/${proto_path}/generated-java" +folder_name=$(extract_folder_name "${destination_path}") +copy_directory_if_exists "${output_folder}/${destination_path}/proto-${folder_name}" \ + "${pre_processed_libs_folder}/${proto_path}/generated-java/proto-google-cloud-${folder_name}" +copy_directory_if_exists "${output_folder}/${destination_path}/grpc-${folder_name}" \ + "${pre_processed_libs_folder}/${proto_path}/generated-java/grpc-google-cloud-${folder_name}" +copy_directory_if_exists "${output_folder}/${destination_path}/gapic-${folder_name}" \ + "${pre_processed_libs_folder}/${proto_path}/generated-java/gapic-google-cloud-${folder_name}" +copy_directory_if_exists "${output_folder}/${destination_path}/samples" \ + "${pre_processed_libs_folder}/${proto_path}/generated-java/samples" +pushd "${pre_processed_libs_folder}" +# create an empty repository so owl-bot-copy can process this as a repo +# (cannot process non-git-repositories) +git init +git commit --allow-empty -m 'empty commit' +popd # pre_processed_libs_folder + +docker run --rm \ + --user $(id -u):$(id -g) \ + -v "${workspace}:/repo" \ + -v "${pre_processed_libs_folder}:/pre-processed-libraries" \ + -w /repo \ + --env HOME=/tmp \ + gcr.io/cloud-devrel-public-resources/owlbot-cli:latest \ + copy-code \ + --source-repo-commit-hash=none \ + --source-repo=/pre-processed-libraries \ + --config-file=.OwlBot.yaml + + +echo 'running owl-bot post-processor' +versions_file_arg="" +if [ -f "${versions_file}" ];then + versions_file_arg="-v ${versions_file}:/versions.txt" +fi +# run the postprocessor +docker run --rm \ + -v "${workspace}:/workspace" \ + ${versions_file_arg} \ + --user $(id -u):$(id -g) \ + "${owlbot_postprocessor_image}" diff --git a/library_generation/test/compare_poms.py b/library_generation/test/compare_poms.py new file mode 100644 index 0000000000..c2abd8da13 --- /dev/null +++ b/library_generation/test/compare_poms.py @@ -0,0 +1,116 @@ +""" +Utility to compare the contents of two XML files. +This focuses on the tree structure of both XML files, meaning that element order and whitespace will be disregarded. +The only comparison points are: element path (e.g. project/dependencies) and element text +There is a special case for `dependency`, where the maven coordinates are prepared as well +""" + +import sys +import xml.etree.ElementTree as ET +from collections import Counter + +""" +prints to stderr +""" +def eprint(*args, **kwargs): + print(*args, file=sys.stderr, **kwargs) + +""" +Convenience method to access a node's child elements via path and get its text +""" +def get_text_from_element(node, element_name, namespace): + child = node.find(namespace + element_name) + return child.text if child is not None else '' + +""" +Convenience method to pretty print the contents of a Counter (or dict) +""" +def print_counter(counter): + for key, value in counter.items(): + eprint(f'{key}: {value}') + +""" +Recursively traverses a node tree and appends element text to a given +`elements` array. If the element tag is `dependency` +then the maven coordinates for its children will be computed as well +""" +def append_to_element_list(node, path, elements): + namespace_start, namespace_end, tag_name = node.tag.rpartition('}') + namespace = namespace_start + namespace_end + if tag_name == 'dependency': + group_id = get_text_from_element(node, 'groupId', namespace) + artifact_id = get_text_from_element(node, 'artifactId', namespace) + artifact_str = '' + artifact_str += group_id + artifact_str += ':' + artifact_id + elements.append(path + '/' + tag_name + '=' + artifact_str) + if node.text and len(node.text.strip()) > 0: + elements.append(path + '/' + tag_name + '=' + node.text) + + if tag_name == 'version': + # versions may be yet to be processed, we disregard them + return elements + + for child in node: + child_path = path + '/' + tag_name + append_to_element_list(child, child_path, elements) + + return elements + +""" +compares two XMLs for content differences +the argument print_whole_trees determines if both trees should be printed +""" +def compare_xml(file1, file2, print_whole_trees): + try: + tree1 = ET.parse(file1) + tree2 = ET.parse(file2) + except ET.ParseError as e: + eprint(f'Error parsing XML') + raise e + except FileNotFoundError as e: + eprint(f'Error reading file') + raise e + + tree1_elements = [] + tree2_elements = [] + + append_to_element_list(tree1.getroot(), '/', tree1_elements) + append_to_element_list(tree2.getroot(), '/', tree2_elements) + + tree1_counter = Counter(tree1_elements) + tree2_counter = Counter(tree2_elements) + intersection = tree1_counter & tree2_counter + only_in_tree1 = tree1_counter - intersection + only_in_tree2 = tree2_counter - intersection + if print_whole_trees == 'true': + eprint('tree1') + print_counter(tree2_counter) + eprint('tree2') + print_counter(tree1_counter) + if len(only_in_tree1) > 0 or len(only_in_tree2) > 0: + eprint('only in ' + file1) + print_counter(only_in_tree1) + eprint('only in ' + file2) + print_counter(only_in_tree2) + return True + return False + + +if __name__ == "__main__": + if len(sys.argv) != 4: + eprint("Usage: python compare_xml.py ") + sys.exit(1) + + file1 = sys.argv[1] + file2 = sys.argv[2] + print_whole_trees = sys.argv[3] + has_diff = compare_xml(file1, file2, print_whole_trees) + + if has_diff: + eprint(f'The poms are different') + sys.exit(1) + eprint('The XML files are the same.') + sys.exit(0) + + diff --git a/library_generation/test/generate_library_integration_test.sh b/library_generation/test/generate_library_integration_test.sh index c4ea9fde25..64b526e47c 100755 --- a/library_generation/test/generate_library_integration_test.sh +++ b/library_generation/test/generate_library_integration_test.sh @@ -3,34 +3,51 @@ set -xeo pipefail # This script is used to test the result of `generate_library.sh` against generated -# source code in googleapis-gen repository. +# source code in the specified repository. # Specifically, this script will do # 1. checkout the master branch of googleapis/google and WORKSPACE # 2. parse version of gapic-generator-java, protobuf and grpc from WORKSPACE # 3. generate a library with proto_path and destination_path in a proto_path # list by invoking `generate_library.sh`. GAPIC options to generate a library # will be parsed from proto_path/BUILD.bazel. -# 4. checkout the master branch googleapis-gen repository and compare the result. +# 4. depending on whether postprocessing is enabled, +# 4.1 checkout the master branch of googleapis-gen repository and compare the result, or +# 4.2 checkout the master branch of google-cloud-java or HW library repository and compare the result # defaults googleapis_gen_url="git@github.com:googleapis/googleapis-gen.git" +enable_postprocessing="true" + script_dir=$(dirname "$(readlink -f "$0")") proto_path_list="${script_dir}/resources/proto_path_list.txt" library_generation_dir="${script_dir}"/.. source "${script_dir}/test_utilities.sh" +source "${script_dir}/../utilities.sh" output_folder="$(pwd)/output" while [[ $# -gt 0 ]]; do key="$1" case $key in - --proto_path_list) + -p|--proto_path_list) proto_path_list="$2" shift ;; - --googleapis_gen_url) + -e|--enable_postprocessing) + enable_postprocessing="$2" + shift + ;; + -s|--owlbot_sha) + owlbot_sha="$2" + shift + ;; + -g|--googleapis_gen_url) googleapis_gen_url="$2" shift ;; + -v|--versions_file) + versions_file="$2" + shift + ;; *) echo "Invalid option: [$1]" exit 1 @@ -39,7 +56,6 @@ esac shift # past argument or value done -library_generation_dir="${script_dir}"/.. mkdir -p "${output_folder}" pushd "${output_folder}" # checkout the master branch of googleapis/google (proto files) and WORKSPACE @@ -57,12 +73,25 @@ protobuf_version=$(get_version_from_WORKSPACE "protobuf-" WORKSPACE "-") echo "The version of protobuf is ${protobuf_version}" popd # googleapis popd # output_folder +if [ -f "${output_folder}/generation_times" ];then + rm "${output_folder}/generation_times" +fi +if [ -z "${versions_file}" ]; then + # google-cloud-java will be downloaded before each call of + # `generate_library.sh` + versions_file="${output_folder}/google-cloud-java/versions.txt" +fi grep -v '^ *#' < "${proto_path_list}" | while IFS= read -r line; do proto_path=$(echo "$line" | cut -d " " -f 1) - destination_path=$(echo "$line" | cut -d " " -f 2) - # parse GAPIC options from proto_path/BUILD.bazel + repository_path=$(echo "$line" | cut -d " " -f 2) + is_handwritten=$(echo "$line" | cut -d " " -f 3) + # parse destination_path pushd "${output_folder}" + echo "Checking out googleapis-gen repository..." + sparse_clone "${googleapis_gen_url}" "${proto_path}" + destination_path=$(compute_destination_path "${proto_path}" "${output_folder}") + # parse GAPIC options from proto_path/BUILD.bazel proto_build_file_path="${proto_path}/BUILD.bazel" proto_only=$(get_proto_only_from_BUILD "${proto_build_file_path}") gapic_additional_protos=$(get_gapic_additional_protos_from_BUILD "${proto_build_file_path}") @@ -80,39 +109,114 @@ grep -v '^ *#' < "${proto_path_list}" | while IFS= read -r line; do service_config=${service_config}, service_yaml=${service_yaml}, include_samples=${include_samples}." + pushd "${output_folder}" + if [ "${is_handwritten}" == "true" ]; then + echo 'this is a handwritten library' + popd # output folder + continue + else + echo 'this is a monorepo library' + sparse_clone "https://github.com/googleapis/google-cloud-java.git" "${repository_path} google-cloud-pom-parent google-cloud-jar-parent versions.txt .github" + # compute path from output_folder to source of truth library location + # (e.g. google-cloud-java/java-compute) + repository_path="google-cloud-java/${repository_path}" + target_folder="${output_folder}/${repository_path}" + popd # output_folder + fi # generate GAPIC client library echo "Generating library from ${proto_path}, to ${destination_path}..." - "${library_generation_dir}"/generate_library.sh \ - -p "${proto_path}" \ - -d "${destination_path}" \ - --gapic_generator_version "${gapic_generator_version}" \ - --protobuf_version "${protobuf_version}" \ - --proto_only "${proto_only}" \ - --gapic_additional_protos "${gapic_additional_protos}" \ - --transport "${transport}" \ - --rest_numeric_enums "${rest_numeric_enums}" \ - --gapic_yaml "${gapic_yaml}" \ - --service_config "${service_config}" \ - --service_yaml "${service_yaml}" \ - --include_samples "${include_samples}" - echo "Generate library finished." - echo "Checking out googleapis-gen repository..." + generation_start=$(date "+%s") + if [ $enable_postprocessing == "true" ]; then + if [[ "${repository_path}" == "null" ]]; then + # we need a repository to compare the generated results with. Skip this + # library + continue + fi + "${library_generation_dir}"/generate_library.sh \ + -p "${proto_path}" \ + -d "${repository_path}" \ + --gapic_generator_version "${gapic_generator_version}" \ + --protobuf_version "${protobuf_version}" \ + --proto_only "${proto_only}" \ + --gapic_additional_protos "${gapic_additional_protos}" \ + --transport "${transport}" \ + --rest_numeric_enums "${rest_numeric_enums}" \ + --gapic_yaml "${gapic_yaml}" \ + --service_config "${service_config}" \ + --service_yaml "${service_yaml}" \ + --include_samples "${include_samples}" \ + --enable_postprocessing "true" \ + --versions_file "${output_folder}/google-cloud-java/versions.txt" + else + "${library_generation_dir}"/generate_library.sh \ + -p "${proto_path}" \ + -d "${destination_path}" \ + --gapic_generator_version "${gapic_generator_version}" \ + --protobuf_version "${protobuf_version}" \ + --proto_only "${proto_only}" \ + --gapic_additional_protos "${gapic_additional_protos}" \ + --transport "${transport}" \ + --rest_numeric_enums "${rest_numeric_enums}" \ + --gapic_yaml "${gapic_yaml}" \ + --service_config "${service_config}" \ + --service_yaml "${service_yaml}" \ + --include_samples "${include_samples}" \ + --enable_postprocessing "false" + fi + generation_end=$(date "+%s") + # some generations are less than 1 second (0 produces exit code 1 in `expr`) + generation_duration_seconds=$(expr "${generation_end}" - "${generation_start}" || true) + echo "Generation time for ${repository_path} was ${generation_duration_seconds} seconds." + pushd "${output_folder}" + echo "${proto_path} ${generation_duration_seconds}" >> generation_times + echo "Generate library finished." echo "Compare generation result..." - pushd "${output_folder}" - sparse_clone "${googleapis_gen_url}" "${proto_path}/${destination_path}" - RESULT=0 - # include gapic_metadata.json and package-info.java after - # resolving https://github.com/googleapis/sdk-platform-java/issues/1986 - diff -r "googleapis-gen/${proto_path}/${destination_path}" "${output_folder}/${destination_path}" -x "*gradle*" -x "gapic_metadata.json" -x "package-info.java" || RESULT=$? + if [ $enable_postprocessing == "true" ]; then + echo "Checking out repository..." + pushd "${target_folder}" + SOURCE_DIFF_RESULT=0 + git diff \ + --ignore-space-at-eol \ + -r \ + --exit-code \ + -- \ + ':!*pom.xml' \ + ':!*README.md' \ + ':!*package-info.java' \ + || SOURCE_DIFF_RESULT=$? - if [ ${RESULT} == 0 ] ; then - echo "SUCCESS: Comparison finished, no difference is found." - else - echo "FAILURE: Differences found in proto path: ${proto_path}." - exit "${RESULT}" + POM_DIFF_RESULT=$(compare_poms "${target_folder}") + popd # target_folder + if [[ ${SOURCE_DIFF_RESULT} == 0 ]] && [[ ${POM_DIFF_RESULT} == 0 ]] ; then + echo "SUCCESS: Comparison finished, no difference is found." + # Delete google-cloud-java to allow a sparse clone of the next library + rm -rdf google-cloud-java + elif [ ${SOURCE_DIFF_RESULT} != 0 ]; then + echo "FAILURE: Differences found in proto path: ${proto_path}." + exit "${SOURCE_DIFF_RESULT}" + elif [ ${POM_DIFF_RESULT} != 0 ]; then + echo "FAILURE: Differences found in generated poms" + exit "${POM_DIFF_RESULT}" + fi + elif [ $enable_postprocessing == "false" ]; then + # include gapic_metadata.json and package-info.java after + # resolving https://github.com/googleapis/sdk-platform-java/issues/1986 + SOURCE_DIFF_RESULT=0 + diff --strip-trailing-cr -r "googleapis-gen/${proto_path}/${destination_path}" "${output_folder}/${destination_path}" \ + -x "*gradle*" \ + -x "gapic_metadata.json" \ + -x "package-info.java" || SOURCE_DIFF_RESULT=$? + if [ ${SOURCE_DIFF_RESULT} == 0 ] ; then + echo "SUCCESS: Comparison finished, no difference is found." + else + echo "FAILURE: Differences found in proto path: ${proto_path}." + exit "${SOURCE_DIFF_RESULT}" + fi fi + popd # output_folder done - -rm -rf "${output_folder}" +echo "ALL TESTS SUCCEEDED" +echo "generation times in seconds (does not consider repo checkout):" +cat "${output_folder}/generation_times" diff --git a/library_generation/test/generate_library_unit_tests.sh b/library_generation/test/generate_library_unit_tests.sh index 8fc94ce94b..8a6ef0f42d 100755 --- a/library_generation/test/generate_library_unit_tests.sh +++ b/library_generation/test/generate_library_unit_tests.sh @@ -305,6 +305,56 @@ get_version_from_valid_WORKSPACE_test() { assertEquals '2.25.1-SNAPSHOT' "${obtained_ggj_version}" } +get_repo_metadata_json_valid_repo_succeeds() { + local output_folder="${script_dir}/resources" + local repository_path="test-monorepo/test-service" + local repo_metadata_json=$(get_repo_metadata_json "${repository_path}" "${output_folder}") + assertEquals "${output_folder}/${repository_path}/.repo-metadata.json" \ + "${repo_metadata_json}" +} + +get_repo_metadata_json_invalid_repo_fails() { + local output_folder="${script_dir}/resources" + local repository_path="test-monorepo/java-nonexistent" + $(get_repo_metadata_json "${repository_path}" "${output_folder}") || res=$? + assertEquals 1 ${res} +} + +get_owlbot_sha_valid_repo_succeeds() { + local output_folder="${script_dir}/resources" + local repository_root="test-monorepo" + local owlbot_sha=$(get_owlbot_sha "${output_folder}" "${repository_root}") + assertEquals 'fb7584f6adb3847ac480ed49a4bfe1463965026b2919a1be270e3174f3ce1191' \ + "${owlbot_sha}" +} + +get_owlbot_sha_invalid_repo_fails() { + local output_folder="${script_dir}/resources" + local repository_root="nonexistent-repo" + $(get_owlbot_sha "${output_folder}" "${repository_root}") || res=$? + assertEquals 1 ${res} +} + +copy_directory_if_exists_valid_folder_succeeds() { + local source_folder="${script_dir}/resources" + local destination="${script_dir}/test_destination_folder" + mkdir -p "${destination}" + copy_directory_if_exists "${source_folder}" "${destination}/copied-folder" + n_matching_folders=$(ls "${destination}" | grep -e 'copied-folder' | wc -l) + rm -rdf "${destination}" + assertEquals 1 ${n_matching_folders} +} + +copy_directory_if_exists_invalid_folder_does_not_copy() { + local source_folder="${script_dir}/non-existent" + local destination="${script_dir}/test_destination_folder" + mkdir -p "${destination}" + copy_directory_if_exists "${source_folder}" "${destination}/copied-folder" + n_matching_folders=$(ls "${destination}" | grep -e 'copied-folder' | wc -l) || res=$? + rm -rdf "${destination}" + assertEquals 0 ${n_matching_folders} +} + # Execute tests. # One line per test. test_list=( @@ -344,6 +394,12 @@ test_list=( get_include_samples_from_BUILD_false_test get_include_samples_from_BUILD_empty_test get_version_from_valid_WORKSPACE_test + get_repo_metadata_json_valid_repo_succeeds + get_repo_metadata_json_invalid_repo_fails + get_owlbot_sha_valid_repo_succeeds + get_owlbot_sha_invalid_repo_fails + copy_directory_if_exists_valid_folder_succeeds + copy_directory_if_exists_invalid_folder_does_not_copy ) pushd "${script_dir}" diff --git a/library_generation/test/resources/proto_path_list.txt b/library_generation/test/resources/proto_path_list.txt index 2910caa62b..8943c03716 100755 --- a/library_generation/test/resources/proto_path_list.txt +++ b/library_generation/test/resources/proto_path_list.txt @@ -1,19 +1,24 @@ # This file is used in integration test against `generate_library.sh`. # Format: -# proto_path destination_path -google/bigtable/v2 google-cloud-bigtable-v2-java -google/cloud/apigeeconnect/v1 google-cloud-apigeeconnect-v1-java -google/cloud/asset/v1 google-cloud-asset-v1-java -google/cloud/compute/v1 google-cloud-compute-v1-java -google/cloud/kms/v1 google-cloud-kms-v1-java -google/cloud/optimization/v1 google-cloud-optimization-v1-java -google/cloud/redis/v1 google-cloud-redis-v1-java -google/cloud/videointelligence/v1p3beta1 google-cloud-videointelligence-v1p3beta1-java -google/example/library/v1 google-cloud-example-library-v1-java -google/devtools/containeranalysis/v1 google-cloud-devtools-containeranalysis-v1-java -google/firestore/bundle google-cloud-firestore-bundle-v1-java -google/iam/v1 google-iam-v1-java -google/iam/credentials/v1 google-cloud-iam-credentials-v1-java -google/logging/v2 google-cloud-logging-v2-java -google/pubsub/v1 google-cloud-pubsub-v1-java -google/storage/v2 google-cloud-storage-v2-java +# proto_path repository_path is_handwritten +# google/bigtable/admin/v2 java-bigtable true +# google/bigtable/v2 java-bigtable true +google/cloud/apigeeconnect/v1 java-apigee-connect false +google/cloud/asset/v1p5beta1 java-asset false +google/cloud/asset/v1p2beta1 java-asset false +google/cloud/asset/v1p1beta1 java-asset false +google/cloud/asset/v1p7beta1 java-asset false +google/cloud/asset/v1 java-asset false +# google/cloud/dialogflow/v2beta1 java-dialogflow false +# google/cloud/dialogflow/v2 java-dialogflow false +google/cloud/compute/v1 java-compute false +google/cloud/kms/v1 java-kms false +google/cloud/redis/v1 java-redis false +google/cloud/redis/v1beta1 java-redis false +# google/example/library/v1 google-cloud-example-library-v1-java null false +google/devtools/containeranalysis/v1 java-containeranalysis false +google/iam/v1 java-iam false +google/iam/credentials/v1 java-iamcredentials false +google/logging/v2 java-logging true +google/pubsub/v1 java-pubsub true +google/storage/v2 java-storage true diff --git a/library_generation/test/resources/test-monorepo/.github/.OwlBot.lock.yaml b/library_generation/test/resources/test-monorepo/.github/.OwlBot.lock.yaml new file mode 100644 index 0000000000..77200af4c9 --- /dev/null +++ b/library_generation/test/resources/test-monorepo/.github/.OwlBot.lock.yaml @@ -0,0 +1,17 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +docker: + image: gcr.io/cloud-devrel-public-resources/owlbot-java:latest + digest: sha256:fb7584f6adb3847ac480ed49a4bfe1463965026b2919a1be270e3174f3ce1191 + # created: 2023-01-20T00:00:00.000000000Z diff --git a/library_generation/test/resources/test-monorepo/test-service/.repo-metadata.json b/library_generation/test/resources/test-monorepo/test-service/.repo-metadata.json new file mode 100644 index 0000000000..d5b5078213 --- /dev/null +++ b/library_generation/test/resources/test-monorepo/test-service/.repo-metadata.json @@ -0,0 +1,18 @@ +{ + "api_shortname": "cloudasset", + "name_pretty": "Cloud Asset Inventory", + "product_documentation": "https://cloud.google.com/resource-manager/docs/cloud-asset-inventory/overview", + "api_reference": "https://cloud.google.com/resource-manager/docs/cloud-asset-inventory/overview", + "api_description": "provides inventory services based on a time series database. This database keeps a five week history of Google Cloud asset metadata. The Cloud Asset Inventory export service allows you to export all asset metadata at a certain timestamp or export event change history during a timeframe.", + "client_documentation": "https://cloud.google.com/java/docs/reference/google-cloud-asset/latest/overview", + "issue_tracker": "https://issuetracker.google.com/issues/new?component=187210&template=0", + "release_level": "stable", + "transport": "grpc", + "requires_billing": true, + "language": "java", + "repo": "googleapis/google-cloud-java", + "repo_short": "java-asset", + "distribution_name": "com.google.cloud:google-cloud-asset", + "api_id": "cloudasset.googleapis.com", + "library_type": "GAPIC_AUTO" +} diff --git a/library_generation/test/test_utilities.sh b/library_generation/test/test_utilities.sh index 578dd8c56d..3da3bd0392 100755 --- a/library_generation/test/test_utilities.sh +++ b/library_generation/test/test_utilities.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash set -xeo pipefail +test_utilities_script_dir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) # Utility functions commonly used in test cases. @@ -296,3 +297,42 @@ sparse_clone() { git checkout popd } + +# performs a deep structural comparison between the current pom in a git +# folder and the one at HEAD. +# This function is OS-dependent, so it sources the main utilities script to +# perform detection +compare_poms() { + target_dir=$1 + source "${test_utilities_script_dir}/../utilities.sh" + os_architecture=$(detect_os_architecture) + pushd "${target_dir}" &> /dev/null + find . -name 'pom.xml' -exec cp {} {}.new \; + find . -name 'pom.xml' -exec git checkout HEAD -- {} \; + # compare_poms.py exits with non-zero if diffs are found + set -e + result=0 + if [ "${os_architecture}" == "linux-x86_64" ]; then + find . -name 'pom.xml' -print0 | xargs -i -0 python "${test_utilities_script_dir}/compare_poms.py" {} {}.new false || result=$? + else + find . -name 'pom.xml' -print0 | xargs -I{} -0 python "${test_utilities_script_dir}/compare_poms.py" {} {}.new false || result=$? + fi + popd &> /dev/null # target_dir + echo ${result} +} + +# computes the `destination_path` variable by inspecting the contents of the +# googleapis-gen at $proto_path. +compute_destination_path() { + local proto_path=$1 + local output_folder=$2 + pushd "${output_folder}" &> /dev/null + local destination_path=$(find "googleapis-gen/${proto_path}" -maxdepth 1 -name 'google-*-java' \ + | rev \ + | cut -d'/' -f1 \ + | rev + ) + popd &> /dev/null # output_folder + echo "${destination_path}" +} + diff --git a/library_generation/utilities.sh b/library_generation/utilities.sh index cd92506c6e..66e489d2bc 100755 --- a/library_generation/utilities.sh +++ b/library_generation/utilities.sh @@ -6,7 +6,7 @@ set -xeo pipefail extract_folder_name() { local destination_path=$1 local folder_name=${destination_path##*/} - echo "$folder_name" + echo "${folder_name}" } remove_empty_files() { @@ -203,9 +203,7 @@ download_fail() { exit 1 } -# gets the output folder where all sources and dependencies will be located. It -# relies on utilities_script_dir which points to the same location as -# `generate_library.sh` +# gets the output folder where all sources and dependencies will be located. get_output_folder() { echo "$(pwd)/output" } @@ -227,3 +225,51 @@ detect_os_architecture() { esac echo "${os_architecture}" } + +# returns the metadata json path if given, or defaults to the one found in +# $repository_path +# Arguments +# 1 - repository_path: path from output_folder to the location of the library +# containing .repo-metadata. It assumes the existence of google-cloud-java in +# the output folder +# 2 - output_folder: root for the generated libraries, used in conjunction with +get_repo_metadata_json() { + local repository_path=$1 + local output_folder=$2 + >&2 echo 'Attempting to obtain .repo-metadata.json from repository_path' + local default_metadata_json_path="${output_folder}/${repository_path}/.repo-metadata.json" + if [ -f "${default_metadata_json_path}" ]; then + echo "${default_metadata_json_path}" + else + >&2 echo 'failed to obtain json from repository_path' + exit 1 + fi +} + +# returns the owlbot image sha contained in google-cloud-java. This is default +# behavior that may be overriden by a custom value in the future. +# Arguments +# 1 - output_folder: root for the generated libraries, used in conjunction with +# 2 - repository_root: usually "google-cloud-java". The .OwlBot.yaml +# file is looked into its .github folder +get_owlbot_sha() { + local output_folder=$1 + local repository_root=$2 + if [ ! -d "${output_folder}/${repository_root}" ]; + then + >&2 echo 'No repository to infer owlbot_sha was provided. This is necessary for post-processing' >&2 + exit 1 + fi + >&2 echo "Attempting to obtain owlbot_sha from monorepo folder" + owlbot_sha=$(grep 'sha256' "${output_folder}/${repository_root}/.github/.OwlBot.lock.yaml" | cut -d: -f3) + echo "${owlbot_sha}" +} + +# copies $1 as a folder as $2 only if $1 exists +copy_directory_if_exists() { + local source_folder=$1 + local destination_folder=$2 + if [ -d "${source_folder}" ]; then + cp -r "${source_folder}" "${destination_folder}" + fi +} diff --git a/showcase/scripts/generate_showcase.sh b/showcase/scripts/generate_showcase.sh index 69530fe354..ef9e2bf850 100755 --- a/showcase/scripts/generate_showcase.sh +++ b/showcase/scripts/generate_showcase.sh @@ -66,6 +66,7 @@ bash "${SCRIPT_DIR}/../../library_generation/generate_library.sh" \ --service_config "${service_config}" \ --service_yaml "${service_yaml}" \ --include_samples "${include_samples}" \ + --enable_postprocessing "false" \ --transport "${transport}" exit_code=$?