forked from Brewtarget/brewtarget
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbt
executable file
·2301 lines (2166 loc) · 134 KB
/
bt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
#
# bt is part of Brewtarget, and is copyright the following authors 2022-2023:
# • Matt Young <[email protected]>
#
# Brewtarget is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# Brewtarget is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with this program. If not, see
# <http://www.gnu.org/licenses/>.
#
#-----------------------------------------------------------------------------------------------------------------------
# This build tool (bt) script helps with Git setup, meson build configuration and packaging. You need to have Python
# 3.10 or newer installed first. On Windows you also need to have Pip installed and to do some extra manual stuff noted
# below.
#
# Usage is:
#
# ./bt setup Sets up Git options and configures the 'mbuild' meson build directory
#
# ./bt setup all As above but also tries to install all tools and dependencies we need
#
# ./bt package Does the packaging. First runs 'meson install' (with extra options to "install"
# binaries, data etc to a subdirectory of the build directory rather than to where they
# need to be for a local install). Then creates a distributable package, making use
# of various build variables passed back from Meson.
#
# NOTE: This tool, used in conjunction with Meson, is now the official way to build and package the software. We
# continue to support CMake for local compiles and installs, but not for packaging.
#
# .:TODO:. At some point we should be able to retire:
# configure
# setupgit.sh
# CMakeLists.txt
# src/CMakeLists.txt
#
# .:TODO:. We should probably also break this file up into several smaller ones!
#
# Note that Python allows both single and double quotes for delimiting strings. In Meson, we need single quotes, in
# C++, we need double quotes. We mostly try to use single quotes below for consistency with Meson, except where using
# double quotes avoids having to escape a single quote.
#-----------------------------------------------------------------------------------------------------------------------
#-----------------------------------------------------------------------------------------------------------------------
# **********************************************************************************************************************
# *
# * WINDOWS USERS PLEASE NOTE that, on Windows, we assume you are running in the MSYS2 MINGW32 environment. This is one
# * of, typically, four different environments available to you after installing MSYS2. You must run this script from
# * the "MSYS2 MinGW 32-bit" shell, and not one of the other ones.
# *
# * Additionally on Windows, there are also a couple of extra things you need to do before running this bt script:
# *
# * - For historical reasons, Linux and other platforms need to run both Python v2 (still used by some bits of
# * system) and Python v3 (eg that you installed yourself) so there are usually two corresponding Python
# * executables, python2 and python3. On Windows there is only whatever Python you installed and it's called
# * python.exe. To keep the shebang in the bt script working, we just make a softlink to python called python3:
# *
# * if [[ ! -f $(dirname $(which python))/python3 ]]; then ln -s $(which python) $(dirname $(which python))/python3; fi
# *
# * - Getting Unicode input/output to work is fun. We should already have a Unicode locale, but it seems we also
# * need to set PYTHONIOENCODING (see https://docs.python.org/3/using/cmdline.html#envvar-PYTHONIOENCODING, even
# * though it seems to imply you don't need to set it on recent versions of Python).
# *
# * export PYTHONIOENCODING=utf8
# *
# * - The version of Pip we install above does not put it in the "right" place. Specifically it will not be in the
# * PATH when we run bt. The following seems to be the least hacky way around this:
# *
# * curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
# * python get-pip.py
# * python -m pip install -U --force-reinstall pip
# * rm get-pip.py
# *
# * See https://stackoverflow.com/questions/48087004/installing-pip-on-msys for more discussion on this.
# *
# **********************************************************************************************************************
#-----------------------------------------------------------------------------------------------------------------------
#-----------------------------------------------------------------------------------------------------------------------
# Python built-in modules we use
#-----------------------------------------------------------------------------------------------------------------------
import argparse
import datetime
import glob
import logging
import os
import pathlib
import platform
import re
import shutil
import stat
import subprocess
import sys
import tempfile
from decimal import Decimal
#-----------------------------------------------------------------------------------------------------------------------
# Global constants
#-----------------------------------------------------------------------------------------------------------------------
# There is some inevitable duplication with constants in meson.build, but we try to keep it to a minimum
projectName = 'brewtarget'
capitalisedProjectName = projectName.capitalize()
projectUrl = 'https://github.com/' + capitalisedProjectName + '/' + projectName + '/'
#-----------------------------------------------------------------------------------------------------------------------
# Welcome banner and environment info
#-----------------------------------------------------------------------------------------------------------------------
# The '%c' argument to strftime means "Locale’s appropriate date and time representation"
print(
'⭐ ' + capitalisedProjectName + ' Build Tool (bt) running on ' + platform.system() + ' (' + platform.release() +
'), using Python ' + platform.python_version() + ', at ' + datetime.datetime.now().strftime('%c') + ' ⭐'
)
#-----------------------------------------------------------------------------------------------------------------------
# Set up logging to stderr
#-----------------------------------------------------------------------------------------------------------------------
logging.basicConfig(format='%(message)s')
log = logging.getLogger(__name__)
# This is our default log level, but it can be overridden via the -v and -q command line options -- see below
log.setLevel(logging.INFO)
# Include the log level in the message
handler = logging.StreamHandler()
handler.setFormatter(
# You can add timestamps etc to logs, but that's overkill for this script. Source file location of log message is
# however pretty useful for debugging.
logging.Formatter('{levelname:s}: {message} [{filename:s}:{lineno:d}]', style='{')
)
log.addHandler(handler)
# If we don't do this, everything gets printed twice
log.propagate = False
#-----------------------------------------------------------------------------------------------------------------------
# Python 3rd-party modules we use
#-----------------------------------------------------------------------------------------------------------------------
# Per https://docs.python.org/3/library/ensurepip.html, the official way to ensure Pip is installed and at the latest
# version is via 'python -m ensurepip --upgrade' (which should of course be 'python3 -m ensurepip --upgrade' on systems
# that have both Python 2 and Python 3). However, on Debian/Ubuntu, this will give an error "No module named ensurepip"
# because ensurepip is deliberately disabled to push you towards using 'sudo apt update' +
# 'sudo apt install python3-pip'.
if (platform.system() != 'Linux'):
# https://docs.python.org/3/library/sys.html#sys.executable says sys.executable is '"the absolute path of the
# executable binary for the Python interpreter, on systems where this makes sense".
log.info(
'Attempting to ensure latest version of Pip is installed via ' + sys.executable + ' -m ensurepip --upgrade'
)
subprocess.run([sys.executable, '-m', 'ensurepip', '--upgrade'])
else:
# We don't want to run a sudo command every time the script is invoked, so check whether it's necessary
exe_pip = shutil.which('pip3')
if (exe_pip is None or exe_pip == ''):
log.info('Attempting to install Pip')
subprocess.run(['sudo', 'apt', 'update'])
subprocess.run(['sudo', 'apt', 'install', 'python3-pip'])
# If Pip still isn't installed we need to bail here.
exe_pip = shutil.which('pip3')
if (exe_pip is None or exe_pip == ''):
pathEnvVar = ''
if ('PATH' in os.environ):
pathEnvVar = os.environ['PATH']
log.critical(
'Cannot find pip (PATH=' + pathEnvVar + ') - please see https://pip.pypa.io/en/stable/installation/ for how to ' +
'install'
)
exit(1)
#
# We use the packaging module (see https://pypi.org/project/packaging/) for handling version numbers (as described at
# https://packaging.pypa.io/en/stable/version.html).
#
# On MacOS at least, we also need to install setuptools to be able to access packaging.version.
#
subprocess.run([exe_pip, 'install', 'packaging'])
subprocess.run([exe_pip, 'install', 'setuptools'])
import packaging.version
# The requests library (see https://pypi.org/project/requests/) is used for downloading files in a more Pythonic way
# than invoking wget through the shell.
subprocess.run([exe_pip, 'install', 'requests'])
import requests
#
# Once all platforms we're running on have Python version 3.11 or above, we will be able to use the built-in tomllib
# library (see https://docs.python.org/3/library/tomllib.html) for parsing TOML. Until then, it's easier to import the
# tomlkit library (see https://pypi.org/project/tomlkit/) which actually has rather more functionality than we need
#
subprocess.run([exe_pip, 'install', 'tomlkit'])
import tomlkit
#-----------------------------------------------------------------------------------------------------------------------
# Parse command line arguments
#-----------------------------------------------------------------------------------------------------------------------
# We do this (nearly) first as we want the program to exit straight away if incorrect arguments are specified
# Choosing which action to call is done a the end of the script, after all functions are defined
#
# Using Python argparse saves us writing a lot of boilerplate, although the help text it generates on the command line
# is perhaps a bit more than we want (eg having to separate 'bt --help' and 'bt setup --help' is overkill for us).
# There are ways around this -- eg see
# https://stackoverflow.com/questions/20094215/argparse-subparser-monolithic-help-output -- but they are probably more
# complexity than is merited here.
#
parser = argparse.ArgumentParser(
prog = 'bt',
description = capitalisedProjectName + ' build tool. A utility to help with installing dependencies, Git ' +
'setup, Meson build configuration and packaging.',
epilog = 'See ' + projectUrl + ' for info and latest releases'
)
# Log level
group = parser.add_mutually_exclusive_group()
group.add_argument('-v', '--verbose', action = 'store_true', help = 'Enable debug logging of this script')
group.add_argument('-q', '--quiet', action = 'store_true', help = 'Suppress info logging of this script')
# Per https://docs.python.org/3/library/argparse.html#sub-commands, you use sub-parsers for sub-commands.
subparsers = parser.add_subparsers(
dest = 'subCommand',
required = True,
title = 'action',
description = "Exactly one of the following actions must be specified. (For actions marked ✴, specify -h or "
"--help AFTER the action for info about options -- eg '%(prog)s setup --help'.)"
)
# Parser for 'setup'
parser_setup = subparsers.add_parser('setup', help = '✴ Set up meson build directory (mbuild) and git options')
subparsers_setup = parser_setup.add_subparsers(dest = 'setupOption', required = False)
parser_setup_all = subparsers_setup.add_parser(
'all',
help = 'Specifying this will also automatically install libraries and frameworks we depend on'
)
# Parser for 'package'
parser_package = subparsers.add_parser('package', help='Build a distributable installer')
#
# Process the arguments for use below
#
# This try/expect ensures that help is printed if the script is invoked without arguments. It's not perfect as you get
# the usage line twice (because parser.parse_args() outputs it to stderr before throwing SystemExit) but it's good
# enough for now at least.
#
try:
args = parser.parse_args()
except SystemExit as se:
if (se.code != None and se.code != 0):
parser.print_help()
sys.exit(0)
#
# The one thing we do set straight away is log level
# Possible levels are 'CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG', 'NOTSET'. We choose 'INFO' for default, 'DEBUG'
# for verbose and 'WARNING' for quiet. You wouldn't want to suppress warnings, would you? :-)
#
if (args.verbose):
log.setLevel(logging.DEBUG)
elif (args.quiet):
log.setLevel(logging.WARNING)
log.debug('Parsed command line arguments as ' + str(args))
#-----------------------------------------------------------------------------------------------------------------------
# Note the working directory from which we were invoked -- though it shouldn't matter as we try to be independent of
# this
#-----------------------------------------------------------------------------------------------------------------------
log.debug('Working directory when invoked: ' + pathlib.Path.cwd().as_posix())
#-----------------------------------------------------------------------------------------------------------------------
# Directories
#-----------------------------------------------------------------------------------------------------------------------
dir_base = pathlib.Path(__file__).parent.resolve()
dir_gitInfo = dir_base.joinpath('.git')
dir_build = dir_base.joinpath('mbuild')
# Where submodules live and how many there are. Currently there are 2: libbacktrace and valijson
dir_gitSubmodules = dir_base.joinpath('third-party')
num_gitSubmodules = 2
# Top-level packaging directory - NB deliberately different name from 'packaging' (= dir_base.joinpath('packaging'))
dir_packages = dir_build.joinpath('packages')
dir_packages_platform = dir_packages.joinpath(platform.system().lower()) # Platform-specific packaging directory
dir_packages_source = dir_packages.joinpath('source')
#-----------------------------------------------------------------------------------------------------------------------
# Helper function for checking result of running external commands
#
# Given a CompletedProcess object returned from subprocess.run(), this checks the return code and, if it is non-zero
# stops this script with an error message and the same return code. Otherwise the CompletedProcess object is returned
# to the caller (to make it easier to chain things together).
#-----------------------------------------------------------------------------------------------------------------------
def abortOnRunFail(runResult: subprocess.CompletedProcess):
if (runResult.returncode != 0):
# According to https://docs.python.org/3/library/subprocess.html#subprocess.CompletedProcess,
# CompletedProcess.args (the arguments used to launch the process) "may be a list or a string", but its not clear
# when it would be one or the other.
if (isinstance(runResult.args, str)):
log.critical('Error running ' + runResult.args)
else:
commandName = os.path.basename(runResult.args[0])
log.critical('Error running ' + commandName + ' (' + ' '.join(str(ii) for ii in runResult.args) + ')')
exit(runResult.returncode)
return runResult
#-----------------------------------------------------------------------------------------------------------------------
# Helper function for copying one or more files to a directory that might not yet exist
#-----------------------------------------------------------------------------------------------------------------------
def copyFilesToDir(files, directory):
os.makedirs(directory, exist_ok=True)
for currentFile in files:
log.debug('Copying ' + currentFile + ' to ' + directory)
shutil.copy2(currentFile, directory)
return
#-----------------------------------------------------------------------------------------------------------------------
# Helper function for counting files in a directory tree
#-----------------------------------------------------------------------------------------------------------------------
def numFilesInTree(path):
numFiles = 0
for root, dirs, files in os.walk(path):
numFiles += len(files)
return numFiles
#-----------------------------------------------------------------------------------------------------------------------
# Helper function for downloading a file
#-----------------------------------------------------------------------------------------------------------------------
def downloadFile(url):
filename = url.split('/')[-1]
log.info('Downloading ' + url + ' to ' + filename + ' in directory ' + pathlib.Path.cwd().as_posix())
response = requests.get(url)
if (response.status_code != 200):
log.critical('Error code ' + response.status_code + ' while downloading ' + url)
exit(1)
with open(filename, 'wb') as fd:
for chunk in response.iter_content(chunk_size = 128):
fd.write(chunk)
return
#-----------------------------------------------------------------------------------------------------------------------
# Set global variables exe_git and exe_meson with the locations of the git and meson executables plus mesonVersion with
# the version of meson installed
#
# We want to give helpful error messages if Meson or Git is not installed. For other missing dependencies we can rely
# on Meson itself to give explanatory error messages.
#-----------------------------------------------------------------------------------------------------------------------
def findMesonAndGit():
# Advice at https://docs.python.org/3/library/subprocess.html is "For maximum reliability, use a fully qualified path
# for the executable. To search for an unqualified name on PATH, use shutil.which()"
# Check Meson is installed. (See installDependencies() below for what we do to attempt to install it from this
# script.)
global exe_meson
exe_meson = shutil.which("meson")
if (exe_meson is None or exe_meson == ""):
log.critical('Cannot find meson - please see https://mesonbuild.com/Getting-meson.html for how to install')
exit(1)
global mesonVersion
rawVersion = abortOnRunFail(subprocess.run([exe_meson, '--version'], capture_output=True)).stdout.decode('UTF-8').rstrip()
log.debug('Meson version raw: ' + rawVersion)
mesonVersion = packaging.version.parse(rawVersion)
log.debug('Meson version parsed: ' + str(mesonVersion))
# Check Git is installed if its magic directory is present
global exe_git
exe_git = shutil.which("git")
if (dir_gitInfo.is_dir()):
log.debug('Found git information directory:' + dir_gitInfo.as_posix())
if (exe_git is None or exe_git == ""):
log.critical('Cannot find git - please see https://git-scm.com/downloads for how to install')
exit(1)
return
#-----------------------------------------------------------------------------------------------------------------------
# Copy a file, removing comments and folded lines
#
# Have had various problems with comments in debian package control file, even though they are theoretically allowed, so
# we strip them out here, hence slightly more involved code than just
# shutil.copy2(dir_build.joinpath('control'), dir_packages_deb_control)
#
# Similarly, some of the fields in the debian control file that we want to split across multiple lines are not actually
# allowed to be so "folded" by the Debian package generator. So, we do our own folding here. (At the same time, we
# remove extra spaces that make sense on the unfolded line but not once everything is joined onto single line.)
#-----------------------------------------------------------------------------------------------------------------------
def copyWithoutCommentsOrFolds(inputPath, outputPath):
with open(inputPath, 'r') as inputFile, open(outputPath, 'w') as outputFile:
for line in inputFile:
if (not line.startswith('#')):
if (not line.endswith('\\\n')):
outputFile.write(line)
else:
foldedLine = ""
while (line.endswith('\\\n')):
foldedLine += line.removesuffix('\\\n')
line = next(inputFile)
foldedLine += line
# The split and join here is a handy trick for removing repeated spaces from the line without
# fumbling around with regular expressions. Note that this takes the newline off the end, hence
# why we have to add it back manually.
outputFile.write(' '.join(foldedLine.split()))
outputFile.write('\n')
return
#-----------------------------------------------------------------------------------------------------------------------
# Create fileToDistribute.sha256sum for a given fileToDistribute in a given directory
#-----------------------------------------------------------------------------------------------------------------------
def writeSha256sum(directory, fileToDistribute):
#
# In Python 3.11 we could use the file_digest() function from the hashlib module to do this. But it's rather
# more work to do in Python 3.10, so we just use the `sha256sum` command instead.
#
# Note however, that `sha256sum` includes the supplied directory path of a file in its output. We want just the
# filename, not its full or partial path on the build machine. So we change into the directory of the file before
# running the `sha256sum` command.
#
previousWorkingDirectory = pathlib.Path.cwd().as_posix()
os.chdir(directory)
with open(directory.joinpath(fileToDistribute + '.sha256sum').as_posix(),'w') as sha256File:
abortOnRunFail(
subprocess.run(['sha256sum', fileToDistribute],
capture_output=False,
stdout=sha256File)
)
os.chdir(previousWorkingDirectory)
return
#-----------------------------------------------------------------------------------------------------------------------
# Ensure git submodules are present
#
# When a git repository is cloned, the submodules don't get cloned until you specifically ask for it via the
# --recurse-submodules flag.
#
# (Adding submodules is done via Git itself. Eg:
# cd ../third-party
# git submodule add https://github.com/ianlancetaylor/libbacktrace
# But this only needs to be done once, by one person, and committed to our repository, where the connection is
# stored in the .gitmodules file.)
#-----------------------------------------------------------------------------------------------------------------------
def ensureSubmodulesPresent():
findMesonAndGit()
if (not dir_gitSubmodules.is_dir()):
log.info('Creating submodules directory: ' + dir_gitSubmodules.as_posix())
os.makedirs(dir_gitSubmodules, exist_ok=True)
if (numFilesInTree(dir_gitSubmodules) < num_gitSubmodules):
log.info('Pulling in submodules in ' + dir_gitSubmodules.as_posix())
abortOnRunFail(subprocess.run([exe_git, "submodule", "init"], capture_output=False))
abortOnRunFail(subprocess.run([exe_git, "submodule", "update"], capture_output=False))
return
#-----------------------------------------------------------------------------------------------------------------------
# Function to install dependencies -- called if the user runs 'bt setup all'
#-----------------------------------------------------------------------------------------------------------------------
def installDependencies():
log.info('Checking which dependencies need to be installed')
#
# I looked at using ConanCenter (https://conan.io/center/) as a source of libraries, so that we could automate
# installing dependencies, but it does not have all the ones we need. Eg it has Boost, Qt, Xerces-C and Valijson,
# but not Xalan-C. (Someone else has already requested Xalan-C, see
# https://github.com/conan-io/conan-center-index/issues/5546, but that request has been open a long time, so its
# fulfilment doesn't seem imminent.) It also doesn't yet integrate quite as well with meson as we might like (eg
# as at 2023-01-15, https://docs.conan.io/en/latest/reference/conanfile/tools/meson.html is listed as "experimental
# and subject to breaking changes".
#
# Another option is vcpkg (https://vcpkg.io/en/index.html), which does have both Xerces-C and Xalan-C, along with
# Boost, Qt and Valijson. There is an example here https://github.com/Neumann-A/meson-vcpkg of how to use vcpkg from
# Meson. However, it's pretty slow to get started with because it builds from source everything it installs
# (including tools it depends on such as CMake) -- even if they are already installed on your system from another
# source. This is laudably correct but I'm too impatient to do things that way.
#
# Will probably take another look at Conan in future, subject to working out how to have it use already-installed
# versions of libraries/frameworks if they are present. The recommended way to install Conan is via a Python
# package, which makes that part easy. However, there is a fair bit of other ramp-up to do, and some breaking
# changes between "current" Conan 1.X and "soon-to-be-released" Conan 2.0. So, will leave it for now and stick
# mostly to native installs for each of the 3 platforms (Linux, Windows, Mac).
#
# Other notes:
# - GNU coreutils (https://www.gnu.org/software/coreutils/manual/coreutils.html) is probably already installed on
# most Linux distros, but not necessarily on Mac and Windows. It gives us sha256sum.
#
match platform.system():
#-----------------------------------------------------------------------------------------------------------------
#---------------------------------------------- Linux Dependencies -----------------------------------------------
#-----------------------------------------------------------------------------------------------------------------
case 'Linux':
#
# NOTE: For the moment at least, we are assuming you are on Ubuntu or another Debian-based Linux. For other
# flavours of the OS you need to install libraries and frameworks manually.
#
#
# We need a recent version of Boost, ie Boost 1.79 or newer, to use Boost.JSON. For Windows and Mac this is
# fine if you are installing from MSYS2 (https://packages.msys2.org/package/mingw-w64-x86_64-boost) or
# Homebrew (https://formulae.brew.sh/formula/boost) respectively. Unfortunately, as of late-2022, many
# Linux distros provide only older versions of Boost. (Eg, on Ubuntu, you can see this by running
# 'apt-cache policy libboost-dev'.)
#
# First, check whether Boost is installed and if so, what version
#
# We'll look in the following places:
# /usr/include/boost/version.hpp <-- Distro-installed Boost
# /usr/local/include/boost/version.hpp <-- Manually-installed Boost
# ${BOOST_ROOT}/boost/version.hpp <-- If the BOOST_ROOT environment variable is set it gives an
# alternative place to look
#
# Although things should compile with 1.79.0, if we're going to all the bother of installing Boost, we'll
# install a more recent one
minBoostVersion = packaging.version.parse('1.79.0')
boostVersionToInstall = packaging.version.parse('1.80.0') # NB: This _must_ have the patch version
maxBoostVersionFound = packaging.version.parse('0')
possibleBoostVersionHeaders = [pathlib.Path('/usr/include/boost/version.hpp'),
pathlib.Path('/usr/local/include/boost/version.hpp')]
if ('BOOST_ROOT' in os.environ):
possibleBoostVersionHeaders.append(pathlib.Path(os.environ['BOOST_ROOT']).joinpath('boost/version.hpp'))
for boostVersionHeader in possibleBoostVersionHeaders:
if (boostVersionHeader.is_file()):
runResult = abortOnRunFail(
subprocess.run(
['grep', '#define BOOST_LIB_VERSION ', boostVersionHeader.as_posix()],
encoding = "utf-8",
capture_output = True
)
)
log.debug('In ' + boostVersionHeader.as_posix() + ' found ' + str(runResult.stdout))
versionFoundRaw = re.sub(
r'^.*BOOST_LIB_VERSION "([0-9_]*)".*$', r'\1', str(runResult.stdout).rstrip()
).replace('_', '.')
versionFound = packaging.version.parse(versionFoundRaw)
if (versionFound > maxBoostVersionFound):
maxBoostVersionFound = versionFound
log.debug('Parsed as ' + str(versionFound) + '. (Highest found = ' + str(maxBoostVersionFound) + ')')
#
# The Boost version.hpp configuration header file gives two constants for defining the version of Boost
# installed:
#
# BOOST_VERSION is a pure numeric value:
# BOOST_VERSION % 100 is the patch level
# BOOST_VERSION / 100 % 1000 is the minor version
# BOOST_VERSION / 100000 is the major version
# So, eg, for Boost 1.79.0 (= 1.079.00), BOOST_VERSION = 107900
#
# BOOST_LIB_VERSION is a string value with underscores instead of dots (and without the patch level if that's
# 0). So, eg for Boost 1.79.0, BOOST_LIB_VERSION = "1_79" (and for 1.23.45 it would be "1_23_45")
#
# We use BOOST_LIB_VERSION as it's easy to convert it to a version number that Python can understand
#
log.debug(
'Max version of Boost found: ' + str(maxBoostVersionFound) + '. Need >= ' + str(minBoostVersion) +
', otherwise will try to install ' + str(boostVersionToInstall)
)
if (maxBoostVersionFound < minBoostVersion):
log.info(
'Installing Boost ' + str(boostVersionToInstall) + ' as newest version found was ' +
str(maxBoostVersionFound)
)
#
# To manually install the latest version of Boost from source, first we uninstall any old version
# installed via the distro (eg, on Ubuntu, this means 'sudo apt remove libboost-all-dev'), then we follow
# the instructions at https://www.boost.org/more/getting_started/index.html.
#
# It's best to leave the default install location: headers in the 'boost' subdirectory of
# /usr/local/include and libraries in /usr/local/lib.
#
# (It might initially _seem_ a good idea to put things in the same place as the distro packages, ie
# running './bootstrap.sh --prefix=/usr' to put headers in /usr/include and libraries in /usr/lib.
# However, this will mean that Meson cannot find the manually-installed Boost, even though it can find
# distro-installed Boost in this location.) So, eg, for Boost 1.80 on Linux, this means the following
# in the shell:
#
# cd ~
# mkdir ~/boost-tmp
# cd ~/boost-tmp
# wget https://boostorg.jfrog.io/artifactory/main/release/1.80.0/source/boost_1_80_0.tar.bz2
# tar --bzip2 -xf boost_1_80_0.tar.bz2
# cd boost_1_80_0
# ./bootstrap.sh
# sudo ./b2 install
# cd ~
# sudo rm -rf ~/boost-tmp
#
# We can handle the temporary directory stuff more elegantly (ie RAII style) in Python however
#
with tempfile.TemporaryDirectory(ignore_cleanup_errors = True) as tmpDirName:
previousWorkingDirectory = pathlib.Path.cwd().as_posix()
os.chdir(tmpDirName)
log.debug('Working directory now ' + pathlib.Path.cwd().as_posix())
boostUnderscoreName = 'boost_' + str(boostVersionToInstall).replace('.', '_')
downloadFile(
'https://boostorg.jfrog.io/artifactory/main/release/' + str(boostVersionToInstall) + '/source/' +
boostUnderscoreName + '.tar.bz2'
)
log.debug('Boost download completed')
shutil.unpack_archive(boostUnderscoreName + '.tar.bz2')
log.debug('Boost archive extracted')
os.chdir(boostUnderscoreName)
log.debug('Working directory now ' + pathlib.Path.cwd().as_posix())
abortOnRunFail(subprocess.run(['./bootstrap.sh']))
log.debug('Boost bootstrap finished')
abortOnRunFail(subprocess.run(['sudo', './b2', 'install']))
log.debug('Boost install finished')
os.chdir(previousWorkingDirectory)
log.debug('Working directory now ' + pathlib.Path.cwd().as_posix() + '. Removing ' + tmpDirName)
#
# The only issue with the RAII approach to removing the temporary directory is that some of the files
# inside it will be owned by root, so there will be a permissions error when Python attempts to delete
# the directory tree. Fixing the permissions beforehand is a slightly clunky way around this.
#
abortOnRunFail(
subprocess.run(
['sudo', 'chmod', '--recursive', 'a+rw', tmpDirName]
)
)
#
# Almost everything else we can rely on the distro packages. A few notes:
# - We need CMake even for the Meson build because meson uses CMake as one of its library-finding tools
# - The pandoc package helps us create man pages from markdown input
# - The build-essential and debhelper packages are for creating Debian packages
# - The rpm and rpmlint packages are for creating RPM packages
#
log.info('Ensuring other libraries and frameworks are installed')
abortOnRunFail(subprocess.run(['sudo', 'apt-get', 'update']))
abortOnRunFail(
subprocess.run(
['sudo', 'apt', 'install', '-y', 'build-essential',
'cmake',
'coreutils',
'debhelper',
'git',
'libqt5multimedia5-plugins',
'libqt5sql5-psql',
'libqt5sql5-sqlite',
'libqt5svg5-dev',
'libxalan-c-dev',
'libxerces-c-dev',
'lintian',
'meson',
'ninja-build',
'pandoc',
'python3',
'qtbase5-dev',
'qtmultimedia5-dev',
'qttools5-dev',
'qttools5-dev-tools',
'rpm',
'rpmlint']
)
)
#
# Ubuntu 20.04 packages only have Meson 0.53.2, and we need 0.60.0 or later. In this case it means we have to
# install Meson via pip, which is not ideal on Linux.
#
# Specifically, as explained at https://mesonbuild.com/Getting-meson.html#installing-meson-with-pip, although
# using the pip3 install gets a newer version, we have to do the pip install as root (which is normally not
# recommended). If we don't do this, then running `meson install` (or even `sudo meson install`) will barf on
# Linux (because we need to be able to install files into system directories).
#
# So, where a sufficiently recent version of Meson is available in the distro packages (eg
# `sudo apt install meson` on Ubuntu etc) it is much better to install this. Installing via pip is a last
# resort.
#
# The distro ID we get from 'lsb_release -is' will be 'Ubuntu' for all the variants of Ubuntu (eg including
# Kubuntu). Not sure what happens on derivatives such as Linux Mint though.
#
distroName = str(
abortOnRunFail(subprocess.run(['lsb_release', '-is'], encoding = "utf-8", capture_output = True)).stdout
).rstrip()
log.debug('Linux distro: ' + distroName)
if ('Ubuntu' == distroName):
ubuntuRelease = str(
abortOnRunFail(subprocess.run(['lsb_release', '-rs'], encoding = "utf-8", capture_output = True)).stdout
).rstrip()
log.debug('Ubuntu release: ' + ubuntuRelease)
if (Decimal(ubuntuRelease) < Decimal('22.04')):
log.info('Installing newer version of Meson the hard way')
abortOnRunFail(subprocess.run(['sudo', 'apt', 'remove', '-y', 'meson']))
abortOnRunFail(subprocess.run(['sudo', 'pip3', 'install', 'meson']))
#-----------------------------------------------------------------------------------------------------------------
#--------------------------------------------- Windows Dependencies ----------------------------------------------
#-----------------------------------------------------------------------------------------------------------------
case 'Windows':
log.debug('Windows')
#
# First thing is to detect whether we're in the MSYS2 environment, and, if so, whether we're in the right
# version of it.
#
# We take the existence of an executable `uname` in the path as a pretty good indicator that we're in MSYS2
# or similar environment). Then the result of running that should tell us if we're in the 32-bit version of
# MSYS2. (See comment below on why we don't yet support the 64-bit version, though I'm sure we'll fix this one
# day.)
#
exe_uname = shutil.which('uname')
if (exe_uname is None or exe_uname == ''):
log.critical('Cannot find uname. This script needs to be run under MSYS2 - see https://www.msys2.org/')
exit(1)
# We could just run uname without the -a option, but the latter gives some useful diagnostics to log
unameResult = str(
abortOnRunFail(subprocess.run([exe_uname, '-a'], encoding = "utf-8", capture_output = True)).stdout
).rstrip()
log.debug('Running uname -a gives ' + unameResult)
# Output from `uname -a` will be of the form
# MINGW64_NT-10.0-19044 Matt-Virt-Win 3.4.3.x86_64 2023-01-11 20:20 UTC x86_64 Msys
# We just need the bit before the first underscore, eg
# MINGW64
terminalVersion = unameResult.split(sep='_', maxsplit=1)[0]
if (terminalVersion != 'MINGW32'):
# One day we'll try to get the 64-bit build working on Windows. I think it's just the packaging step that's
# the problem. For now, it's easier to insist on 32-bit at set-up. (Obviously 32-bit apps run just fine on
# 64-bit Windows. I don't think there would be any noticeable difference to the end user in having a 64-bit
# version of the app.)
log.critical('Running in ' + terminalVersion + ' but need to run in MINGW32 (ie 32-bit build environment)')
exit(1)
log.info('Ensuring required libraries and frameworks are installed')
#
# Before we install packages, we want to make sure the MSYS2 installation itself is up-to-date, otherwise you
# can hit problems
#
# pacman -S -y should download a fresh copy of the master package database
# pacman -S -u should upgrades all currently-installed packages that are out-of-date
#
abortOnRunFail(subprocess.run(['pacman', '-S', '-y', '--noconfirm']))
abortOnRunFail(subprocess.run(['pacman', '-S', '-u', '--noconfirm']))
#
# We'd normally want to go with the 64-bit versions of things (x86_64) but AIUI it's a bit hard to handle this
# in the NSIS installer, so we the 32-bit versions (i686).
#
# We _could_ just invoke pacman once with the list of everything we want to install. However, this can make
# debugging a bit harder when there is a pacman problem, because it doesn't always give the most explanatory
# error messages. So we loop round and install one thing at a time.
#
# Note that the --disable-download-timeout option on Pacman proved often necessary because of slow mirror
# sites, so we now specify it routinely.
#
arch = 'i686'
installList = ['base-devel',
'cmake',
'coreutils',
'doxygen',
'gcc',
'git',
'mingw-w64-' + arch + '-boost',
'mingw-w64-' + arch + '-cmake',
'mingw-w64-' + arch + '-libbacktrace',
'mingw-w64-' + arch + '-meson',
'mingw-w64-' + arch + '-nsis',
'mingw-w64-' + arch + '-qt5-base',
'mingw-w64-' + arch + '-qt5-static',
'mingw-w64-' + arch + '-toolchain',
'mingw-w64-' + arch + '-xalan-c',
'mingw-w64-' + arch + '-xerces-c']
for packageToInstall in installList:
log.debug('Installing ' + packageToInstall)
abortOnRunFail(
subprocess.run(
['pacman', '-S', '--needed', '--noconfirm', '--disable-download-timeout', packageToInstall]
)
)
#
# Download NSIS plugins
#
# In theory we can use RAII here, eg:
#
# with tempfile.TemporaryDirectory(ignore_cleanup_errors = True) as tmpDirName:
# previousWorkingDirectory = pathlib.Path.cwd().as_posix()
# os.chdir(tmpDirName)
# ...
# os.chdir(previousWorkingDirectory)
#
# However, in practice, this gets messy when there is an error (eg download fails) because Windows doesn't like
# deleting files or directories that are in use. So, in the event of the script needing to terminate early,
# you get loads of errors, up to and including "maximum recursion depth exceeded" which rather mask whatever
# the original problem was.
#
tmpDirName = tempfile.mkdtemp()
previousWorkingDirectory = pathlib.Path.cwd().as_posix()
os.chdir(tmpDirName)
downloadFile('https://nsis.sourceforge.io/mediawiki/images/a/af/Locate.zip')
shutil.unpack_archive('Locate.zip', 'Locate')
downloadFile('https://nsis.sourceforge.io/mediawiki/images/7/76/Nsislog.zip')
shutil.unpack_archive('Nsislog.zip', 'Nsislog')
copyFilesToDir(['Locate/Include/Locate.nsh'], '/mingw32/share/nsis/Include/')
copyFilesToDir(['Locate/Plugin/locate.dll',
'Nsislog/plugin/nsislog.dll'],'/mingw32/share/nsis/Plugins/ansi/')
os.chdir(previousWorkingDirectory)
shutil.rmtree(tmpDirName, ignore_errors=False)
#-----------------------------------------------------------------------------------------------------------------
#---------------------------------------------- Mac OS Dependencies ----------------------------------------------
#-----------------------------------------------------------------------------------------------------------------
case 'Darwin':
log.debug('Mac')
#
# We install most of the Mac dependencies via Homebrew (https://brew.sh/) using the `brew` command below.
# However, as at 2023-12-01, Homebrew has stopped supplying a package for Xalan-C. So, we install that using
# MacPorts (https://ports.macports.org/), which provides the `port` command.
#
# Note that MacPorts (port) requires sudo but Homebrew (brew) does not. Perhaps more importantly, they two
# package managers install things to different locations:
# - Homebrew packages are installed under /usr/local/Cellar/ with symlinks in /usr/local/opt/
# - MacPorts packages are installed under /opt/local
# This means we need to have both directories in the include path when we come to compile. Thankfully, both
# CMake and Meson take care of finding a library automatically once given its name.
#
# Note too that package names vary slightly between HomeBrew and MacPorts. Don't assume you can guess one from
# the other, as it's not always evident.
#
#
# Installing Homebrew is, in theory, somewhat easier and more self-contained than MacPorts as you just run the
# following:
# /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# In practice, invoking that command from this script is a bit fiddly to get right. For the moment, we simply
# assume Homebrew is already installed (because it is on the GitHub actions).
#
#
# We install as many of our dependencies as possible with with Homebrew, and do this first, because some of
# these packages will also be needed for the installation of MacPorts to work.
#
# We could make this list shorter if we wanted as, eg, installing Xalan-C will cause Xerces-C to be installed
# too (as the former depends on the latter). However, I think it's clearer to explicitly list all the direct
# dependencies (eg we do make calls directly into Xerces).
#
# For the moment, we install Qt 5 (= 5.15.7), as there are code changes required to use Qt 6
#
# .:TBD:. Installing Boost here doesn't seem to give us libboost_stacktrace_backtrace
# Also, trying to use the "--cc=clang" option to install boost gives an error ("Error: boost: no bottle
# available!") For the moment, we're just using Boost header files on Mac though, so this should be
# OK.
#
# We install the tree command here as, although it's not needed to do the build itself, it's useful for
# diagnosing certain build problems (eg to see what changes certain parts of the build have made to the build
# directory tree) when the build is running as a GitHub action.
#
installListBrew = ['llvm',
'gcc',
'cmake',
'coreutils',
'boost',
'doxygen',
'git',
'meson',
'ninja',
'pandoc',
'tree',
'qt@5',
# 'xalan-c',
# 'xerces-c'
]
for packageToInstall in installListBrew:
log.debug('Installing ' + packageToInstall + ' via Homebrew')
abortOnRunFail(subprocess.run(['brew', 'install', packageToInstall]))
#
# By default, even once Qt5 is installed, Meson will not find it
#
# See https://stackoverflow.com/questions/29431882/get-qt5-up-and-running-on-a-new-mac for suggestion to do
# the following to "symlink the various Qt5 binaries and libraries into your /usr/local/bin and /usr/local/lib
# directories".
#
# Additionally, per lengthy discussion at https://github.com/Homebrew/legacy-homebrew/issues/29938, it seems
# we might also need either:
# ln -s /usr/local/Cellar/qt5/5.15.7/mkspecs /usr/local/mkspecs
# ln -s /usr/local/Cellar/qt5/5.15.7/plugins /usr/local/plugins
# or:
# export PATH=/usr/local/opt/qt5/bin:$PATH
# The former gives permission errors, so we do the latter in mac.yml
#
abortOnRunFail(subprocess.run(['brew', 'link', '--force', 'qt5']))
#
# In theory, we can now install MacPorts. However, I didn't manage to get the following working. The
# configure script complains about the lack of /usr/local/.//mkspecs/macx-clang. So, for now, we comment this
# bit out and install MacPorts for GitHub actions via the mac.yml script.
#
# This is a source install - per instructions at https://guide.macports.org/#installing.macports.source. In
# principle, we could install a precompiled binary, but it's a bit painful to do programatically as even to
# download the right package you have to know not just the the Darwin version of MacOS you are running, but
# also its "release name" (aka "friendly name"). See
# https://apple.stackexchange.com/questions/333452/how-can-i-find-the-friendly-name-of-the-operating-system-from-the-shell-term
# for various ways to do this if we had to, though we might just as easily copy the info
# from https://en.wikipedia.org/wiki/MacOS#Timeline_of_releases
#
## log.debug('Installing MacPorts')
## abortOnRunFail(subprocess.run(['curl', '-O', 'https://distfiles.macports.org/MacPorts/MacPorts-2.8.1.tar.bz2']))
## abortOnRunFail(subprocess.run(['tar', 'xf', 'MacPorts-2.8.1.tar.bz2']))
## abortOnRunFail(subprocess.run(['cd', 'MacPorts-2.8.1/']))
## abortOnRunFail(subprocess.run(['./configure']))
## abortOnRunFail(subprocess.run(['make']))
## abortOnRunFail(subprocess.run(['sudo', 'make', 'install']))
## abortOnRunFail(subprocess.run(['export', 'PATH=/opt/local/bin:/opt/local/sbin:$PATH']))
#
# Now install Xalan-C via MacPorts
#
installListPort = ['xalanc',
'xercesc3']
for packageToInstall in installListPort:
log.debug('Installing ' + packageToInstall + ' via MacPorts')
abortOnRunFail(subprocess.run(['sudo', 'port', 'install', packageToInstall]))
#
# dmgbuild is a Python package that provides a command line tool to create macOS disk images (aka .dmg
# files) -- see https://dmgbuild.readthedocs.io/en/latest/
#
# Note that we install with the [badge_icons] extra so we can use the badge_icon setting (see
# https://dmgbuild.readthedocs.io/en/latest/settings.html#badge_icon)
#
abortOnRunFail(subprocess.run(['pip3', 'install', 'dmgbuild[badge_icons]']))
case _:
log.critical('Unrecognised platform: ' + platform.system())
exit(1)
#--------------------------------------------------------------------------------------------------------------------
#------------------------------------------- Cross-platform Dependencies --------------------------------------------
#--------------------------------------------------------------------------------------------------------------------
#
# We use libbacktrace from https://github.com/ianlancetaylor/libbacktrace. It's not available as a Debian package
# and not any more included with GCC by default. It's not a large library so, unless and until we start using Conan,
# the easiest approach seems to be to bring it in as a Git submodule and compile from source.
#
ensureSubmodulesPresent()
log.info('Checking libbacktrace is built')
previousWorkingDirectory = pathlib.Path.cwd().as_posix()
backtraceDir = dir_gitSubmodules.joinpath('libbacktrace')
os.chdir(backtraceDir)
log.debug('Run configure and make in ' + backtraceDir.as_posix())
#
# We only want to configure and compile libbacktrace once, so we do it here rather than in Meson.build
#
# Libbacktrace uses autoconf/automake so it's relatively simple to build, but for a couple of gotchas
#
# Note that, although on Linux you can just invoke `./configure`, this doesn't work in the MSYS2 environment, so,
# knowing that 'configure' is a shell script, we invoke it as such. However, we must be careful to run it with the
# correct shell, specifically `sh` (aka dash on Linux) rather than `bash`. Otherwise, the Makefile it generates will
# not work properly, and we'll end up building a library with missing symbols that gives link errors on our own
# executables.
#
# (I haven't delved deeply into this but, confusingly, if you run `sh ./configure` it puts 'SHELL = /bin/bash' in the
# Makefile, whereas, if you run `bash ./configure`, it puts the line 'SHELL = /bin/sh' in the Makefile.)
#
abortOnRunFail(subprocess.run(['sh', './configure']))
abortOnRunFail(subprocess.run(['make']))
os.chdir(previousWorkingDirectory)
log.info('*** Finished checking / installing dependencies ***')
return
#-----------------------------------------------------------------------------------------------------------------------
# ./bt setup
#-----------------------------------------------------------------------------------------------------------------------
def doSetup(setupOption):
if (setupOption == 'all'):
installDependencies()
findMesonAndGit()
# If this is a git checkout then let's set up git with the project standards
if (dir_gitInfo.is_dir()):
log.info('Setting up ' + capitalisedProjectName + ' git preferences')
# Enforce indentation with spaces, not tabs.
abortOnRunFail(
subprocess.run(
[exe_git,
"config",
"--file", dir_gitInfo.joinpath('config').as_posix(),
"core.whitespace",
"tabwidth=3,tab-in-indent"],
capture_output=False
)
)
# Enable the standard pre-commit hook that warns you about whitespace errors
shutil.copy2(dir_gitInfo.joinpath('hooks/pre-commit.sample'),
dir_gitInfo.joinpath('hooks/pre-commit'))
ensureSubmodulesPresent()
# Check whether Meson build directory is already set up. (Although nothing bad happens, if you run setup twice,
# it complains and tells you to run configure.)
# Best clue that set-up has been run (rather than, say, user just created empty mbuild directory by hand) is the
# presence of meson-info/meson-info.json (which is created by setup for IDE integration -- see
# https://mesonbuild.com/IDE-integration.html#ide-integration)
runMesonSetup = True
warnAboutCurrentDirectory = False
if (dir_build.joinpath('meson-info/meson-info.json').is_file()):
log.info('Meson build directory ' + dir_build.as_posix() + ' appears to be already set up')
#
# You usually only need to reset things after you've done certain edits to defaults etc in meson.build. There
# are a whole bunch of things you can control with the 'meson configure' command, but it's simplest in some ways