cargo2ebuild: Handle version parse errors and do not replace existing.

This adds:
* error handling for version parse errors, so the script can
  still be used (with user cleanup required).
* a '-x' command line flag that can be used to overwrite existing
  ebuilds. The new default behavior is to leave existing ebuilds.

BUG=None
TEST=manual testing.

Change-Id: Ib78bd272c83d02eae1aeb235eb553077b0471067
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/dev-util/+/2868045
Tested-by: Allen Webb <allenwebb@google.com>
Reviewed-by: Abhishek Pandit-Subedi <abhishekpandit@chromium.org>
Commit-Queue: Allen Webb <allenwebb@google.com>
diff --git a/contrib/cargo2ebuild.py b/contrib/cargo2ebuild.py
index 6a76cee..c07836c 100755
--- a/contrib/cargo2ebuild.py
+++ b/contrib/cargo2ebuild.py
@@ -30,7 +30,7 @@
 #   license: Ebuild compatible license string.
 #   dependencies: Ebuild compatible dependency string.
 #   autogen_notice: Autogenerated notification string.
-EBUILD_TEMPLATE = \
+EBUILD_TEMPLATE = (
 """# Copyright {copyright_year} The Chromium OS Authors. All rights reserved.
 # Distributed under the terms of the GNU General Public License v2
 
@@ -49,13 +49,13 @@
 KEYWORDS="*"
 
 {dependencies}{autogen_notice}
-"""
+""")
 
 # Required parameters:
 #   copyright_year: Current year for copyright assignment.
 #   crate_features: Features to add to this empty crate.
 #   autogen_notice: Autogenerated notification string.
-EMPTY_CRATE = \
+EMPTY_CRATE = (
 """# Copyright {copyright_year} The Chromium OS Authors. All rights reserved.
 # Distributed under the terms of the GNU General Public License v2
 
@@ -72,7 +72,7 @@
 SLOT="${{PV}}/${{PR}}"
 KEYWORDS="*"
 {autogen_notice}
-"""
+""")
 
 LICENSES = {
     'Apache-2.0': 'Apache-2.0',
@@ -91,6 +91,10 @@
 )
 
 
+class VersionParseError(Exception):
+    """Error that is returned when parsing a version fails."""
+
+
 def prepare_staging(args):
     """Prepare staging directory."""
     sdir = args.staging_dir
@@ -137,6 +141,7 @@
     m = re.match(VERSION_RE, version)
     if not m:
         print('{} failed to parse dep version: {}'.format(name, version))
+        raise VersionParseError
 
     dep = m.group('dep')
     major = m.group('major')
@@ -299,10 +304,15 @@
                     'features': dep['features'],
             }
 
-        # Convert requirement to version tuple
-        (deptype, major, minor, patch) = version_to_tuple(dep['name'], dep['req'])
-        ebuild = dep_to_ebuild(dep['name'], deptype, major, minor, patch)
-        deps.append('\t{}'.format(ebuild))
+        # Convert version requirement to ebuild DEPEND.
+        try:
+            # Convert requirement to version tuple
+            (deptype, major, minor, patch) = version_to_tuple(dep['name'], dep['req'])
+            ebuild = dep_to_ebuild(dep['name'], deptype, major, minor, patch)
+            deps.append('\t{}'.format(ebuild))
+        except VersionParseError:
+            # Rarely dependencies look something like ">=0.6, <0.8"
+            deps.append("\t$(die 'Please replace with proper DEPEND: {} = {}')".format(dep['name'], dep['req']))
 
     if not deps:
         return ''
@@ -359,12 +369,16 @@
             crate_name, ret))
 
 
-def update_ebuild(package, ebuild_dir, target_dir):
+def update_ebuild(package, args, ebuild_dir, target_dir):
     """Update ebuild with generated one and generate MANIFEST."""
     ebuild_src = get_ebuild_path(package, ebuild_dir)
     ebuild_dest = get_ebuild_path(package, target_dir, make_dir=True)
 
-    shutil.copy(ebuild_src, ebuild_dest)
+    # Do not overwrite existing ebuilds unless explicity asked to.
+    if args.overwrite_existing_ebuilds or not os.path.exists(ebuild_dest):
+        shutil.copy(ebuild_src, ebuild_dest)
+    else:
+        print('ebuild {} already exists, skipping.'.format(ebuild_dest))
 
     # Generate manifest w/ ebuild digest
     ret = subprocess.run(['ebuild', ebuild_dest, 'digest']).returncode
@@ -383,7 +397,7 @@
 
     if not args.dry_run and package['name'] not in args.skip:
         upload_gsutil(package, staging_dir, no_upload=args.no_upload)
-        update_ebuild(package, ebuild_dir, target_dir)
+        update_ebuild(package, args, ebuild_dir, target_dir)
 
 def process_empty_package(empty_package, args):
     """Process packages that should generate empty ebuilds."""
@@ -445,9 +459,12 @@
         processed_packages[p['name']] = True
 
     for key in optional_packages:
-        if key not in processed_packages and not check_if_package_is_required(
-                optional_packages[key], args):
-            process_empty_package(optional_packages[key], args)
+        try:
+            if key not in processed_packages and not check_if_package_is_required(
+                    optional_packages[key], args):
+                process_empty_package(optional_packages[key], args)
+        except VersionParseError:
+            print('{} has a malformed version'.format(key))
 
 
 def parse_args(argv):
@@ -477,6 +494,10 @@
                         '--no-upload',
                         action='store_true',
                         help='Skip uploading crates to distfiles')
+    parser.add_argument('-x',
+                        '--overwrite-existing-ebuilds',
+                        action='store_true',
+                        help='Skip uploading crates to distfiles')
 
     parser.add_argument('manifest_path',
                         help='Cargo.toml used to generate ebuilds.')