11#!/usr/bin/env python
22
33import sys
4- import os
54from pathlib import Path
65import subprocess
6+ import argparse
7+ import re
78
89PUBLIC_HEADER = '''
910// Copyright {YEAR} RISC Zero, Inc.
1920// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2021// See the License for the specific language governing permissions and
2122// limitations under the License.
22- ''' .strip ().splitlines ()
23+ ''' .strip ()
24+
25+ PUBLIC_HEADER_RE = re .compile (
26+ "^"
27+ + PUBLIC_HEADER .replace ("(" , "\\ (" )
28+ .replace (")" , "\\ )" )
29+ .replace ("{YEAR}" , "(?P<year>[0-9]+)" ),
30+ )
2331
2432EXTENSIONS = [
2533 '.cpp' ,
3644]
3745
3846def check_header (expected_year , lines_actual ):
39- for (expected , actual ) in zip (PUBLIC_HEADER , lines_actual ):
47+ for (expected , actual ) in zip (PUBLIC_HEADER . splitlines () , lines_actual ):
4048 expected = expected .replace ('{YEAR}' , expected_year )
4149 if expected != actual :
4250 return (expected , actual )
4351 return None
4452
4553
46- def check_file (root , file ):
54+ def fix_file (file_obj , file_contents , start , end , insert ):
55+ file_contents = file_contents [:start ] + insert + file_contents [end :]
56+ file_obj .seek (0 )
57+ file_obj .truncate ()
58+ file_obj .write (file_contents )
59+
60+
61+ def is_comment_line (line : str ) -> bool :
62+ return line .strip ().startswith ("//" )
63+
64+
65+ def is_probably_license_block (lines : list [str ]) -> bool :
66+ license_keywords = ["copyright" , "license" , "spdx" , "apache" , "mit" ]
67+ text = "\n " .join (lines ).lower ()
68+ return any (kw in text for kw in license_keywords )
69+
70+
71+ def find_license_block (file_contents : str ) -> tuple [int , int ] | None :
72+ """Return (char_start, char_end) span of a license block, or None if not found."""
73+ lines = file_contents .splitlines (keepends = True )
74+
75+ license_lines = []
76+ for line in lines :
77+ stripped = line .strip ()
78+ if stripped == "" or is_comment_line (stripped ):
79+ license_lines .append (line )
80+ else :
81+ break
82+
83+ if license_lines and is_probably_license_block (license_lines ):
84+ char_start = 0
85+ char_end = sum (len (line ) for line in license_lines )
86+ return char_start , char_end
87+
88+ return None
89+
90+
91+ def check_file (root , file , fix ):
4792 cmd = ['git' , 'log' , '-1' , '--format=%ad' , '--date=format:%Y' , '--' , file ]
4893 expected_year = subprocess .check_output (cmd , encoding = 'UTF-8' ).strip ()
4994 rel_path = file .relative_to (root )
50- lines = file .read_text ().splitlines ()
51- result = check_header (expected_year , lines )
52- if result :
53- print (f'{ rel_path } : invalid header!' )
54- print (f' expected: { result [0 ]} ' )
55- print (f' actual: { result [1 ]} ' )
56- return 1
95+
96+ with open (file , "r+" ) as file_obj :
97+ file_contents = file_obj .read ()
98+ match = PUBLIC_HEADER_RE .match (file_contents )
99+
100+ if match :
101+ actual_year = match .group ("year" )
102+ if actual_year != expected_year :
103+ print (f'{ rel_path } : invalid header!' )
104+ print (f'license has wrong year { actual_year } , expected { expected_year } ' )
105+ if fix :
106+ print (f'fixing { rel_path } ' )
107+ start , end = match .span (1 )
108+ fix_file (file_obj , file_contents , start , end , expected_year )
109+ else :
110+ return 1
111+ else :
112+ lines = file_contents .splitlines ()
113+ result = check_header (expected_year , lines )
114+ if result :
115+ print (f'{ rel_path } : invalid header!' )
116+ print (f' expected: { result [0 ]} ' )
117+ print (f' actual: { result [1 ]} ' )
118+ if fix :
119+ print (f'fixing { rel_path } ' )
120+ new_header = PUBLIC_HEADER .replace ("{YEAR}" , expected_year ) + "\n \n "
121+ span = find_license_block (file_contents )
122+ if span :
123+ start , end = span
124+ fix_file (file_obj , file_contents , start , end , new_header )
125+ else :
126+ fix_file (file_obj , file_contents , 0 , 0 , new_header )
127+ else :
128+ return 1
129+
57130 return 0
58131
59132
@@ -72,8 +145,19 @@ def tracked_files():
72145
73146
74147def main ():
148+ parser = argparse .ArgumentParser (
149+ description = "to update years, use the --fix option"
150+ )
151+ parser .add_argument ("--file" , type = Path )
152+ parser .add_argument (
153+ "--fix" , action = "store_true" , help = "modify files with correct year"
154+ )
155+ args = parser .parse_args ()
156+
75157 root = repo_root ()
76158 ret = 0
159+ if args .file :
160+ sys .exit (check_file (root , args .file .resolve (), args .fix ))
77161 for path in tracked_files ():
78162 if path .suffix in EXTENSIONS :
79163 skip = False
@@ -84,7 +168,7 @@ def main():
84168 if skip :
85169 continue
86170
87- ret |= check_file (root , path )
171+ ret |= check_file (root , path , args . fix )
88172 sys .exit (ret )
89173
90174
0 commit comments