Skip to content

Commit d624569

Browse files
authored
Allow custom build directory (#221)
Closes #171 - Add `-C` flag to meson commands to set custom build dir - Add tests and verify that compilation happens in correct location
2 parents 55b6987 + a2eb125 commit d624569

6 files changed

Lines changed: 181 additions & 104 deletions

File tree

spin/cmds/meson.py

Lines changed: 92 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,6 @@
1313
from .util import get_commands, get_config
1414
from .util import run as _run
1515

16-
install_dir = "build-install"
17-
build_dir = "build"
18-
1916

2017
class GcovReportFormat(str, Enum):
2118
html = "html"
@@ -68,16 +65,15 @@ def _is_editable_install_of_same_source(distname):
6865
return editable_path and os.path.samefile(_editable_install_path(distname), ".")
6966

7067

71-
def _set_pythonpath(quiet=False):
68+
def _set_pythonpath(build_dir, quiet=False):
7269
"""Set first entry of PYTHONPATH to site packages directory.
7370
71+
For editable installs, leave the PYTHONPATH alone.
72+
7473
Returns
7574
-------
7675
site_packages
7776
"""
78-
site_packages = _get_site_packages()
79-
env = os.environ
80-
8177
cfg = get_config()
8278
distname = cfg.get("project.name", None)
8379
if distname:
@@ -101,6 +97,9 @@ def _set_pythonpath(quiet=False):
10197
fg="bright_red",
10298
)
10399

100+
site_packages = _get_site_packages(build_dir)
101+
env = os.environ
102+
104103
if "PYTHONPATH" in env:
105104
env["PYTHONPATH"] = f"{site_packages}{os.pathsep}{env['PYTHONPATH']}"
106105
else:
@@ -114,7 +113,12 @@ def _set_pythonpath(quiet=False):
114113
return site_packages
115114

116115

117-
def _get_site_packages():
116+
def _get_install_dir(build_dir):
117+
return f"{build_dir}-install"
118+
119+
120+
def _get_site_packages(build_dir):
121+
install_dir = _get_install_dir(build_dir)
118122
try:
119123
cfg = get_config()
120124
distname = cfg.get("project.name", None)
@@ -168,9 +172,9 @@ def _meson_version():
168172
pass
169173

170174

171-
def _meson_version_configured():
175+
def _meson_version_configured(build_dir):
172176
try:
173-
meson_info_fn = os.path.join("build", "meson-info", "meson-info.json")
177+
meson_info_fn = os.path.join(build_dir, "meson-info", "meson-info.json")
174178
with open(meson_info_fn) as f:
175179
meson_info = json.load(f)
176180
return meson_info["meson_version"]["full"]
@@ -194,7 +198,7 @@ def _meson_coverage_configured() -> bool:
194198
return False
195199

196200

197-
def _check_coverage_tool_installation(coverage_type: GcovReportFormat):
201+
def _check_coverage_tool_installation(coverage_type: GcovReportFormat, build_dir):
198202
requirements = { # https://github.com/mesonbuild/meson/blob/6e381714c7cb15877e2bcaa304b93c212252ada3/docs/markdown/Unit-tests.md?plain=1#L49-L62
199203
GcovReportFormat.html: ["Gcovr/GenHTML", "lcov"],
200204
GcovReportFormat.xml: ["Gcovr (version 3.3 or higher)"],
@@ -226,6 +230,16 @@ def _check_coverage_tool_installation(coverage_type: GcovReportFormat):
226230
)
227231

228232

