bundle-rust-downstream.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. #!/usr/bin/python3
  2. # --- BEGIN COPYRIGHT BLOCK ---
  3. # Copyright (C) 2021 Red Hat, Inc.
  4. # All rights reserved.
  5. #
  6. # License: GPL (version 3 or any later version).
  7. # See LICENSE for details.
  8. # --- END COPYRIGHT BLOCK ---
  9. #
  10. # PYTHON_ARGCOMPLETE_OK
  11. import os
  12. import sys
  13. import time
  14. import signal
  15. import argparse
  16. import argcomplete
  17. import shutil
  18. import toml
  19. from lib389.cli_base import setup_script_logger
  20. from rust2rpm import licensing
  21. SPECFILE_COMMENT_LINE = 'Bundled cargo crates list'
  22. START_LINE = f"##### {SPECFILE_COMMENT_LINE} - START #####\n"
  23. END_LINE = f"##### {SPECFILE_COMMENT_LINE} - END #####\n"
  24. parser = argparse.ArgumentParser(
  25. formatter_class=argparse.RawDescriptionHelpFormatter,
  26. description="""Add 'Provides: bundled(crate(foo)) = version' to a Fedora based specfile.
  27. Additionally, add a helper comment with a comulated License metainfo which is based on Cargo.lock file content.""")
  28. parser.add_argument('-v', '--verbose',
  29. help="Display verbose operation tracing during command execution",
  30. action='store_true', default=False)
  31. parser.add_argument('cargo_lock_file',
  32. help="The path to Cargo.lock file.")
  33. parser.add_argument('spec_file',
  34. help="The path to spec file that will be modified.")
  35. parser.add_argument('vendor_dir',
  36. help="The path to the vendor directory file that will be modified.")
  37. parser.add_argument('--backup-specfile',
  38. help="Make a backup of the downstream specfile.",
  39. action='store_true', default=False)
  40. # handle a control-c gracefully
  41. def signal_handler(signal, frame):
  42. print('\n\nExiting...')
  43. sys.exit(0)
  44. def get_license_list(vendor_dir):
  45. license_list = list()
  46. for root, _, files in os.walk(vendor_dir):
  47. for file in files:
  48. name = os.path.join(root, file)
  49. if os.path.isfile(name) and "Cargo.toml" in name:
  50. with open(name, "r") as file:
  51. contents = file.read()
  52. data = toml.loads(contents)
  53. license, warning = licensing.translate_license_fedora(data["package"]["license"])
  54. # Normalise
  55. license = license.replace("/", " or ").replace(" / ", " or ")
  56. license = license.replace("Apache-2.0", "ASL 2.0")
  57. license = license.replace("WITH LLVM-exception", "with exceptions")
  58. if "or" in license or "and" in license:
  59. license = f"({license})"
  60. if license == "(MIT or ASL 2.0)":
  61. license = "(ASL 2.0 or MIT)"
  62. if license not in license_list:
  63. if warning is not None:
  64. # Ignore known warnings
  65. if not warning.endswith("LLVM-exception!") and \
  66. not warning.endswith("MIT/Apache-2.0!"):
  67. print(f"{license}: {warning}")
  68. license_list.append(license)
  69. return " and ".join(license_list)
  70. def backup_specfile(spec_file):
  71. time_now = time.strftime("%Y%m%d_%H%M%S")
  72. log.info(f"Backing up file {spec_file} to {spec_file}.{time_now}")
  73. shutil.copy2(spec_file, f"{spec_file}.{time_now}")
  74. def replace_license(spec_file, license_string):
  75. result = []
  76. with open(spec_file, "r") as file:
  77. contents = file.readlines()
  78. for line in contents:
  79. if line.startswith("License: "):
  80. result.append("# IMPORTANT - Check if it looks right. Additionally, "
  81. "compare with the original line. Then, remove this comment and # FIX ME - part.\n")
  82. result.append(f"# FIX ME - License: GPLv3+ and {license_string}\n")
  83. else:
  84. result.append(line)
  85. with open(spec_file, "w") as file:
  86. file.writelines(result)
  87. log.info(f"Licenses are successfully updated - {spec_file}")
  88. def clean_specfile(spec_file):
  89. result = []
  90. remove_lines = False
  91. cleaned = False
  92. with open(spec_file, "r") as file:
  93. contents = file.readlines()
  94. log.info(f"Remove '{SPECFILE_COMMENT_LINE}' content from {spec_file}")
  95. for line in contents:
  96. if line == START_LINE:
  97. remove_lines = True
  98. log.debug(f"Remove '{START_LINE}' from {spec_file}")
  99. elif line == END_LINE:
  100. remove_lines = False
  101. cleaned = True
  102. log.debug(f"Remove '{END_LINE}' from {spec_file}")
  103. elif not remove_lines:
  104. result.append(line)
  105. else:
  106. log.debug(f"Remove '{line}' from {spec_file}")
  107. with open(spec_file, "w") as file:
  108. file.writelines(result)
  109. return cleaned
  110. def write_provides_bundled_crate(cargo_lock_file, spec_file, cleaned):
  111. # Generate 'Provides' out of cargo_lock_file
  112. with open(cargo_lock_file, "r") as file:
  113. contents = file.read()
  114. data = toml.loads(contents)
  115. provides_lines = []
  116. for package in data["package"]:
  117. provides_lines.append(f"Provides: bundled(crate({package['name']})) = {package['version'].replace('-', '_')}\n")
  118. # Find a line index where 'Provides' ends
  119. with open(spec_file, "r") as file:
  120. spec_file_lines = file.readlines()
  121. last_provides = -1
  122. for i in range(0, len(spec_file_lines)):
  123. if spec_file_lines[i].startswith("%description"):
  124. break
  125. if spec_file_lines[i].startswith("Provides:"):
  126. last_provides = i
  127. # Insert the generated 'Provides' to the specfile
  128. log.info(f"Add the fresh '{SPECFILE_COMMENT_LINE}' content to {spec_file}")
  129. i = last_provides + 2
  130. spec_file_lines.insert(i, START_LINE)
  131. for line in sorted(provides_lines):
  132. i = i + 1
  133. log.debug(f"Adding '{line[:-1]}' as a line {i} to buffer")
  134. spec_file_lines.insert(i, line)
  135. i = i + 1
  136. spec_file_lines.insert(i, END_LINE)
  137. # Insert an empty line if we haven't cleaned the old content
  138. # (as the old content already has an extra empty line that wasn't removed)
  139. if not cleaned:
  140. i = i + 1
  141. spec_file_lines.insert(i, "\n")
  142. log.debug(f"Commit the buffer to {spec_file}")
  143. with open(spec_file, "w") as file:
  144. file.writelines(spec_file_lines)
  145. if __name__ == '__main__':
  146. args = parser.parse_args()
  147. log = setup_script_logger('bundle-rust-downstream', args.verbose)
  148. log.debug("389-ds-base Rust Crates to Bundled Downstream Specfile tool")
  149. log.debug(f"Called with: {args}")
  150. if not os.path.exists(args.spec_file):
  151. log.info(f"File doesn't exists: {args.spec_file}")
  152. sys.exit(1)
  153. if not os.path.exists(args.cargo_lock_file):
  154. log.info(f"File doesn't exists: {args.cargo_lock_file}")
  155. sys.exit(1)
  156. if args.backup_specfile:
  157. backup_specfile(args.spec_file)
  158. cleaned = clean_specfile(args.spec_file)
  159. write_provides_bundled_crate(args.cargo_lock_file, args.spec_file, cleaned)
  160. license_string = get_license_list(args.vendor_dir)
  161. replace_license(args.spec_file, license_string)
  162. log.info(f"Specfile {args.spec_file} is successfully modified! Please:\n"
  163. "1. Open the specfile with your editor of choice\n"
  164. "2. Make sure that Provides with bundled crates are correct\n"
  165. "3. Follow the instructions for 'License:' field and remove the helper comments")