1313from .util import get_commands , get_config
1414from .util import run as _run
1515
16- install_dir = "build-install"
17- build_dir = "build"
18-
1916
2017class 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
241256def 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
379421def 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
796844def 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