|
2 | 2 |
|
3 | 3 | require 'find' |
4 | 4 |
|
5 | | -license_text = "/*\n" + File.read('LICENSE') + "*/\n\n" |
| 5 | +## |
| 6 | +# Builds the canonical license block that will be inserted at the top of files |
| 7 | +# when needed. |
| 8 | +def build_license_block |
| 9 | + license_body = File.read('LICENSE') |
| 10 | + "/*\n#{license_body}*/\n\n" |
| 11 | +end |
| 12 | + |
| 13 | +## |
| 14 | +# Normalizes a string by collapsing all whitespace (spaces, tabs, newlines) |
| 15 | +# into single spaces and trimming the ends. Used to compare license text while |
| 16 | +# ignoring formatting differences such as indentation or wrapping. |
| 17 | +def normalize_whitespace(text) |
| 18 | + text.gsub(/\s+/, ' ').strip |
| 19 | +end |
| 20 | + |
| 21 | +## |
| 22 | +# Given the contents of a file, extracts the very first comment at the top of |
| 23 | +# the file if it exists and returns a tuple of: |
| 24 | +# - full_comment_with_markers: the raw comment including comment markers |
| 25 | +# - inner_comment_text: the text inside the comment with common decorations removed |
| 26 | +# - range_end_index: the index where the comment ends in the file contents |
| 27 | +# If there is no top-of-file comment, returns [nil, nil, 0]. |
| 28 | +def extract_top_comment(content) |
| 29 | + # Handle optional UTF-8 BOM at start |
| 30 | + content = content.sub(/^\uFEFF/, '') |
| 31 | + |
| 32 | + if content.start_with?('/*') |
| 33 | + end_idx = content.index('*/', 2) |
| 34 | + return [nil, nil, 0] unless end_idx |
| 35 | + full = content[0..(end_idx + 1)] |
| 36 | + inner = full[2..-3] |
| 37 | + # Remove leading '*' decoration often found in block comments |
| 38 | + inner = inner.lines.map { |line| line.sub(/^\s*\*\s?/, '') }.join |
| 39 | + return [full, inner, end_idx + 2] |
| 40 | + elsif content.start_with?('//') |
| 41 | + lines = [] |
| 42 | + consumed = 0 |
| 43 | + content.each_line do |line| |
| 44 | + break unless line.start_with?('//') |
| 45 | + lines << line |
| 46 | + consumed += line.bytesize |
| 47 | + end |
| 48 | + full = lines.join |
| 49 | + inner = lines.map { |l| l.sub(/^\/\/\s?/, '') }.join |
| 50 | + return [full, inner, consumed] |
| 51 | + else |
| 52 | + return [nil, nil, 0] |
| 53 | + end |
| 54 | +end |
| 55 | + |
| 56 | +## |
| 57 | +# Returns true if the provided file content begins with a license comment whose |
| 58 | +# normalized text matches the repository LICENSE content, ignoring whitespace. |
| 59 | +def license_at_top?(content, normalized_license) |
| 60 | + _full, inner, _ = extract_top_comment(content) |
| 61 | + return false unless inner |
| 62 | + normalize_whitespace(inner) == normalized_license |
| 63 | +end |
| 64 | + |
| 65 | +## |
| 66 | +# Processes a single file, ensuring the license block exists at the very top of |
| 67 | +# the file. If the file already has the license (with any whitespace formatting), |
| 68 | +# it is left unchanged. Otherwise, the license block is prepended above any |
| 69 | +# existing content. |
| 70 | +def process_file(path, license_block, normalized_license, write: true) |
| 71 | + content = File.read(path) |
| 72 | + |
| 73 | + # If license already at the very top (ignoring whitespace differences), do nothing. |
| 74 | + return false if license_at_top?(content, normalized_license) |
| 75 | + |
| 76 | + # Remove leading BOM and leading blank lines to place license at true top. |
| 77 | + content = content.sub(/^\uFEFF/, '') |
| 78 | + content = content.sub(/^\n+/, '') |
| 79 | + |
| 80 | + updated = license_block + content |
| 81 | + if write |
| 82 | + File.write(path, updated) |
| 83 | + end |
| 84 | + true |
| 85 | +end |
| 86 | + |
| 87 | +## |
| 88 | +# Iterates through a directory tree and applies the license update for supported |
| 89 | +# file extensions. Returns the list of files that would be or were modified. |
| 90 | +def copy_license(dir, license_block, normalized_license, check_only: false) |
| 91 | + modified = [] |
| 92 | + supported_exts = %w[.swift .h .mm .java .js .ts .tsx] |
6 | 93 |
|
7 | | -def copy_license(dir, text) |
8 | 94 | Find.find(dir) do |path| |
9 | | - next unless File.file?(path) && |
10 | | - path.end_with?('.swift') || |
11 | | - path.end_with?('.h') || |
12 | | - path.end_with?('.mm') || |
13 | | - path.end_with?('.java') || |
14 | | - path.end_with?('.js') || |
15 | | - path.end_with?('.ts') || |
16 | | - path.end_with?('.tsx') |
17 | | - |
18 | | - print("[copy_license] " + path + "\n") |
19 | | - |
20 | | - content = File.read(path) |
21 | | - |
22 | | - # Remove existing license |
23 | | - content.sub!(/\/\*.*?\*\//m, '') |
24 | | - # remove existing newlines from start of file |
25 | | - content.gsub!(/\A\n*/, '') |
26 | | - # Add new license |
27 | | - content.prepend(text) |
28 | | - |
29 | | - File.write(path, content) |
| 95 | + next unless File.file?(path) && supported_exts.any? { |ext| path.end_with?(ext) } |
| 96 | + |
| 97 | + changed = process_file(path, license_block, normalized_license, write: !check_only) |
| 98 | + if changed |
| 99 | + modified << path |
| 100 | + puts("[copy_license] #{check_only ? 'would update' : 'updated'} #{path}") |
| 101 | + end |
30 | 102 | end |
| 103 | + |
| 104 | + modified |
31 | 105 | end |
32 | 106 |
|
33 | | -copy_license('modules/@shopify/checkout-sheet-kit/ios', license_text) |
34 | | -copy_license('modules/@shopify/checkout-sheet-kit/android', license_text) |
35 | | -copy_license('modules/@shopify/checkout-sheet-kit/src', license_text) |
| 107 | +license_block = build_license_block |
| 108 | +normalized_license = normalize_whitespace(File.read('LICENSE')) |
| 109 | + |
| 110 | +check_only = ARGV.include?('--check') |
| 111 | + |
| 112 | +modified = [] |
| 113 | +modified += copy_license('modules/@shopify/checkout-sheet-kit/ios', license_block, normalized_license, check_only: check_only) |
| 114 | +modified += copy_license('modules/@shopify/checkout-sheet-kit/android', license_block, normalized_license, check_only: check_only) |
| 115 | +modified += copy_license('modules/@shopify/checkout-sheet-kit/src', license_block, normalized_license, check_only: check_only) |
| 116 | + |
| 117 | +if check_only |
| 118 | + if modified.empty? |
| 119 | + puts('[copy_license] all files compliant') |
| 120 | + exit 0 |
| 121 | + else |
| 122 | + puts("[copy_license] non-compliant files: #{modified.count}") |
| 123 | + exit 1 |
| 124 | + end |
| 125 | +end |
0 commit comments