1 import os
, sys
, re
, subprocess
, argparse
3 # This python3 library provides a few helpful routines that are
4 # used by the latest packaging scripts.
6 default_encoding
= 'utf-8'
8 # Output the msg args to stderr. Accepts all the args that print() accepts.
10 print(*msg
, file=sys
.stderr
)
13 # Output the msg args to stderr and die with a non-zero return-code.
14 # Accepts all the args that print() accepts.
20 # Set this to an encoding name or set it to None to avoid the default encoding idiom.
21 def set_default_encoding(enc
):
22 default_encoding
= enc
25 # Set shell=True if the cmd is a string; sets a default encoding unless raw=True was specified.
26 def _tweak_opts(cmd
, opts
, **maybe_set_args
):
27 def _maybe_set(o
, **msa
): # Only set a value if the user didn't already set it.
28 for var
, val
in msa
.items():
33 _maybe_set(opts
, **maybe_set_args
)
35 if isinstance(cmd
, str):
36 _maybe_set(opts
, shell
=True)
38 want_raw
= opts
.pop('raw', False)
39 if default_encoding
and not want_raw
:
40 _maybe_set(opts
, encoding
=default_encoding
)
42 capture
= opts
.pop('capture', None)
44 if capture
== 'stdout':
45 _maybe_set(opts
, stdout
=subprocess
.PIPE
)
46 elif capture
== 'stderr':
47 _maybe_set(opts
, stderr
=subprocess
.PIPE
)
48 elif capture
== 'output':
49 _maybe_set(opts
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
)
50 elif capture
== 'combined':
51 _maybe_set(opts
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.STDOUT
)
53 discard
= opts
.pop('discard', None)
55 # We DO want to override any already set stdout|stderr values (unlike above).
56 if discard
== 'stdout' or discard
== 'output':
57 opts
['stdout'] = subprocess
.DEVNULL
58 if discard
== 'stderr' or discard
== 'output':
59 opts
['stderr'] = subprocess
.DEVNULL
64 # This does a normal subprocess.run() with some auto-args added to make life easier.
65 def cmd_run(cmd
, **opts
):
66 return subprocess
.run(cmd
, **_tweak_opts(cmd
, opts
))
69 # Like cmd_run() with a default check=True specified.
70 def cmd_chk(cmd
, **opts
):
71 return subprocess
.run(cmd
, **_tweak_opts(cmd
, opts
, check
=True))
74 # Capture stdout in a string and return an object with out, err, and rc (return code).
75 # It defaults to capture='stdout' (so err is empty) but can be overridden using
76 # capture='combined' or capture='output' (the latter populates the err value).
77 def cmd_txt(cmd
, **opts
):
78 input = opts
.pop('input', None)
80 opts
['stdin'] = subprocess
.PIPE
81 proc
= subprocess
.Popen(cmd
, **_tweak_opts(cmd
, opts
, capture
='stdout'))
82 out
, err
= proc
.communicate(input=input)
83 return argparse
.Namespace(out
=out
, err
=err
, rc
=proc
.returncode
)
86 # Just like calling cmd_txt() except that it raises an error if the command has a non-0 return code.
87 # The raised error includes the cmd, the return code, and the captured output.
88 def cmd_txt_chk(cmd
, **opts
):
89 ct
= cmd_txt(cmd
, **opts
)
91 cmd_err
= f
'Command "{cmd}" returned non-0 exit status "{ct.rc}" and output:\n{ct.out}{ct.err}'
92 raise Exception(cmd_err
)
96 # Starts a piped-output command of stdout (by default) and leaves it up to you to read
97 # the output and call communicate() on the returned object.
98 def cmd_pipe(cmd
, **opts
):
99 return subprocess
.Popen(cmd
, **_tweak_opts(cmd
, opts
, capture
='stdout'))
102 # Runs a "git status" command and dies if the checkout is not clean (the
103 # arg fatal_unless_clean can be used to make that non-fatal. Returns a
104 # tuple of the current branch, the is_clean flag, and the status text.
105 def check_git_status(fatal_unless_clean
=True, subdir
='.'):
106 status_txt
= cmd_txt_chk(f
"cd '{subdir}' && git status").out
107 is_clean
= re
.search(r
'\nnothing to commit.+working (directory|tree) clean', status_txt
) != None
109 if not is_clean
and fatal_unless_clean
:
113 subdir
= f
" *{subdir}*"
114 die(f
"The{subdir} checkout is not clean:\n" + status_txt
)
116 m
= re
.match(r
'^(?:# )?On branch (.+)\n', status_txt
)
117 cur_branch
= m
[1] if m
else None
119 return (cur_branch
, is_clean
, status_txt
)
122 # Calls check_git_status() on the current git checkout and (optionally) a subdir path's
123 # checkout. Use fatal_unless_clean to indicate if an unclean checkout is fatal or not.
124 # The master_branch arg indicates what branch we want both checkouts to be using, and
125 # if the branch is wrong the user is given the option of either switching to the right
126 # branch or aborting.
127 def check_git_state(master_branch
, fatal_unless_clean
=True, check_extra_dir
=None):
128 cur_branch
= check_git_status(fatal_unless_clean
)[0]
129 branch
= re
.sub(r
'^patch/([^/]+)/[^/]+$', r
'\1', cur_branch
) # change patch/BRANCH/PATCH_NAME into BRANCH
130 if branch
!= master_branch
:
131 print(f
"The checkout is not on the {master_branch} branch.")
132 if master_branch
!= 'master':
134 ans
= input(f
"Do you want me to continue with --branch={branch}? [n] ")
135 if not ans
or not re
.match(r
'^y', ans
, flags
=re
.I
):
137 master_branch
= branch
139 if check_extra_dir
and os
.path
.isdir(os
.path
.join(check_extra_dir
, '.git')):
140 branch
= check_git_status(fatal_unless_clean
, check_extra_dir
)[0]
141 if branch
!= master_branch
:
142 print(f
"The *{check_extra_dir}* checkout is on branch {branch}, not branch {master_branch}.")
143 ans
= input(f
"Do you want to change it to branch {master_branch}? [n] ")
144 if not ans
or not re
.match(r
'^y', ans
, flags
=re
.I
):
146 subdir
.check_call(f
"cd {check_extra_dir} && git checkout '{master_branch}'", shell
=True)
148 return (cur_branch
, master_branch
)
151 # Return the git hash of the most recent commit.
152 def latest_git_hash(branch
):
153 out
= cmd_txt_chk(['git', 'log', '-1', '--no-color', branch
]).out
154 m
= re
.search(r
'^commit (\S+)', out
, flags
=re
.M
)
156 die(f
"Unable to determine commit hash for master branch: {branch}")
160 # Return a set of all branch names that have the format "patch/BASE_BRANCH/NAME"
161 # for the given base_branch string. Just the NAME portion is put into the set.
162 def get_patch_branches(base_branch
):
164 proc
= cmd_pipe('git branch -l'.split())
165 for line
in proc
.stdout
:
166 m
= re
.search(r
' patch/([^/]+)/(.+)', line
)
167 if m
and m
[1] == base_branch
:
173 def mandate_gensend_hook():
174 hook
= '.git/hooks/pre-push'
175 if not os
.path
.exists(hook
):
176 print('Creating hook file:', hook
)
177 cmd_chk(['./rsync', '-a', 'packaging/pre-push', hook
])
179 ct
= cmd_txt(['fgrep', 'make gensend', hook
], discard
='output')
181 die('Please add a "make gensend" into your', hook
, 'script.')
184 # Snag the GENFILES values out of the Makefile file and return them as a list.
185 def get_gen_files(want_dir_plus_list
=False):
186 cont_re
= re
.compile(r
'\\\n')
190 auto_dir
= os
.path
.join('auto-build-save', cmd_txt('git rev-parse --abbrev-ref HEAD').out
.strip().replace('/', '%'))
192 with
open(auto_dir
+ '/Makefile', 'r', encoding
='utf-8') as fh
:
195 chk
= re
.sub(r
'^GENFILES=', '', line
)
199 m
= re
.search(r
'\\$', line
)
200 line
= re
.sub(r
'^\s+|\s*\\\n?$|\s+$', '', line
)
201 gen_files
+= line
.split()
205 if want_dir_plus_list
:
206 return (auto_dir
, gen_files
)
208 return [ os
.path
.join(auto_dir
, fn
) for fn
in gen_files
]
211 def get_rsync_version():
212 with
open('version.h', 'r', encoding
='utf-8') as fh
:
214 m
= re
.match(r
'^#define\s+RSYNC_VERSION\s+"(\d.+?)"', txt
)
217 die("Unable to find RSYNC_VERSION define in version.h")
220 def get_NEWS_version_info():
221 rel_re
= re
.compile(r
'^\| \S{2} \w{3} \d{4}\s+\|\s+(?P<ver>\d+\.\d+\.\d+)\s+\|\s+(?P<pdate>\d{2} \w{3} \d{4})?\s+\|\s+(?P<pver>\d+)\s+\|')
222 last_version
= last_protocol_version
= None
225 with
open('NEWS.md', 'r', encoding
='utf-8') as fh
:
227 if not last_version
: # Find the first non-dev|pre version with a release date.
228 m
= re
.search(r
'rsync (\d+\.\d+\.\d+) .*\d\d\d\d', line
)
231 m
= rel_re
.match(line
)
234 pdate
[m
['ver']] = m
['pdate']
235 if m
['ver'] == last_version
:
236 last_protocol_version
= m
['pver']
238 if not last_protocol_version
:
239 die(f
"Unable to determine protocol_version for {last_version}.")
241 return last_version
, last_protocol_version
, pdate
244 def get_protocol_versions():
245 protocol_version
= subprotocol_version
= None
247 with
open('rsync.h', 'r', encoding
='utf-8') as fh
:
249 m
= re
.match(r
'^#define\s+PROTOCOL_VERSION\s+(\d+)', line
)
251 protocol_version
= m
[1]
253 m
= re
.match(r
'^#define\s+SUBPROTOCOL_VERSION\s+(\d+)', line
)
255 subprotocol_version
= m
[1]
258 if not protocol_version
:
259 die("Unable to determine the current PROTOCOL_VERSION.")
261 if not subprotocol_version
:
262 die("Unable to determine the current SUBPROTOCOL_VERSION.")
264 return protocol_version
, subprotocol_version