Ben Murdoch | 097c5b2 | 2016-05-18 11:27:45 +0100 | [diff] [blame^] | 1 | #!/usr/bin/env python |
| 2 | # Copyright 2015 The Chromium Authors. All rights reserved. |
| 3 | # Use of this source code is governed by a BSD-style license that can be |
| 4 | # found in the LICENSE file. |
| 5 | |
| 6 | ''' |
| 7 | Script to help uploading and downloading the Google Play services library to |
| 8 | and from a Google Cloud storage. |
| 9 | ''' |
| 10 | |
| 11 | import argparse |
| 12 | import logging |
| 13 | import os |
| 14 | import re |
| 15 | import shutil |
| 16 | import sys |
| 17 | import tempfile |
| 18 | import zipfile |
| 19 | |
| 20 | sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir)) |
| 21 | import devil_chromium |
| 22 | from devil.utils import cmd_helper |
| 23 | from play_services import utils |
| 24 | from pylib import constants |
| 25 | from pylib.constants import host_paths |
| 26 | from pylib.utils import logging_utils |
| 27 | |
| 28 | sys.path.append(os.path.join(host_paths.DIR_SOURCE_ROOT, 'build')) |
| 29 | import find_depot_tools # pylint: disable=import-error,unused-import |
| 30 | import breakpad |
| 31 | import download_from_google_storage |
| 32 | import upload_to_google_storage |
| 33 | |
| 34 | |
| 35 | # Directory where the SHA1 files for the zip and the license are stored |
| 36 | # It should be managed by git to provided information about new versions. |
| 37 | SHA1_DIRECTORY = os.path.join(host_paths.DIR_SOURCE_ROOT, 'build', 'android', |
| 38 | 'play_services') |
| 39 | |
| 40 | # Default bucket used for storing the files. |
| 41 | GMS_CLOUD_STORAGE = 'chromium-android-tools/play-services' |
| 42 | |
| 43 | # Path to the default configuration file. It exposes the currently installed |
| 44 | # version of the library in a human readable way. |
| 45 | CONFIG_DEFAULT_PATH = os.path.join(host_paths.DIR_SOURCE_ROOT, 'build', |
| 46 | 'android', 'play_services', 'config.json') |
| 47 | |
| 48 | LICENSE_FILE_NAME = 'LICENSE' |
| 49 | ZIP_FILE_NAME = 'google_play_services_library.zip' |
| 50 | GMS_PACKAGE_ID = 'extra-google-google_play_services' # used by sdk manager |
| 51 | |
| 52 | LICENSE_PATTERN = re.compile(r'^Pkg\.License=(?P<text>.*)$', re.MULTILINE) |
| 53 | |
| 54 | |
| 55 | def main(raw_args): |
| 56 | parser = argparse.ArgumentParser( |
| 57 | description=__doc__ + 'Please see the subcommand help for more details.', |
| 58 | formatter_class=utils.DefaultsRawHelpFormatter) |
| 59 | subparsers = parser.add_subparsers(title='commands') |
| 60 | |
| 61 | # Download arguments |
| 62 | parser_download = subparsers.add_parser( |
| 63 | 'download', |
| 64 | help='download the library from the cloud storage', |
| 65 | description=Download.__doc__, |
| 66 | formatter_class=utils.DefaultsRawHelpFormatter) |
| 67 | parser_download.set_defaults(func=Download) |
| 68 | AddBasicArguments(parser_download) |
| 69 | AddBucketArguments(parser_download) |
| 70 | |
| 71 | # SDK Update arguments |
| 72 | parser_sdk = subparsers.add_parser( |
| 73 | 'sdk', |
| 74 | help='get the latest Google Play services SDK using Android SDK Manager', |
| 75 | description=UpdateSdk.__doc__, |
| 76 | formatter_class=utils.DefaultsRawHelpFormatter) |
| 77 | parser_sdk.set_defaults(func=UpdateSdk) |
| 78 | AddBasicArguments(parser_sdk) |
| 79 | |
| 80 | # Upload arguments |
| 81 | parser_upload = subparsers.add_parser( |
| 82 | 'upload', |
| 83 | help='upload the library to the cloud storage', |
| 84 | description=Upload.__doc__, |
| 85 | formatter_class=utils.DefaultsRawHelpFormatter) |
| 86 | |
| 87 | parser_upload.add_argument('--skip-git', |
| 88 | action='store_true', |
| 89 | help="don't commit the changes at the end") |
| 90 | parser_upload.set_defaults(func=Upload) |
| 91 | AddBasicArguments(parser_upload) |
| 92 | AddBucketArguments(parser_upload) |
| 93 | |
| 94 | args = parser.parse_args(raw_args) |
| 95 | if args.verbose: |
| 96 | logging.basicConfig(level=logging.DEBUG) |
| 97 | logging_utils.ColorStreamHandler.MakeDefault(not _IsBotEnvironment()) |
| 98 | devil_chromium.Initialize() |
| 99 | return args.func(args) |
| 100 | |
| 101 | |
| 102 | def AddBasicArguments(parser): |
| 103 | ''' |
| 104 | Defines the common arguments on subparser rather than the main one. This |
| 105 | allows to put arguments after the command: `foo.py upload --debug --force` |
| 106 | instead of `foo.py --debug upload --force` |
| 107 | ''' |
| 108 | |
| 109 | parser.add_argument('--sdk-root', |
| 110 | help='base path to the Android SDK tools root', |
| 111 | default=constants.ANDROID_SDK_ROOT) |
| 112 | |
| 113 | parser.add_argument('-v', '--verbose', |
| 114 | action='store_true', |
| 115 | help='print debug information') |
| 116 | |
| 117 | |
| 118 | def AddBucketArguments(parser): |
| 119 | parser.add_argument('--bucket', |
| 120 | help='name of the bucket where the files are stored', |
| 121 | default=GMS_CLOUD_STORAGE) |
| 122 | |
| 123 | parser.add_argument('--config', |
| 124 | help='JSON Configuration file', |
| 125 | default=CONFIG_DEFAULT_PATH) |
| 126 | |
| 127 | parser.add_argument('--dry-run', |
| 128 | action='store_true', |
| 129 | help=('run the script in dry run mode. Files will be ' |
| 130 | 'copied to a local directory instead of the ' |
| 131 | 'cloud storage. The bucket name will be as path ' |
| 132 | 'to that directory relative to the repository ' |
| 133 | 'root.')) |
| 134 | |
| 135 | parser.add_argument('-f', '--force', |
| 136 | action='store_true', |
| 137 | help='run even if the library is already up to date') |
| 138 | |
| 139 | |
| 140 | def Download(args): |
| 141 | ''' |
| 142 | Downloads the Google Play services library from a Google Cloud Storage bucket |
| 143 | and installs it to |
| 144 | //third_party/android_tools/sdk/extras/google/google_play_services. |
| 145 | |
| 146 | A license check will be made, and the user might have to accept the license |
| 147 | if that has not been done before. |
| 148 | ''' |
| 149 | |
| 150 | if not os.path.isdir(args.sdk_root): |
| 151 | logging.debug('Did not find the Android SDK root directory at "%s".', |
| 152 | args.sdk_root) |
| 153 | if not args.force: |
| 154 | logging.info('Skipping, not on an android checkout.') |
| 155 | return 0 |
| 156 | |
| 157 | config = utils.ConfigParser(args.config) |
| 158 | paths = PlayServicesPaths(args.sdk_root, config.version_xml_path) |
| 159 | |
| 160 | if os.path.isdir(paths.package) and not os.access(paths.package, os.W_OK): |
| 161 | logging.error('Failed updating the Google Play Services library. ' |
| 162 | 'The location is not writable. Please remove the ' |
| 163 | 'directory (%s) and try again.', paths.package) |
| 164 | return -2 |
| 165 | |
| 166 | new_lib_zip_sha1 = os.path.join(SHA1_DIRECTORY, ZIP_FILE_NAME + '.sha1') |
| 167 | |
| 168 | logging.debug('Comparing zip hashes: %s and %s', new_lib_zip_sha1, |
| 169 | paths.lib_zip_sha1) |
| 170 | if utils.FileEquals(new_lib_zip_sha1, paths.lib_zip_sha1) and not args.force: |
| 171 | logging.info('Skipping, the Google Play services library is up to date.') |
| 172 | return 0 |
| 173 | |
| 174 | bucket_path = _VerifyBucketPathFormat(args.bucket, |
| 175 | config.version_number, |
| 176 | args.dry_run) |
| 177 | |
| 178 | tmp_root = tempfile.mkdtemp() |
| 179 | try: |
| 180 | # setup the destination directory |
| 181 | if not os.path.isdir(paths.package): |
| 182 | os.makedirs(paths.package) |
| 183 | |
| 184 | # download license file from bucket/{version_number}/license.sha1 |
| 185 | new_license = os.path.join(tmp_root, LICENSE_FILE_NAME) |
| 186 | |
| 187 | license_sha1 = os.path.join(SHA1_DIRECTORY, LICENSE_FILE_NAME + '.sha1') |
| 188 | _DownloadFromBucket(bucket_path, license_sha1, new_license, |
| 189 | args.verbose, args.dry_run) |
| 190 | |
| 191 | if (not _IsBotEnvironment() and |
| 192 | not _CheckLicenseAgreement(new_license, paths.license, |
| 193 | config.version_number)): |
| 194 | logging.warning('Your version of the Google Play services library is ' |
| 195 | 'not up to date. You might run into issues building ' |
| 196 | 'or running the app. Please run `%s download` to ' |
| 197 | 'retry downloading it.', __file__) |
| 198 | return 0 |
| 199 | |
| 200 | new_lib_zip = os.path.join(tmp_root, ZIP_FILE_NAME) |
| 201 | _DownloadFromBucket(bucket_path, new_lib_zip_sha1, new_lib_zip, |
| 202 | args.verbose, args.dry_run) |
| 203 | |
| 204 | try: |
| 205 | # We remove the current version of the Google Play services SDK. |
| 206 | if os.path.exists(paths.package): |
| 207 | shutil.rmtree(paths.package) |
| 208 | os.makedirs(paths.package) |
| 209 | |
| 210 | logging.debug('Extracting the library to %s', paths.lib) |
| 211 | with zipfile.ZipFile(new_lib_zip, "r") as new_lib_zip_file: |
| 212 | new_lib_zip_file.extractall(paths.lib) |
| 213 | |
| 214 | logging.debug('Copying %s to %s', new_license, paths.license) |
| 215 | shutil.copy(new_license, paths.license) |
| 216 | |
| 217 | logging.debug('Copying %s to %s', new_lib_zip_sha1, paths.lib_zip_sha1) |
| 218 | shutil.copy(new_lib_zip_sha1, paths.lib_zip_sha1) |
| 219 | |
| 220 | logging.info('Update complete.') |
| 221 | |
| 222 | except Exception as e: # pylint: disable=broad-except |
| 223 | logging.error('Failed updating the Google Play Services library. ' |
| 224 | 'An error occurred while installing the new version in ' |
| 225 | 'the SDK directory: %s ', e) |
| 226 | return -3 |
| 227 | finally: |
| 228 | shutil.rmtree(tmp_root) |
| 229 | |
| 230 | return 0 |
| 231 | |
| 232 | |
| 233 | def UpdateSdk(args): |
| 234 | ''' |
| 235 | Uses the Android SDK Manager to download the latest Google Play services SDK |
| 236 | locally. Its usual installation path is |
| 237 | //third_party/android_tools/sdk/extras/google/google_play_services |
| 238 | ''' |
| 239 | |
| 240 | # This should function should not run on bots and could fail for many user |
| 241 | # and setup related reasons. Also, exceptions here are not caught, so we |
| 242 | # disable breakpad to avoid spamming the logs. |
| 243 | breakpad.IS_ENABLED = False |
| 244 | |
| 245 | sdk_manager = os.path.join(args.sdk_root, 'tools', 'android') |
| 246 | cmd = [sdk_manager, 'update', 'sdk', '--no-ui', '--filter', GMS_PACKAGE_ID] |
| 247 | cmd_helper.Call(cmd) |
| 248 | # If no update is needed, it still returns successfully so we just do nothing |
| 249 | |
| 250 | return 0 |
| 251 | |
| 252 | |
| 253 | def Upload(args): |
| 254 | ''' |
| 255 | Uploads the library from the local Google Play services SDK to a Google Cloud |
| 256 | storage bucket. |
| 257 | |
| 258 | By default, a local commit will be made at the end of the operation. |
| 259 | ''' |
| 260 | |
| 261 | # This should function should not run on bots and could fail for many user |
| 262 | # and setup related reasons. Also, exceptions here are not caught, so we |
| 263 | # disable breakpad to avoid spamming the logs. |
| 264 | breakpad.IS_ENABLED = False |
| 265 | |
| 266 | config = utils.ConfigParser(args.config) |
| 267 | paths = PlayServicesPaths(args.sdk_root, config.version_xml_path) |
| 268 | |
| 269 | if not args.skip_git and utils.IsRepoDirty(host_paths.DIR_SOURCE_ROOT): |
| 270 | logging.error('The repo is dirty. Please commit or stash your changes.') |
| 271 | return -1 |
| 272 | |
| 273 | new_version_number = utils.GetVersionNumberFromLibraryResources( |
| 274 | paths.version_xml) |
| 275 | logging.debug('comparing versions: new=%d, old=%s', |
| 276 | new_version_number, config.version_number) |
| 277 | if new_version_number <= config.version_number and not args.force: |
| 278 | logging.info('The checked in version of the library is already the latest ' |
| 279 | 'one. No update is needed. Please rerun with --force to skip ' |
| 280 | 'this check.') |
| 281 | return 0 |
| 282 | |
| 283 | tmp_root = tempfile.mkdtemp() |
| 284 | try: |
| 285 | new_lib_zip = os.path.join(tmp_root, ZIP_FILE_NAME) |
| 286 | new_license = os.path.join(tmp_root, LICENSE_FILE_NAME) |
| 287 | |
| 288 | # need to strip '.zip' from the file name here |
| 289 | shutil.make_archive(new_lib_zip[:-4], 'zip', paths.lib) |
| 290 | _ExtractLicenseFile(new_license, paths.source_prop) |
| 291 | |
| 292 | bucket_path = _VerifyBucketPathFormat(args.bucket, new_version_number, |
| 293 | args.dry_run) |
| 294 | files_to_upload = [new_lib_zip, new_license] |
| 295 | logging.debug('Uploading %s to %s', files_to_upload, bucket_path) |
| 296 | _UploadToBucket(bucket_path, files_to_upload, args.dry_run) |
| 297 | |
| 298 | new_lib_zip_sha1 = os.path.join(SHA1_DIRECTORY, |
| 299 | ZIP_FILE_NAME + '.sha1') |
| 300 | new_license_sha1 = os.path.join(SHA1_DIRECTORY, |
| 301 | LICENSE_FILE_NAME + '.sha1') |
| 302 | shutil.copy(new_lib_zip + '.sha1', new_lib_zip_sha1) |
| 303 | shutil.copy(new_license + '.sha1', new_license_sha1) |
| 304 | finally: |
| 305 | shutil.rmtree(tmp_root) |
| 306 | |
| 307 | config.UpdateVersionNumber(new_version_number) |
| 308 | |
| 309 | if not args.skip_git: |
| 310 | commit_message = ('Update the Google Play services dependency to %s\n' |
| 311 | '\n') % new_version_number |
| 312 | utils.MakeLocalCommit(host_paths.DIR_SOURCE_ROOT, |
| 313 | [new_lib_zip_sha1, new_license_sha1, config.path], |
| 314 | commit_message) |
| 315 | |
| 316 | return 0 |
| 317 | |
| 318 | |
| 319 | def _DownloadFromBucket(bucket_path, sha1_file, destination, verbose, |
| 320 | is_dry_run): |
| 321 | '''Downloads the file designated by the provided sha1 from a cloud bucket.''' |
| 322 | |
| 323 | download_from_google_storage.download_from_google_storage( |
| 324 | input_filename=sha1_file, |
| 325 | base_url=bucket_path, |
| 326 | gsutil=_InitGsutil(is_dry_run), |
| 327 | num_threads=1, |
| 328 | directory=None, |
| 329 | recursive=False, |
| 330 | force=False, |
| 331 | output=destination, |
| 332 | ignore_errors=False, |
| 333 | sha1_file=sha1_file, |
| 334 | verbose=verbose, |
| 335 | auto_platform=True, |
| 336 | extract=False) |
| 337 | |
| 338 | |
| 339 | def _UploadToBucket(bucket_path, files_to_upload, is_dry_run): |
| 340 | '''Uploads the files designated by the provided paths to a cloud bucket. ''' |
| 341 | |
| 342 | upload_to_google_storage.upload_to_google_storage( |
| 343 | input_filenames=files_to_upload, |
| 344 | base_url=bucket_path, |
| 345 | gsutil=_InitGsutil(is_dry_run), |
| 346 | force=False, |
| 347 | use_md5=False, |
| 348 | num_threads=1, |
| 349 | skip_hashing=False, |
| 350 | gzip=None) |
| 351 | |
| 352 | |
| 353 | def _InitGsutil(is_dry_run): |
| 354 | '''Initialize the Gsutil object as regular or dummy version for dry runs. ''' |
| 355 | |
| 356 | if is_dry_run: |
| 357 | return DummyGsutil() |
| 358 | else: |
| 359 | return download_from_google_storage.Gsutil( |
| 360 | download_from_google_storage.GSUTIL_DEFAULT_PATH) |
| 361 | |
| 362 | |
| 363 | def _ExtractLicenseFile(license_path, prop_file_path): |
| 364 | with open(prop_file_path, 'r') as prop_file: |
| 365 | prop_file_content = prop_file.read() |
| 366 | |
| 367 | match = LICENSE_PATTERN.search(prop_file_content) |
| 368 | if not match: |
| 369 | raise AttributeError('The license was not found in ' + |
| 370 | os.path.abspath(prop_file_path)) |
| 371 | |
| 372 | with open(license_path, 'w') as license_file: |
| 373 | license_file.write(match.group('text')) |
| 374 | |
| 375 | |
| 376 | def _CheckLicenseAgreement(expected_license_path, actual_license_path, |
| 377 | version_number): |
| 378 | ''' |
| 379 | Checks that the new license is the one already accepted by the user. If it |
| 380 | isn't, it prompts the user to accept it. Returns whether the expected license |
| 381 | has been accepted. |
| 382 | ''' |
| 383 | |
| 384 | if utils.FileEquals(expected_license_path, actual_license_path): |
| 385 | return True |
| 386 | |
| 387 | with open(expected_license_path) as license_file: |
| 388 | # Uses plain print rather than logging to make sure this is not formatted |
| 389 | # by the logger. |
| 390 | print ('Updating the Google Play services SDK to ' |
| 391 | 'version %d.' % version_number) |
| 392 | |
| 393 | # The output is buffered when running as part of gclient hooks. We split |
| 394 | # the text here and flush is explicitly to avoid having part of it dropped |
| 395 | # out. |
| 396 | # Note: text contains *escaped* new lines, so we split by '\\n', not '\n'. |
| 397 | for license_part in license_file.read().split('\\n'): |
| 398 | print license_part |
| 399 | sys.stdout.flush() |
| 400 | |
| 401 | # Need to put the prompt on a separate line otherwise the gclient hook buffer |
| 402 | # only prints it after we received an input. |
| 403 | print 'Do you accept the license? [y/n]: ' |
| 404 | sys.stdout.flush() |
| 405 | return raw_input('> ') in ('Y', 'y') |
| 406 | |
| 407 | |
| 408 | def _IsBotEnvironment(): |
| 409 | return bool(os.environ.get('CHROME_HEADLESS')) |
| 410 | |
| 411 | |
| 412 | def _VerifyBucketPathFormat(bucket_name, version_number, is_dry_run): |
| 413 | ''' |
| 414 | Formats and checks the download/upload path depending on whether we are |
| 415 | running in dry run mode or not. Returns a supposedly safe path to use with |
| 416 | Gsutil. |
| 417 | ''' |
| 418 | |
| 419 | if is_dry_run: |
| 420 | bucket_path = os.path.abspath(os.path.join(bucket_name, |
| 421 | str(version_number))) |
| 422 | if not os.path.isdir(bucket_path): |
| 423 | os.makedirs(bucket_path) |
| 424 | else: |
| 425 | if bucket_name.startswith('gs://'): |
| 426 | # We enforce the syntax without gs:// for consistency with the standalone |
| 427 | # download/upload scripts and to make dry run transition easier. |
| 428 | raise AttributeError('Please provide the bucket name without the gs:// ' |
| 429 | 'prefix (e.g. %s)' % GMS_CLOUD_STORAGE) |
| 430 | bucket_path = 'gs://%s/%d' % (bucket_name, version_number) |
| 431 | |
| 432 | return bucket_path |
| 433 | |
| 434 | |
| 435 | class PlayServicesPaths(object): |
| 436 | ''' |
| 437 | Describes the different paths to be used in the update process. |
| 438 | |
| 439 | Filesystem hierarchy | Exposed property / notes |
| 440 | ---------------------------------------------------|------------------------- |
| 441 | [sdk_root] | sdk_root / (1) |
| 442 | +- extras | |
| 443 | +- google | |
| 444 | +- google_play_services | package / (2) |
| 445 | +- source.properties | source_prop / (3) |
| 446 | +- LICENSE | license / (4) |
| 447 | +- google_play_services_library.zip.sha1 | lib_zip_sha1 / (5) |
| 448 | +- libproject | |
| 449 | +- google-play-services_lib | lib / (6) |
| 450 | +- res | |
| 451 | +- values | |
| 452 | +- version.xml | version_xml (7) |
| 453 | |
| 454 | Notes: |
| 455 | |
| 456 | 1. sdk_root: Path provided as a parameter to the script (--sdk_root) |
| 457 | 2. package: This directory contains the Google Play services SDK itself. |
| 458 | When downloaded via the Android SDK manager, it will contain, |
| 459 | documentation, samples and other files in addition to the library. When |
| 460 | the update script downloads the library from our cloud storage, it is |
| 461 | cleared. |
| 462 | 3. source_prop: File created by the Android SDK manager that contains |
| 463 | the package information, such as the version info and the license. |
| 464 | 4. license: File created by the update script. Contains the license accepted |
| 465 | by the user. |
| 466 | 5. lib_zip_sha1: sha1 of the library zip that has been installed by the |
| 467 | update script. It is compared with the one required by the config file to |
| 468 | check if an update is necessary. |
| 469 | 6. lib: Contains the library itself: jar and resources. This is what is |
| 470 | downloaded from the cloud storage. |
| 471 | 7. version_xml: File that contains the exact Google Play services library |
| 472 | version, the one that we track. The version looks like 811500, is used in |
| 473 | the code and the on-device APK, as opposed to the SDK package version |
| 474 | which looks like 27.0.0 and is used only by the Android SDK manager. |
| 475 | |
| 476 | ''' |
| 477 | |
| 478 | def __init__(self, sdk_root, version_xml_path): |
| 479 | relative_package = os.path.join('extras', 'google', 'google_play_services') |
| 480 | relative_lib = os.path.join(relative_package, 'libproject', |
| 481 | 'google-play-services_lib') |
| 482 | self.sdk_root = sdk_root |
| 483 | |
| 484 | self.package = os.path.join(sdk_root, relative_package) |
| 485 | self.lib_zip_sha1 = os.path.join(self.package, ZIP_FILE_NAME + '.sha1') |
| 486 | self.license = os.path.join(self.package, LICENSE_FILE_NAME) |
| 487 | self.source_prop = os.path.join(self.package, 'source.properties') |
| 488 | |
| 489 | self.lib = os.path.join(sdk_root, relative_lib) |
| 490 | self.version_xml = os.path.join(self.lib, version_xml_path) |
| 491 | |
| 492 | |
| 493 | class DummyGsutil(download_from_google_storage.Gsutil): |
| 494 | ''' |
| 495 | Class that replaces Gsutil to use a local directory instead of an online |
| 496 | bucket. It relies on the fact that Gsutil commands are very similar to shell |
| 497 | ones, so for the ones used here (ls, cp), it works to just use them with a |
| 498 | local directory. |
| 499 | ''' |
| 500 | |
| 501 | def __init__(self): |
| 502 | super(DummyGsutil, self).__init__( |
| 503 | download_from_google_storage.GSUTIL_DEFAULT_PATH) |
| 504 | |
| 505 | def call(self, *args): |
| 506 | logging.debug('Calling command "%s"', str(args)) |
| 507 | return cmd_helper.GetCmdStatusOutputAndError(args) |
| 508 | |
| 509 | def check_call(self, *args): |
| 510 | logging.debug('Calling command "%s"', str(args)) |
| 511 | return cmd_helper.GetCmdStatusOutputAndError(args) |
| 512 | |
| 513 | |
| 514 | if __name__ == '__main__': |
| 515 | sys.exit(main(sys.argv[1:])) |