# Copyright 2020 The Nomulus Authors. All Rights Reserved. # # 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. """Script to generate dr-build and the properties file. """ import argparse import dataclasses import io import os import shutil import subprocess import sys from typing import List, Union @dataclasses.dataclass class Property: name : str = '' desc : str = '' default : str = '' constraints : type = str def validate(self, value: str): """Verify that "value" is appropriate for the property.""" if type is bool: if value not in ('true', 'false'): raise ValidationError('value of {self.name} must be "true" or ' '"false".') @dataclasses.dataclass class GradleFlag: flags : Union[str, List[str]] desc : str has_arg : bool = False PROPERTIES_HEADER = """\ # This file defines properties used by the gradle build. It must be kept in # sync with config/nom_build.py. # # To regenerate, run ./nom_build --generate-gradle-properties # # To view property descriptions (which are command line flags for # nom_build), run ./nom_build --help. # # DO NOT EDIT THIS FILE BY HAND org.gradle.jvmargs=-Xmx1024m org.gradle.caching=true """ # Help text to be displayed (in addition to the synopsis and flag help, which # are displayed automatically). HELP_TEXT = """\ A wrapper around the gradle build that provides the following features: - Converts properties into flags to guard against property name spelling errors and to provide help descriptions for all properties. - Provides pseudo-commands (with the ":nom:" prefix) that encapsulate common actions that are difficult to implement in gradle. Pseudo-commands: :nom:generate_golden_file - regenerates the golden file from the current set of flyway files. """ # Define all of our special gradle properties here. # TODO(b/169318491): use consistent naming style for properties and variables. PROPERTIES = [ Property('mavenUrl', 'URL to use for the main maven repository (defaults to maven ' 'central). This can be http(s) or a "gcs" repo.'), Property('pluginsUrl', 'URL to use for the gradle plugins repository (defaults to maven ' 'central, see also mavenUrl'), Property('allowInsecureProtocol', 'Allow connecting to plain HTTP repositories. This is provided ' 'to allow us to communicate to a local proxy when doing ' 'dependency updates.'), Property('uploaderDestination', 'Location to upload test reports to. Normally this should be a ' 'GCS url (see also uploaderCredentialsFile)'), Property('uploaderCredentialsFile', 'json credentials file to use to upload test reports.'), Property('uploaderMultithreadedUpload', 'Whether to enable multithread upload.'), Property('verboseTestOutput', 'If true, show all test output in near-realtime.', 'false', bool), Property('flowDocsFile', 'Output filename for the flowDocsTool command.'), Property('enableDependencyLocking', 'Enables dependency locking.', 'true', bool), Property('enableCrossReferencing', 'generate metadata during java compile (used for kythe source ' 'reference generation).', 'false'), Property('testFilter', 'Comma separated list of test patterns, if specified run only ' 'these.'), Property('environment', 'GAE Environment for deployment and staging.'), # Cloud SQL properties Property('dbServer', 'Sets the target database of a Flyway task. This may be a ' 'registry environment name (e.g., alpha) or the host[:port] ' 'of a database that accepts direct IP access.'), Property('dbName', 'Database name to use in connection.', 'postgres'), Property('dbUser', 'Database user name for use in connection'), Property('dbPassword', 'Database password for use in connection'), Property('publish_repo', 'Maven repository that hosts the Cloud SQL schema jar and the ' 'registry server test jars. Such jars are needed for ' 'server/schema integration tests. Please refer to integration project for more ' 'information.'), Property('baseSchemaTag', 'The nomulus version tag of the schema for use in the schema' 'deployment integration test (:db:schemaIncrementalDeployTest)'), Property('schema_version', 'The nomulus version tag of the schema for use in a database' 'integration test.'), Property('nomulus_version', 'The version of nomulus to test against in a database ' 'integration test.'), Property('dot_path', 'The path to "dot", part of the graphviz package that converts ' 'a BEAM pipeline to image. Setting this property to empty string ' 'will disable image generation.', '/usr/bin/dot'), Property('pipeline', 'The name of the Beam pipeline being staged.') ] GRADLE_FLAGS = [ GradleFlag(['-a', '--no-rebuild'], 'Do not rebuild project dependencies.'), GradleFlag(['-b', '--build-file'], 'Specify the build file.', True), GradleFlag(['--build-cache'], 'Enables the Gradle build cache. Gradle will try to reuse ' 'outputs from previous builds.'), GradleFlag(['-c', '--settings-file'], 'Specify the settings file.', True), GradleFlag(['--configure-on-demand'], 'Configure necessary projects only. Gradle will attempt to ' 'reduce configuration time for large multi-project builds. ' '[incubating]'), GradleFlag(['--console'], 'Specifies which type of console output to generate. Values ' "are 'plain', 'auto' (default), 'rich' or 'verbose'.", True), GradleFlag(['--continue'], 'Continue task execution after a task failure.'), GradleFlag(['-D', '--system-prop'], 'Set system property of the JVM (e.g. -Dmyprop=myvalue).', True), GradleFlag(['-d', '--debug'], 'Log in debug mode (includes normal stacktrace).'), GradleFlag(['--daemon'], 'Uses the Gradle Daemon to run the build. Starts the Daemon ' 'if not running.'), GradleFlag(['--foreground'], 'Starts the Gradle Daemon in the foreground.'), GradleFlag(['-g', '--gradle-user-home'], 'Specifies the gradle user home directory.', True), GradleFlag(['-I', '--init-script'], 'Specify an initialization script.', True), GradleFlag(['-i', '--info'], 'Set log level to info.'), GradleFlag(['--include-build'], 'Include the specified build in the composite.', True), GradleFlag(['-m', '--dry-run'], 'Run the builds with all task actions disabled.'), GradleFlag(['--max-workers'], 'Configure the number of concurrent workers Gradle is ' 'allowed to use.', True), GradleFlag(['--no-build-cache'], 'Disables the Gradle build cache.'), GradleFlag(['--no-configure-on-demand'], 'Disables the use of configuration on demand. [incubating]'), GradleFlag(['--no-daemon'], 'Do not use the Gradle daemon to run the build. Useful ' 'occasionally if you have configured Gradle to always run ' 'with the daemon by default.'), GradleFlag(['--no-parallel'], 'Disables parallel execution to build projects.'), GradleFlag(['--no-scan'], 'Disables the creation of a build scan. For more information ' 'about build scans, please visit ' 'https://gradle.com/build-scans.'), GradleFlag(['--offline'], 'Execute the build without accessing network resources.'), GradleFlag(['-P', '--project-prop'], 'Set project property for the build script (e.g. ' '-Pmyprop=myvalue).', True), GradleFlag(['-p', '--project-dir'], 'Specifies the start directory for Gradle. Defaults to ' 'current directory.'), GradleFlag(['--parallel'], 'Build projects in parallel. Gradle will attempt to ' 'determine the optimal number of executor threads to use.'), GradleFlag(['--priority'], 'Specifies the scheduling priority for the Gradle daemon and ' "all processes launched by it. Values are 'normal' (default) " "or 'low' [incubating]", True), GradleFlag(['--profile'], 'Profile build execution time and generates a report in the ' '/reports/profile directory.'), GradleFlag(['--project-cache-dir'], 'Specify the project-specific cache directory. Defaults to ' '.gradle in the root project directory.', True), GradleFlag(['-q', '--quiet'], 'Log errors only.'), GradleFlag(['--refresh-dependencies'], 'Refresh the state of dependencies.'), GradleFlag(['--rerun-tasks'], 'Ignore previously cached task results.'), GradleFlag(['-S', '--full-stacktrace'], 'Print out the full (very verbose) stacktrace for all ' 'exceptions.'), GradleFlag(['-s', '--stacktrace'], 'Print out the stacktrace for all exceptions.'), GradleFlag(['--scan'], 'Creates a build scan. Gradle will emit a warning if the ' 'build scan plugin has not been applied. ' '(https://gradle.com/build-scans)'), GradleFlag(['--status'], 'Shows status of running and recently stopped Gradle ' 'Daemon(s).'), GradleFlag(['--stop'], 'Stops the Gradle Daemon if it is running.'), GradleFlag(['-t', '--continuous'], 'Enables continuous build. Gradle does not exit and will ' 're-execute tasks when task file inputs change.'), GradleFlag(['--update-locks'], 'Perform a partial update of the dependency lock, letting ' 'passed in module notations change version. [incubating]'), GradleFlag(['-v', '--version'], 'Print version info.'), GradleFlag(['-w', '--warn'], 'Set log level to warn.'), GradleFlag(['--warning-mode'], 'Specifies which mode of warnings to generate. Values are ' "'all', 'fail', 'summary'(default) or 'none'", True), GradleFlag(['--write-locks'], 'Persists dependency resolution for locked configurations, ' 'ignoring existing locking information if it exists ' '[incubating]'), GradleFlag(['-x', '--exclude-task'], 'Specify a task to be excluded from execution.', True), ] def generate_gradle_properties() -> str: """Returns the expected contents of gradle.properties.""" out = io.StringIO() out.write(PROPERTIES_HEADER) for prop in PROPERTIES: out.write(f'{prop.name}={prop.default}\n') return out.getvalue() def get_root() -> str: """Returns the root of the nomulus build tree.""" cur_dir = os.getcwd() if not os.path.exists(os.path.join(cur_dir, 'buildSrc')) or \ not os.path.exists(os.path.join(cur_dir, 'core')) or \ not os.path.exists(os.path.join(cur_dir, 'gradle.properties')): raise Exception('You must run this script from the root directory') return cur_dir class Abort(Exception): """Raised to terminate the process with a non-zero error code. Parameters are ignored. """ def do_pseudo_task(task: str) -> None: root = get_root() if task == ':nom:generate_golden_file': if not subprocess.call([f'{root}/gradlew', ':db:test']): print('\033[33mWARNING:\033[0m Golden schema appears to be ' 'up-to-date. If you are making schema changes, be sure to ' 'add a flyway file for them.') return print('\033[33mWARNING:\033[0m Ignore the above failure, it is ' 'expected.') # Copy the new schema into place. shutil.copy(f'{root}/db/build/resources/test/testcontainer/' 'mount/dump.txt', f'{root}/db/src/main/resources/sql/schema/' 'nomulus.golden.sql') # Rerun :db:test and regenerate the ER diagram (at "warning" log # level so it doesn't generate pages of messaging) if subprocess.call([f'{root}/gradlew', ':db:test', 'devTool', '--args=-e localhost --log_level=WARNING ' 'generate_sql_er_diagram -o ' f'{root}/db/src/main/resources/sql/er_diagram']): print('\033[31mERROR:\033[0m Golden file test or ER diagram ' 'generation failed after copying schema. Please check your ' 'flyway files.') raise Abort() else: print(f'\033[31mERROR:\033[0m Unknown task {task}') raise Abort() def main(args) -> int: parser = argparse.ArgumentParser('nom_build', description=HELP_TEXT, formatter_class=argparse.RawTextHelpFormatter) for prop in PROPERTIES: parser.add_argument('--' + prop.name, default=prop.default, help=prop.desc) # Add Gradle flags. We set 'dest' to the first flag to get a name that is # predictable for getattr (even though it will have a leading '-' and thus # we can't use normal python attribute syntax to get it). for flag in GRADLE_FLAGS: if flag.has_arg: parser.add_argument(*flag.flags, dest=flag.flags[0], help=flag.desc) else: parser.add_argument(*flag.flags, dest=flag.flags[0], help=flag.desc, action='store_true') # Add a flag to regenerate the gradle properties file. parser.add_argument('--generate-gradle-properties', help='Regenerate the gradle.properties file. This ' 'file must be regenerated when changes are made to ' 'config/nom_build.py, and should not be updated by ' 'hand.', action='store_true') # Consume the remaining non-flag arguments. parser.add_argument('non_flag_args', nargs='*') # Parse command line arguments. Note that this exits the program and # prints usage if either of the help options (-h, --help) are specified. args = parser.parse_args(args) gradle_properties = generate_gradle_properties() root = get_root() # If we're regenerating properties, do so and exit. if args.generate_gradle_properties: with open(f'{root}/gradle.properties', 'w') as dst: dst.write(gradle_properties) return 0 # Verify that the gradle properties file is what we expect it to be. with open(f'{root}/gradle.properties') as src: if src.read() != gradle_properties: print('\033[33mWARNING:\033[0m Gradle properties out of sync ' 'with nom_build. Run with --generate-gradle-properties ' 'to regenerate.') # Add properties to the gradle argument list. gradle_command = [f'{root}/gradlew'] for prop in PROPERTIES: arg_val = getattr(args, prop.name) if arg_val != prop.default: prop.validate(arg_val) gradle_command.extend(['-P', f'{prop.name}={arg_val}']) # Add Gradle flags to the gradle argument list. for flag in GRADLE_FLAGS: arg_val = getattr(args, flag.flags[0]) if arg_val: gradle_command.append(flag.flags[-1]) if flag.has_arg: gradle_command.append(arg_val) # See if there are any special ":nom:" pseudo-tasks specified. got_non_pseudo_tasks = False got_pseudo_tasks = False for arg in args.non_flag_args[1:]: if arg.startswith(':nom:'): if got_non_pseudo_tasks: # We can't currently deal with the situation of gradle tasks # before pseudo-tasks. This could be implemented by invoking # gradle for only the set of gradle tasks before the pseudo # task, but that's overkill for now. print(f'\033[31mERROR:\033[0m Pseudo task ({arg}) must be ' 'specified prior to all actual gradle tasks. Aborting.') return 1 do_pseudo_task(arg) got_pseudo_tasks = True else: got_non_pseudo_tasks = True non_flag_args = [ arg for arg in args.non_flag_args[1:] if not arg.startswith(':nom:')] if not non_flag_args: if not got_pseudo_tasks: print('\033[33mWARNING:\033[0m No tasks specified. Not ' 'doing anything') return 0 # Add the non-flag args (we exclude the first, which is the command name # itself) and run. gradle_command.extend(non_flag_args) return subprocess.call(gradle_command) if __name__ == '__main__': try: sys.exit(main(sys.argv)) except Abort as ex: sys.exit(1)