233+
build_dir_option = click.option(
234+
"-C",
235+
"--build-dir",
236+
default="build",
237+
show_envvar=True,
238+
envvar="SPIN_BUILD_DIR",
239+
help="Meson build directory; package is installed into './{build-dir}-install'.",
240+
)
241+
242+
229243
@click.command()
230244
@click.option("-j", "--jobs", help="Number of parallel tasks to launch", type=int)
231245
@click.option("--clean", is_flag=True, help="Clean build directory before build")
@@ -238,12 +252,21 @@ def _check_coverage_tool_installation(coverage_type: GcovReportFormat):
238252
help="Enable C code coverage using `gcov`. Use `spin test --gcov` to generate reports.",
239253
)
240254
@click.argument("meson_args", nargs=-1)
255+
@build_dir_option
241256
def build(
242-
*, meson_args, jobs=None, clean=False, verbose=False, gcov=False, quiet=False
257+
*,
258+
meson_args,
259+
jobs=None,
260+
clean=False,
261+
verbose=False,
262+
gcov=False,
263+
quiet=False,
264+
build_dir=None,
243265
):
244266
"""🔧 Build package with Meson/ninja
245267
246-
The package is installed to `build-install`.
268+
The package is installed to `build-install` (unless a different
269+
build directory is specified with `-C`).
247270
248271
MESON_ARGS are passed through e.g.:
249272
@@ -257,7 +280,23 @@ def build(
257280
or set CFLAGS appropriately:
258281
259282
CFLAGS="-O0 -g" spin build
283+
284+
Build into a different build/build-install directory via the
285+
`-C/--build-dir` flag:
286+
287+
spin build -C build-for-feature-x
288+
289+
This feature is useful in combination with a shell alias, e.g.:
290+
291+
$ alias spin-clang="SPIN_BUILD_DIR=build-clang CC=clang spin"
292+
293+
Which can then be used to build (`spin-clang build`), to test (`spin-clang test ...`), etc.
294+
260295
"""
296+
abs_build_dir = os.path.abspath(build_dir)
297+
install_dir = _get_install_dir(build_dir)
298+
abs_install_dir = os.path.abspath(install_dir)
299+
261300
cfg = get_config()
262301
distname = cfg.get("project.name", None)
263302
if distname and _is_editable_install_of_same_source(distname):
@@ -283,7 +322,7 @@ def build(
283322
if os.path.isdir(install_dir):
284323
shutil.rmtree(install_dir)
285324

286-
if not (os.path.exists(build_dir) and _meson_version_configured()):
325+
if not (os.path.exists(build_dir) and _meson_version_configured(build_dir)):
287326
p = _run(setup_cmd, sys_exit=False, output=not quiet)
288327
if p.returncode != 0:
289328
raise RuntimeError(
@@ -293,7 +332,7 @@ def build(
293332
# Build dir has been configured; check if it was configured by
294333
# current version of Meson
295334

296-
if (_meson_version() != _meson_version_configured()) or (
335+
if (_meson_version() != _meson_version_configured(build_dir)) or (
297336
gcov and not _meson_coverage_configured()
298337
):
299338
_run(setup_cmd + ["--reconfigure"], output=not quiet)
@@ -317,7 +356,9 @@ def build(
317356
"-C",
318357
build_dir,
319358
"--destdir",
320-
f"../{install_dir}",
359+
install_dir
360+
if os.path.isabs(install_dir)
361+
else os.path.relpath(abs_install_dir, abs_build_dir),
321362
],
322363
output=(not quiet) and verbose,
323364
)
@@ -375,6 +416,7 @@ def _get_configured_command(command_name):
375416
default="html",
376417
help=f"Format of the gcov report. Can be one of {', '.join(e.value for e in GcovReportFormat)}.",
377418
)
419+
@build_dir_option
378420
@click.pass_context
379421
def test(
380422
ctx,
@@ -386,6 +428,7 @@ def test(
386428
coverage=False,
387429
gcov=None,
388430
gcov_format=None,
431+
build_dir=None,
389432
):
390433
"""🔧 Run tests
391434
@@ -472,13 +515,11 @@ def test(
472515
"Invoking `build` prior to running tests:", bold=True, fg="bright_green"
473516
)
474517
if gcov is not None:
475-
ctx.invoke(build_cmd, gcov=bool(gcov))
518+
ctx.invoke(build_cmd, build_dir=build_dir, gcov=bool(gcov))
476519
else:
477-
ctx.invoke(build_cmd)
520+
ctx.invoke(build_cmd, build_dir=build_dir)
478521

479-
site_path = _set_pythonpath()
480-
if site_path:
481-
print(f'$ export PYTHONPATH="{site_path}"')
522+
site_path = _set_pythonpath(build_dir)
482523

483524
# Sanity check that library built properly
484525
#
@@ -523,6 +564,7 @@ def test(
523564
else:
524565
cmd = ["pytest"]
525566

567+
install_dir = _get_install_dir(build_dir)
526568
if not os.path.exists(install_dir):
527569
os.mkdir(install_dir)
528570

@@ -537,7 +579,7 @@ def test(
537579
bold=True,
538580
fg="bright_yellow",
539581
)
540-
_check_coverage_tool_installation(gcov_format)
582+
_check_coverage_tool_installation(gcov_format, build_dir)
541583

542584
# Generate report
543585
click.secho(
@@ -571,8 +613,9 @@ def test(
571613
@click.command()
572614
@click.option("--code", "-c", help="Python program passed in as a string")
573615
@click.argument("gdb_args", nargs=-1)
616+
@build_dir_option
574617
@click.pass_context
575-
def gdb(ctx, *, code, gdb_args):
618+
def gdb(ctx, *, code, gdb_args, build_dir):
576619
"""👾 Execute code through GDB
577620
578621
spin gdb -c 'import numpy as np; print(np.__version__)'
@@ -598,9 +641,9 @@ def gdb(ctx, *, code, gdb_args):
598641
click.secho(
599642
"Invoking `build` prior to invoking gdb:", bold=True, fg="bright_green"
600643
)
601-
ctx.invoke(build_cmd)
644+
ctx.invoke(build_cmd, build_dir=build_dir)
602645

603-
_set_pythonpath()
646+
_set_pythonpath(build_dir)
604647
gdb_args = list(gdb_args)
605648

606649
if gdb_args and gdb_args[0].endswith(".py"):
@@ -623,8 +666,9 @@ def gdb(ctx, *, code, gdb_args):
623666

624667
@click.command()
625668
@click.argument("ipython_args", nargs=-1)
669+
@build_dir_option
626670
@click.pass_context
627-
def ipython(ctx, *, ipython_args):
671+
def ipython(ctx, *, ipython_args, build_dir):
628672
"""💻 Launch IPython shell with PYTHONPATH set
629673
630674
IPYTHON_ARGS are passed through directly to IPython, e.g.:
@@ -636,18 +680,19 @@ def ipython(ctx, *, ipython_args):
636680
click.secho(
637681
"Invoking `build` prior to invoking ipython:", bold=True, fg="bright_green"
638682
)
639-
ctx.invoke(build_cmd)
683+
ctx.invoke(build_cmd, build_dir=build_dir)
640684

641-
p = _set_pythonpath()
685+
p = _set_pythonpath(build_dir)
642686
if p:
643687
print(f'💻 Launching IPython with PYTHONPATH="{p}"')
644688
_run(["ipython", "--ignore-cwd"] + list(ipython_args), replace=True)
645689

646690

647691
@click.command()
648692
@click.argument("shell_args", nargs=-1)
693+
@build_dir_option
649694
@click.pass_context
650-
def shell(ctx, shell_args=[]):
695+
def shell(ctx, shell_args=[], build_dir=None):
651696
"""💻 Launch shell with PYTHONPATH set
652697
653698
SHELL_ARGS are passed through directly to the shell, e.g.:
@@ -662,9 +707,9 @@ def shell(ctx, shell_args=[]):
662707
click.secho(
663708
"Invoking `build` prior to invoking shell:", bold=True, fg="bright_green"
664709
)
665-
ctx.invoke(build_cmd)
710+
ctx.invoke(build_cmd, build_dir=build_dir)
666711

667-
p = _set_pythonpath()
712+
p = _set_pythonpath(build_dir)
668713
if p:
669714
print(f'💻 Launching shell with PYTHONPATH="{p}"')
670715

@@ -677,8 +722,9 @@ def shell(ctx, shell_args=[]):
677722

678723
@click.command()
679724
@click.argument("python_args", nargs=-1)
725+
@build_dir_option
680726
@click.pass_context
681-
def python(ctx, *, python_args):
727+
def python(ctx, *, python_args, build_dir):
682728
"""🐍 Launch Python shell with PYTHONPATH set
683729
684730
PYTHON_ARGS are passed through directly to Python, e.g.:
@@ -690,9 +736,9 @@ def python(ctx, *, python_args):
690736
click.secho(
691737
"Invoking `build` prior to invoking Python:", bold=True, fg="bright_green"
692738
)
693-
ctx.invoke(build_cmd)
739+
ctx.invoke(build_cmd, build_dir=build_dir)
694740

695-
p = _set_pythonpath()
741+
p = _set_pythonpath(build_dir)
696742
if p:
697743
print(f'🐍 Launching Python with PYTHONPATH="{p}"')
698744

@@ -717,9 +763,10 @@ def python(ctx, *, python_args):
717763

718764

719765
@click.command(context_settings={"ignore_unknown_options": True})
766+
@build_dir_option
720767
@click.argument("args", nargs=-1)
721768
@click.pass_context
722-
def run(ctx, *, args):
769+
def run(ctx, *, args, build_dir=None):
723770
"""🏁 Run a shell command with PYTHONPATH set
724771
725772
\b
@@ -743,7 +790,7 @@ def run(ctx, *, args):
743790
# Redirect spin generated output
744791
with contextlib.redirect_stdout(sys.stderr):
745792
# Also ask build to be quiet
746-
ctx.invoke(build_cmd, quiet=True)
793+
ctx.invoke(build_cmd, build_dir=build_dir, quiet=True)
747794

748795
is_posix = sys.platform in ("linux", "darwin")
749796
shell = len(args) == 1
@@ -754,7 +801,7 @@ def run(ctx, *, args):
754801
# On Windows, we're going to try to use bash
755802
cmd_args = ["bash", "-c", cmd_args]
756803

757-
_set_pythonpath(quiet=True)
804+
_set_pythonpath(build_dir, quiet=True)
758805
p = _run(cmd_args, echo=False, shell=shell, sys_exit=False)
759806

760807
# Is the user trying to run a Python script, without calling the Python interpreter?
@@ -792,6 +839,7 @@ def run(ctx, *, args):
792839
help="Sphinx gallery: enable/disable plots",
793840
)
794841
@click.option("--jobs", "-j", default="auto", help="Number of parallel build jobs")
842+
@build_dir_option
795843
@click.pass_context
796844
def docs(
797845
ctx,
@@ -802,6 +850,7 @@ def docs(
802850
jobs,
803851
sphinx_gallery_plot,
804852
clean_dirs=None,
853+
build_dir=None,
805854
):
806855
"""📖 Build Sphinx documentation
807856
@@ -856,10 +905,10 @@ def docs(
856905
click.secho(
857906
"Invoking `build` prior to building docs:", bold=True, fg="bright_green"
858907
)
859-
ctx.invoke(build_cmd)
908+
ctx.invoke(build_cmd, build_dir=build_dir)
860909

861910
try:
862-
site_path = _get_site_packages()
911+
site_path = _get_site_packages(build_dir)
863912
except FileNotFoundError:
864913
cfg = get_config()
865914
distname = cfg.get("project.name", None)
@@ -894,8 +943,9 @@ def docs(
894943
@click.command()
895944
@click.option("--code", "-c", help="Python program passed in as a string")
896945
@click.argument("lldb_args", nargs=-1)
946+
@build_dir_option
897947
@click.pass_context
898-
def lldb(ctx, *, code, lldb_args):
948+
def lldb(ctx, *, code, lldb_args, build_dir=None):
899949
"""👾 Execute code through LLDB
900950
901951
spin lldb -c 'import numpy as np; print(np.__version__)'
@@ -923,9 +973,9 @@ def lldb(ctx, *, code, lldb_args):
923973
click.secho(
924974
"Invoking `build` prior to invoking lldb:", bold=True, fg="bright_green"
925975
)
926-
ctx.invoke(build_cmd)
976+
ctx.invoke(build_cmd, build_dir=build_dir)
927977

928-
_set_pythonpath()
978+
_set_pythonpath(build_dir)
929979
lldb_args = list(lldb_args)
930980

931981
if code:

0 commit comments

Comments
 (0)