3 # Copyright 2022 by Moshe Kaplan
4 # Based on make-version.pl by Jörg Mayer
6 # Wireshark - Network traffic analyzer
7 # By Gerald Combs <gerald@wireshark.org>
8 # Copyright 1998 Gerald Combs
10 # SPDX-License-Identifier: GPL-2.0-or-later
12 # See below for usage.
14 # If run with the "-r" or "--set-release" argument the VERSION macro in
15 # CMakeLists.txt will have the version_extra template appended to the
16 # version number. vcs_version.h will _not_ be generated if either argument is
19 # make-version.py is called during the build to update vcs_version.h in the build
20 # directory. To set a fixed version, use something like:
22 # cmake -DVCSVERSION_OVERRIDE="Git v3.1.0 packaged as 3.1.0-1"
25 # XXX - We're pretty dumb about the "{vcsinfo}" substitution, and about having
26 # spaces in the package format.
37 GIT_ABBREV_LENGTH
= 12
39 # `git archive` will use an 'export-subst' entry in .gitattributes to replace
40 # the $Format strings with `git log --pretty=format:` placeholders.
41 # The output will look something like the following:
42 # GIT_EXPORT_SUBST_H = '51315cf37cdf6c0add1b1c99cb7941aac4489a6f'
43 # GIT_EXPORT_SUBST_D = 'HEAD -> master, upstream/master, upstream/HEAD'
44 # If the text "$Format" is still present, it means that
45 # git archive did not replace the $Format string, which
46 # means that this not a git archive.
47 GIT_EXPORT_SUBST_H
= '$Format:%H$'
48 GIT_EXPORT_SUBST_D
= '$Format:%D$'
49 IS_GIT_ARCHIVE
= not GIT_EXPORT_SUBST_H
.startswith('$Format')
52 def update_cmakelists_txt(src_dir
, set_version
, repo_data
):
53 if not set_version
and repo_data
['package_string'] == "":
56 cmake_filepath
= os
.path
.join(src_dir
, "CMakeLists.txt")
58 with
open(cmake_filepath
, encoding
='utf-8') as fh
:
59 cmake_contents
= fh
.read()
61 MAJOR_PATTERN
= r
"^set *\( *PROJECT_MAJOR_VERSION *\d+ *\)$"
62 MINOR_PATTERN
= r
"^set *\( *PROJECT_MINOR_VERSION *\d+ *\)$"
63 PATCH_PATTERN
= r
"^set *\( *PROJECT_PATCH_VERSION *\d+ *\)$"
64 VERSION_EXTENSION_PATTERN
= r
"^set *\( *PROJECT_VERSION_EXTENSION .*?$"
66 new_cmake_contents
= cmake_contents
67 new_cmake_contents
= re
.sub(MAJOR_PATTERN
,
68 f
"set(PROJECT_MAJOR_VERSION {repo_data['version_major']})",
71 new_cmake_contents
= re
.sub(MINOR_PATTERN
,
72 f
"set(PROJECT_MINOR_VERSION {repo_data['version_minor']})",
75 new_cmake_contents
= re
.sub(PATCH_PATTERN
,
76 f
"set(PROJECT_PATCH_VERSION {repo_data['version_patch']})",
79 new_cmake_contents
= re
.sub(VERSION_EXTENSION_PATTERN
,
80 f
"set(PROJECT_VERSION_EXTENSION \"{repo_data['package_string']}\")",
84 with
open(cmake_filepath
, mode
='w', encoding
='utf-8') as fh
:
85 fh
.write(new_cmake_contents
)
86 print(cmake_filepath
+ " has been updated.")
89 def update_debian_changelog(src_dir
, repo_data
):
90 # Read packaging/debian/changelog, then write back out an updated version.
92 deb_changelog_filepath
= os
.path
.join(src_dir
, "packaging", "debian", "changelog")
93 with
open(deb_changelog_filepath
, encoding
='utf-8') as fh
:
94 changelog_contents
= fh
.read()
96 CHANGELOG_PATTERN
= r
"^.*"
97 text_replacement
= f
"wireshark ({repo_data['version_major']}.{repo_data['version_minor']}.{repo_data['version_patch']}{repo_data['package_string']}) UNRELEASED; urgency=low"
98 # Note: Only need to replace the first line, so we don't use re.MULTILINE or re.DOTALL
99 new_changelog_contents
= re
.sub(CHANGELOG_PATTERN
, text_replacement
, changelog_contents
)
100 with
open(deb_changelog_filepath
, mode
='w', encoding
='utf-8') as fh
:
101 fh
.write(new_changelog_contents
)
102 print(deb_changelog_filepath
+ " has been updated.")
105 def create_version_file(version_f
, repo_data
):
106 'Write the version to the specified file handle'
108 version_f
.write(f
"{repo_data['version_major']}.{repo_data['version_minor']}.{repo_data['version_patch']}{repo_data['package_string']}\n")
109 print(version_f
.name
+ " has been created.")
112 def update_attributes_asciidoc(src_dir
, repo_data
):
113 # Read doc/attributes.adoc, then write it back out with an updated
114 # wireshark-version replacement line.
115 asiidoc_filepath
= os
.path
.join(src_dir
, "doc", "attributes.adoc")
116 with
open(asiidoc_filepath
, encoding
='utf-8') as fh
:
117 asciidoc_contents
= fh
.read()
119 # Sample line (without quotes): ":wireshark-version: 2.3.1"
120 ASCIIDOC_PATTERN
= r
"^:wireshark-version:.*$"
121 text_replacement
= f
":wireshark-version: {repo_data['version_major']}.{repo_data['version_minor']}.{repo_data['version_patch']}"
123 new_asciidoc_contents
= re
.sub(ASCIIDOC_PATTERN
, text_replacement
, asciidoc_contents
, flags
=re
.MULTILINE
)
125 with
open(asiidoc_filepath
, mode
='w', encoding
='utf-8') as fh
:
126 fh
.write(new_asciidoc_contents
)
127 print(asiidoc_filepath
+ " has been updated.")
130 def update_docinfo_asciidoc(src_dir
, repo_data
):
132 doc_paths
+= [os
.path
.join(src_dir
, 'doc', 'wsdg_src', 'developer-guide-docinfo.xml')]
133 doc_paths
+= [os
.path
.join(src_dir
, 'doc', 'wsug_src', 'user-guide-docinfo.xml')]
135 for doc_path
in doc_paths
:
136 with
open(doc_path
, encoding
='utf-8') as fh
:
137 doc_contents
= fh
.read()
139 # Sample line (without quotes): "<subtitle>For Wireshark 1.2</subtitle>"
140 DOC_PATTERN
= r
"^<subtitle>For Wireshark \d+.\d+<\/subtitle>$"
141 text_replacement
= f
"<subtitle>For Wireshark {repo_data['version_major']}.{repo_data['version_minor']}</subtitle>"
143 new_doc_contents
= re
.sub(DOC_PATTERN
, text_replacement
, doc_contents
, flags
=re
.MULTILINE
)
145 with
open(doc_path
, mode
='w', encoding
='utf-8') as fh
:
146 fh
.write(new_doc_contents
)
147 print(doc_path
+ " has been updated.")
150 def update_cmake_lib_releases(src_dir
, repo_data
):
151 # Read CMakeLists.txt for each library, then write back out an updated version.
153 dir_paths
+= [os
.path
.join(src_dir
, 'epan')]
154 dir_paths
+= [os
.path
.join(src_dir
, 'wiretap')]
156 for dir_path
in dir_paths
:
157 cmakelists_filepath
= os
.path
.join(dir_path
, "CMakeLists.txt")
158 with
open(cmakelists_filepath
, encoding
='utf-8') as fh
:
159 cmakelists_contents
= fh
.read()
161 # Sample line (without quotes; note leading tab: " VERSION "0.0.0" SOVERSION 0")
162 VERSION_PATTERN
= r
'^(\s*VERSION\s+"\d+\.\d+\.)\d+'
163 replacement_text
= f
"\\g<1>{repo_data['version_patch']}"
164 new_cmakelists_contents
= re
.sub(VERSION_PATTERN
,
169 with
open(cmakelists_filepath
, mode
='w', encoding
='utf-8') as fh
:
170 fh
.write(new_cmakelists_contents
)
171 print(cmakelists_filepath
+ " has been updated.")
174 # Update distributed files that contain any version information
175 def update_versioned_files(src_dir
, set_version
, repo_data
):
176 update_cmakelists_txt(src_dir
, set_version
, repo_data
)
177 update_debian_changelog(src_dir
, repo_data
)
179 update_attributes_asciidoc(src_dir
, repo_data
)
180 update_docinfo_asciidoc(src_dir
, repo_data
)
181 update_cmake_lib_releases(src_dir
, repo_data
)
184 def generate_version_h(repo_data
):
185 # Generate new contents of version.h from repository data
187 num_commits_line
= '#define VCS_NUM_COMMITS "0"\n'
189 commit_id_line
= '/* #undef VCS_COMMIT_ID */\n'
191 if not repo_data
.get('enable_vcsversion'):
192 return '/* #undef VCS_VERSION */\n' + num_commits_line
+ commit_id_line
194 if repo_data
.get('num_commits'):
195 num_commits_line
= f
'#define VCS_NUM_COMMITS "{int(repo_data["num_commits"])}"\n'
197 if repo_data
.get('commit_id'):
198 commit_id_line
= f
'#define VCS_COMMIT_ID "{repo_data["commit_id"]}"'
200 if repo_data
.get('git_description'):
201 # Do not bother adding the git branch, the git describe output
202 # normally contains the base tag and commit ID which is more
203 # than sufficient to determine the actual source tree.
204 return f
'#define VCS_VERSION "{repo_data["git_description"]}"\n' + num_commits_line
+ commit_id_line
206 if repo_data
.get('last_change') and repo_data
.get('num_commits'):
207 version_string
= f
"v{repo_data['version_major']}.{repo_data['version_minor']}.{repo_data['version_patch']}"
208 vcs_line
= f
'#define VCS_VERSION "{version_string}-Git-{repo_data["num_commits"]}"\n'
209 return vcs_line
+ num_commits_line
+ commit_id_line
211 if repo_data
.get('commit_id'):
212 vcs_line
= f
'#define VCS_VERSION "Git commit {repo_data["commit_id"]}"\n'
213 return vcs_line
+ num_commits_line
+ commit_id_line
215 vcs_line
= '#define VCS_VERSION "Git Rev Unknown from unknown"\n'
217 return vcs_line
+ num_commits_line
+ commit_id_line
220 def print_VCS_REVISION(version_file
, repo_data
, set_vcs
):
221 # Write the version control system's version to $version_file.
222 # Don't change the file if it is not needed.
224 # XXX - We might want to add VCS_VERSION to CMakeLists.txt so that it can
225 # generate vcs_version.h independently.
227 new_version_h
= generate_version_h(repo_data
)
230 if os
.path
.exists(version_file
):
231 with
open(version_file
, encoding
='utf-8') as fh
:
232 current_version_h
= fh
.read()
233 if current_version_h
== new_version_h
:
240 with
open(version_file
, mode
='w', encoding
='utf-8') as fh
:
241 fh
.write(new_version_h
)
242 print(version_file
+ " has been updated.")
243 elif not repo_data
['enable_vcsversion']:
244 print(version_file
+ " disabled.")
246 print(version_file
+ " unchanged.")
250 def get_version(cmakelists_file_data
):
251 # Reads major, minor, and patch
253 # set(PROJECT_MAJOR_VERSION 3)
254 # set(PROJECT_MINOR_VERSION 7)
255 # set(PROJECT_PATCH_VERSION 2)
257 MAJOR_PATTERN
= r
"^set *\( *PROJECT_MAJOR_VERSION *(\d+) *\)$"
258 MINOR_PATTERN
= r
"^set *\( *PROJECT_MINOR_VERSION *(\d+) *\)$"
259 PATCH_PATTERN
= r
"^set *\( *PROJECT_PATCH_VERSION *(\d+) *\)$"
261 major_match
= re
.search(MAJOR_PATTERN
, cmakelists_file_data
, re
.MULTILINE
)
262 minor_match
= re
.search(MINOR_PATTERN
, cmakelists_file_data
, re
.MULTILINE
)
263 patch_match
= re
.search(PATCH_PATTERN
, cmakelists_file_data
, re
.MULTILINE
)
266 raise Exception("Couldn't get major version")
268 raise Exception("Couldn't get minor version")
270 raise Exception("Couldn't get patch version")
272 major_version
= major_match
.groups()[0]
273 minor_version
= minor_match
.groups()[0]
274 patch_version
= patch_match
.groups()[0]
275 return major_version
, minor_version
, patch_version
278 def read_git_archive(tagged_version_extra
, untagged_version_extra
):
279 # Reads key data from the git repo.
280 # For git archives, this does not need to access the source directory because
281 # `git archive` will use an 'export-subst' entry in .gitattributes to replace
282 # the value for GIT_EXPORT_SUBST_H in the script.
283 # Returns a dictionary with key values from the repository
286 for git_ref
in GIT_EXPORT_SUBST_D
.split(r
', '):
287 match
= re
.match(r
'^tag: (v[1-9].+)', git_ref
)
290 vcs_tag
= match
.groups()[0]
293 print(f
"We are on tag {vcs_tag}.")
294 package_string
= tagged_version_extra
296 print("We are not tagged.")
297 package_string
= untagged_version_extra
299 # Always 0 commits for a git archive
302 # Assume a full commit hash, abbreviate it.
303 commit_id
= GIT_EXPORT_SUBST_H
[:GIT_ABBREV_LENGTH
]
304 package_string
= package_string
.replace("{vcsinfo}", str(num_commits
) + "-" + commit_id
)
307 repo_data
['commit_id'] = commit_id
308 repo_data
['enable_vcsversion'] = True
309 repo_data
['info_source'] = "git archive"
310 repo_data
['is_tagged'] = is_tagged
311 repo_data
['num_commits'] = num_commits
312 repo_data
['package_string'] = package_string
316 def read_git_repo(src_dir
, tagged_version_extra
, untagged_version_extra
):
317 # Reads metadata from the git repo for generating the version string
318 # Returns the data in a dict
320 IS_GIT_INSTALLED
= shutil
.which('git') != ''
321 if not IS_GIT_INSTALLED
:
322 print("Git unavailable. Git revision will be missing from version string.", file=sys
.stderr
)
325 GIT_DIR
= os
.path
.join(src_dir
, '.git')
326 # Check whether to include VCS version information in vcs_version.h
327 enable_vcsversion
= True
328 git_get_commondir_cmd
= shlex
.split(f
'git --git-dir="{GIT_DIR}" rev-parse --git-common-dir')
329 git_commondir
= subprocess
.check_output(git_get_commondir_cmd
, universal_newlines
=True).strip()
330 if git_commondir
and os
.path
.exists(f
"{git_commondir}{os.sep}wireshark-disable-versioning"):
331 print("Header versioning disabled using git override.")
332 enable_vcsversion
= False
334 git_last_changetime_cmd
= shlex
.split(f
'git --git-dir="{GIT_DIR}" log -1 --pretty=format:%at')
335 git_last_changetime
= subprocess
.check_output(git_last_changetime_cmd
, universal_newlines
=True).strip()
337 # Commits since last annotated tag.
338 # Output could be something like: v3.7.2rc0-64-g84d83a8292cb
340 git_last_annotated_cmd
= shlex
.split(f
'git --git-dir="{GIT_DIR}" describe --abbrev={GIT_ABBREV_LENGTH} --long --always --match "v[1-9]*"')
341 git_last_annotated
= subprocess
.check_output(git_last_annotated_cmd
, universal_newlines
=True).strip()
342 parts
= git_last_annotated
.split('-')
343 git_description
= git_last_annotated
345 num_commits
= int(parts
[1])
348 commit_id
= parts
[-1]
350 release_candidate
= ''
351 RC_PATTERN
= r
'^v\d+\.\d+\.\d+(rc\d+)$'
352 match
= re
.match(RC_PATTERN
, parts
[0])
354 release_candidate
= match
.groups()[0]
356 # This command is expected to fail if the version is not tagged
358 git_vcs_tag_cmd
= shlex
.split(f
'git --git-dir="{GIT_DIR}" describe --exact-match --match "v[1-9]*"')
359 git_vcs_tag
= subprocess
.check_output(git_vcs_tag_cmd
, stderr
=subprocess
.DEVNULL
, universal_newlines
=True).strip()
361 except subprocess
.CalledProcessError
:
366 # Get the timestamp; format is similar to: 2022-06-27 23:09:20 -0400
367 # Note: This doesn't appear to be used, only checked for command success
368 git_timestamp_cmd
= shlex
.split(f
'git --git-dir="{GIT_DIR}" log --format="%ad" -n 1 --date=iso')
369 git_timestamp
= subprocess
.check_output(git_timestamp_cmd
, universal_newlines
=True).strip()
372 print(f
"We are on tag {git_vcs_tag}.")
373 package_string
= tagged_version_extra
375 print("We are not tagged.")
376 package_string
= untagged_version_extra
378 package_string
= release_candidate
+ package_string
.replace("{vcsinfo}", str(num_commits
) + "-" + commit_id
)
381 repo_data
['commit_id'] = commit_id
382 repo_data
['enable_vcsversion'] = enable_vcsversion
383 repo_data
['git_timestamp'] = git_timestamp
384 repo_data
['git_description'] = git_description
385 repo_data
['info_source'] = "Command line (git)"
386 repo_data
['is_tagged'] = is_tagged
387 repo_data
['last_change'] = git_last_changetime
388 repo_data
['num_commits'] = num_commits
389 repo_data
['package_string'] = package_string
393 def parse_versionstring(version_arg
):
394 version_parts
= version_arg
.split('.')
395 if len(version_parts
) != 3:
396 msg
= "Version must have three numbers of the form x.y.z. You entered: " + version_arg
397 raise argparse
.ArgumentTypeError(msg
)
398 for i
, version_type
in enumerate(('Major', 'Minor', 'Patch')):
400 int(version_parts
[i
])
402 msg
= f
"{version_type} version must be a number! {version_type} version was '{version_parts[i]}'"
403 raise argparse
.ArgumentTypeError(msg
)
407 def read_repo_info(src_dir
, tagged_version_extra
, untagged_version_extra
):
409 repo_data
= read_git_archive(tagged_version_extra
, untagged_version_extra
)
410 elif os
.path
.exists(src_dir
+ os
.sep
+ '.git') and not os
.path
.exists(os
.path
.join(src_dir
, '.git', 'svn')):
411 repo_data
= read_git_repo(src_dir
, tagged_version_extra
, untagged_version_extra
)
413 raise Exception(src_dir
+ " does not appear to be a git repo or git archive!")
415 cmake_path
= os
.path
.join(src_dir
, "CMakeLists.txt")
416 with
open(cmake_path
, encoding
='utf-8') as fh
:
417 version_major
, version_minor
, version_patch
= get_version(fh
.read())
418 repo_data
['version_major'] = version_major
419 repo_data
['version_minor'] = version_minor
420 repo_data
['version_patch'] = version_patch
425 # CMakeLists.txt calls this with no arguments to create vcs_version.h
426 # AppVeyor calls this with --set-release --untagged-version-extra=-{vcsinfo}-AppVeyor --tagged-version-extra=-AppVeyor
427 # .gitlab-ci calls this with --set-release
428 # Release checklist requires --set-version
430 parser
= argparse
.ArgumentParser(description
='Wireshark file and package versions')
431 action_group
= parser
.add_mutually_exclusive_group()
432 action_group
.add_argument('--set-version', '-v', metavar
='<x.y.z>', type=parse_versionstring
, help='Set the major, minor, and patch versions in the top-level CMakeLists.txt, doc/attributes.adoc, packaging/debian/changelog, and the CMakeLists.txt for all libraries to the provided version number')
433 action_group
.add_argument('--set-release', '-r', action
='store_true', help='Set the extra release information in the top-level CMakeLists.txt based on either default or command-line specified options.')
434 setrel_group
= parser
.add_argument_group()
435 setrel_group
.add_argument('--tagged-version-extra', '-t', default
="", help="Extra version information format to use when a tag is found. No format \
436 (an empty string) is used by default.")
437 setrel_group
.add_argument('--untagged-version-extra', '-u', default
='-{vcsinfo}', help='Extra version information format to use when no tag is found. The format "-{vcsinfo}" (the number of commits and commit ID) is used by default.')
438 parser
.add_argument('--version-file', '-f', metavar
='<file>', type=argparse
.FileType('w'), help='path to version file')
439 parser
.add_argument("src_dir", metavar
='src_dir', nargs
=1, help="path to source code")
440 args
= parser
.parse_args()
442 if args
.version_file
and not args
.set_release
:
443 sys
.stderr
.write('Error: --version-file must be used with --set-release.\n')
446 src_dir
= args
.src_dir
[0]
450 repo_data
['version_major'] = args
.set_version
[0]
451 repo_data
['version_minor'] = args
.set_version
[1]
452 repo_data
['version_patch'] = args
.set_version
[2]
453 repo_data
['package_string'] = ''
455 repo_data
= read_repo_info(src_dir
, args
.tagged_version_extra
, args
.untagged_version_extra
)
457 set_vcs
= not (args
.set_release
or args
.set_version
)
458 VERSION_FILE
= 'vcs_version.h'
459 print_VCS_REVISION(VERSION_FILE
, repo_data
, set_vcs
)
461 if args
.set_release
or args
.set_version
:
462 update_versioned_files(src_dir
, args
.set_version
, repo_data
)
464 if args
.version_file
:
465 create_version_file(args
.version_file
, repo_data
)
469 if __name__
== "__main__":