diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f2edbe0d..e2b6c9d0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -37,7 +37,8 @@ jobs: - name: Set CXX (temporary) if: runner.os == 'Windows' - run: echo "CXX=icl" >> $GITHUB_ENV + run: | + echo "CXX=icl" >> $GITHUB_ENV - name: Setup Graphviz if: runner.os == 'Linux' diff --git a/.github/workflows/pymake-gcc.yml b/.github/workflows/pymake-gcc.yml index 547d794e..69d72e7e 100644 --- a/.github/workflows/pymake-gcc.yml +++ b/.github/workflows/pymake-gcc.yml @@ -46,6 +46,11 @@ jobs: compiler: gcc version: 11 + - name: Set CXX (temporary) + if: runner.os == 'Windows' + run: | + echo "CXX=g++" >> $GITHUB_ENV + - name: Download examples for pytest runs run: | .github/common/download-examples.sh diff --git a/autotest/conftest.py b/autotest/conftest.py index 5df2876b..1a113280 100644 --- a/autotest/conftest.py +++ b/autotest/conftest.py @@ -15,17 +15,6 @@ # misc utilities -@contextlib.contextmanager -def working_directory(path): - """Changes working directory and returns to previous on exit.""" - prev_cwd = os.getcwd() - os.chdir(path) - try: - yield - finally: - os.chdir(prev_cwd) - - def get_pymake_appdir(): appdir = Path.home() / ".pymake" appdir.mkdir(parents=True, exist_ok=True) diff --git a/autotest/test_build.py b/autotest/test_build.py new file mode 100644 index 00000000..c8961cf9 --- /dev/null +++ b/autotest/test_build.py @@ -0,0 +1,113 @@ +import os +import sys +import time + +import pytest +from flaky import flaky +from modflow_devtools.misc import get_ostag, set_dir + +import pymake + +RERUNS = 3 + +targets = pymake.usgs_program_data.get_keys(current=True) +targets_make = [ + t + for t in targets + if t not in ("libmf6", "gridgen", "mf2000", "swtv4", "mflgr") +] +test_ostag = get_ostag() +test_fc_env = os.environ.get("FC") +if "win" in test_ostag: + meson_exclude = ("mt3dms", "vs2dt", "triangle", "gridgen") +elif "win" not in test_ostag and test_fc_env in ("ifort",): + meson_exclude = ("mf2000", "mf2005", "swtv4", "mflgr") +else: + meson_exclude = [] +targets_meson = [t for t in targets if t not in meson_exclude] + + +def build_with_makefile(target, path, fc): + success = True + with set_dir(path): + if os.path.isfile("makefile"): + # wait to delete on windows + if sys.platform.lower() == "win32": + time.sleep(6) + + # clean prior to make + print(f"clean {target} with makefile") + os.system("make clean") + + # build MODFLOW-NWT with makefile + print(f"build {target} with makefile") + return_code = os.system("make") + + # test if running on Windows with ifort, if True the makefile + # should fail + errmsg = f"{target} created by makefile does not exist." + if sys.platform.lower() == "win32" and fc == "ifort": + if return_code != 0: + success = True + else: + success = False + # verify that MODFLOW-NWT was made + else: + success = os.path.isfile(target) + else: + errmsg = "makefile does not exist" + + return success, errmsg + + +@pytest.mark.base +@flaky(max_runs=RERUNS) +@pytest.mark.parametrize("target", targets) +def test_build(function_tmpdir, target: str) -> None: + with set_dir(function_tmpdir): + assert ( + pymake.build_apps( + target, + verbose=True, + clean=False, + ) + == 0 + ), f"could not compile {target}" + + +@pytest.mark.base +@flaky(max_runs=RERUNS) +@pytest.mark.parametrize("target", targets_meson) +def test_meson_build(function_tmpdir, target: str) -> None: + with set_dir(function_tmpdir): + assert ( + pymake.build_apps( + target, + verbose=True, + clean=False, + meson=True, + ) + == 0 + ), f"could not compile {target}" + + +@pytest.mark.base +@flaky(max_runs=RERUNS) +@pytest.mark.skipif(sys.platform == "win32", reason="do not run on Windows") +@pytest.mark.parametrize("target", targets_make) +def test_makefile_build(function_tmpdir, target: str) -> None: + pm = pymake.Pymake(verbose=True) + pm.target = target + pm.makefile = True + pm.makefiledir = "." + pm.inplace = True + pm.dryrun = True + pm.makeclean = False + + with set_dir(function_tmpdir): + pm.download_target(target) + assert pm.download, f"could not download {target} distribution" + assert pm.build() == 0, f"could not compile {target}" + + success, errmsg = build_with_makefile(target, function_tmpdir, pm.fc) + assert success, errmsg diff --git a/autotest/test_cli_cmds.py b/autotest/test_cli_cmds.py index 6413831a..b82aaf44 100644 --- a/autotest/test_cli_cmds.py +++ b/autotest/test_cli_cmds.py @@ -1,8 +1,10 @@ import os +import pathlib as pl import subprocess import pytest from flaky import flaky +from modflow_devtools.misc import set_dir RERUNS = 3 @@ -11,6 +13,11 @@ "crt", ) +meson_parm = ( + True, + False, +) + def run_cli_cmd(cmd: list) -> None: process = subprocess.Popen( @@ -35,12 +42,12 @@ def run_cli_cmd(cmd: list) -> None: @pytest.mark.dependency(name="make_program") @pytest.mark.base @pytest.mark.parametrize("target", targets) -def test_make_program(module_tmpdir, target: str) -> None: +def test_make_program(function_tmpdir, target: str) -> None: cmd = [ "make-program", target, "--appdir", - str(module_tmpdir), + str(function_tmpdir), "--verbose", ] run_cli_cmd(cmd) @@ -60,33 +67,36 @@ def test_make_program_all(module_tmpdir) -> None: run_cli_cmd(cmd) +@flaky(max_runs=RERUNS) @pytest.mark.dependency(name="mfpymake") @pytest.mark.base -def test_mfpymake(module_tmpdir) -> None: - src = ( - "program hello\n" - + " ! This is a comment line; it is ignored by the compiler\n" - + " print *, 'Hello, World!'\n" - + "end program hello\n" - ) - src_file = module_tmpdir / "mfpymake_src" / "hello.f90" - src_file.parent.mkdir(parents=True, exist_ok=True) - with open(src_file, "w") as f: - f.write(src) - cmd = [ - "mfpymake", - str(src_file.parent), - "hello", - "-mc", - "--verbose", - "--appdir", - str(module_tmpdir), - "-fc", - ] - if os.environ.get("FC") is None: - cmd.append("gfortran") - else: - cmd.append(os.environ.get("FC")) - run_cli_cmd(cmd) - cmd = [module_tmpdir / "hello"] - run_cli_cmd(cmd) +@pytest.mark.parametrize("meson", meson_parm) +def test_mfpymake(function_tmpdir, meson: bool) -> None: + with set_dir(function_tmpdir): + src = ( + "program hello\n" + + " ! This is a comment line; it is ignored by the compiler\n" + + " print *, 'Hello, World!'\n" + + "end program hello\n" + ) + src_file = pl.Path("src/hello.f90") + src_file.parent.mkdir(parents=True, exist_ok=True) + with open(src_file, "w") as f: + f.write(src) + cmd = [ + "mfpymake", + str(src_file.parent), + "hello", + # "-mc", + "--verbose", + "-fc", + ] + if os.environ.get("FC") is None: + cmd.append("gfortran") + else: + cmd.append(os.environ.get("FC")) + if meson: + cmd.append("--meson") + run_cli_cmd(cmd) + cmd = [function_tmpdir / "hello"] + run_cli_cmd(cmd) diff --git a/autotest/test_mf6.py b/autotest/test_mf6.py index 8c8a3185..5358f45a 100644 --- a/autotest/test_mf6.py +++ b/autotest/test_mf6.py @@ -1,8 +1,8 @@ import os import sys import time -from platform import system from pathlib import Path +from platform import system import flopy import pytest diff --git a/autotest/test_mfusg.py b/autotest/test_mfusg.py index 8a64a8f6..0f13c3e3 100644 --- a/autotest/test_mfusg.py +++ b/autotest/test_mfusg.py @@ -1,6 +1,6 @@ import os -from platform import system from pathlib import Path +from platform import system import flopy import pytest diff --git a/autotest/test_mp6.py b/autotest/test_mp6.py index ea0c4218..51c14522 100644 --- a/autotest/test_mp6.py +++ b/autotest/test_mp6.py @@ -1,7 +1,7 @@ import os import shutil -from platform import system from pathlib import Path +from platform import system import flopy import pytest diff --git a/autotest/test_mp7.py b/autotest/test_mp7.py index 5284fde3..24b1c669 100644 --- a/autotest/test_mp7.py +++ b/autotest/test_mp7.py @@ -1,14 +1,13 @@ import os import shutil -from platform import system from pathlib import Path +from platform import system import flopy import pytest import pymake - ext = ".exe" if system() == "Windows" else "" diff --git a/autotest/test_seawat.py b/autotest/test_seawat.py index 93ad96e5..1dfbd431 100644 --- a/autotest/test_seawat.py +++ b/autotest/test_seawat.py @@ -1,7 +1,7 @@ import os import sys -from platform import system from pathlib import Path +from platform import system import flopy import pytest diff --git a/pymake/cmds/build.py b/pymake/cmds/build.py index b909d8f9..7d2a7234 100755 --- a/pymake/cmds/build.py +++ b/pymake/cmds/build.py @@ -26,8 +26,17 @@ "zip", "keep", "dryrun", + "meson", +) +COM_ARG_KEYS = ( + "fc", + "cc", + "fflags", + "cflags", + "zip", + "keep", + "dryrun", ) -COM_ARG_KEYS = ("fc", "cc", "fflags", "cflags", "zip", "keep", "dryrun") def main() -> None: diff --git a/pymake/cmds/mfpymakecli.py b/pymake/cmds/mfpymakecli.py index 04d26527..3ec5fd0d 100755 --- a/pymake/cmds/mfpymakecli.py +++ b/pymake/cmds/mfpymakecli.py @@ -63,8 +63,8 @@ def main() -> None: verbose=args.verbose, inplace=args.inplace, networkx=args.networkx, - meson=args.mb, - mesondir=args.mesonbuild_dir, + meson=args.meson, + mesondir=args.mesondir, ) except (EOFError, KeyboardInterrupt): sys.exit(f" cancelling '{sys.argv[0]}'") diff --git a/pymake/pymake_base.py b/pymake/pymake_base.py index 986c81a2..71eb4450 100644 --- a/pymake/pymake_base.py +++ b/pymake/pymake_base.py @@ -184,7 +184,7 @@ def main( inplace = True print( f"Using meson to build {os.path.basename(target)}, " - + "ressetting inplace to True" + + "resetting inplace to True" ) if srcdir is not None and target is not None: diff --git a/pymake/pymake_build_apps.py b/pymake/pymake_build_apps.py index d9a4e923..6d822608 100644 --- a/pymake/pymake_build_apps.py +++ b/pymake/pymake_build_apps.py @@ -197,7 +197,7 @@ def build_apps( # set double precision flag and whether the executable name # can be modified - if target in ["swtv4"]: + if target in ("swtv4",): update_target_name = False else: update_target_name = True @@ -214,7 +214,7 @@ def build_apps( pmobj.inplace = True # set target and srcdir - pmobj.target = target + pmobj.target = target.replace("dev", "") pmobj.srcdir = os.path.join( download_dir, code_dict[target].dirname, code_dict[target].srcdir ) diff --git a/pymake/pymake_parser.py b/pymake/pymake_parser.py index db4d7e98..faeeb8f2 100644 --- a/pymake/pymake_parser.py +++ b/pymake/pymake_parser.py @@ -167,7 +167,7 @@ def _get_standard_arg_dict(): "action": "store_true", }, "makefiledir": { - "tag": ("-md", "--makefile-dir"), + "tag": ("-md", "--makefiledir"), "help": "GNU make makefile directory. (default is '.')", "default": ".", "choices": None, @@ -256,14 +256,14 @@ def _get_standard_arg_dict(): "action": "store_true", }, "meson": { - "tag": ("--mb", "--meson-build"), + "tag": ("--meson",), "help": """Use meson to build executable. (default is False)""", "default": False, "choices": None, "action": "store_true", }, "mesondir": { - "tag": ("-mbd", "--mesonbuild-dir"), + "tag": ("--mesondir",), "help": "meson directory. (default is '.')", "default": ".", "choices": None, diff --git a/pymake/utils/_compiler_switches.py b/pymake/utils/_compiler_switches.py index ad96ae92..d186892e 100644 --- a/pymake/utils/_compiler_switches.py +++ b/pymake/utils/_compiler_switches.py @@ -308,6 +308,23 @@ def _get_fortran_flags( flags.append("ffpe-summary=overflow") if _check_gnu_switch_available("-ffpe-trap"): flags.append("ffpe-trap=overflow,zero,invalid") + if target in ("mf6", "libmf6", "zbud6"): + if _check_gnu_switch_available("-fall-intrinsics"): + flags.append("fall-intrinsics") + if _check_gnu_switch_available("-pedantic"): + flags.append("pedantic") + if _check_gnu_switch_available("-Wcharacter-truncation"): + flags.append("Wcharacter-truncation") + if _check_gnu_switch_available( + "-Wno-unused-dummy-argument" + ): + flags.append("Wno-unused-dummy-argument") + if _check_gnu_switch_available("-Wno-intrinsic-shadow"): + flags.append("Wno-intrinsic-shadow") + if _check_gnu_switch_available("-Wno-maybe-uninitialized"): + flags.append("Wno-maybe-uninitialized") + if _check_gnu_switch_available("-Wno-uninitialized"): + flags.append("Wno-uninitialized") if double: flags += ["fdefault-real-8", "fdefault-double-8"] # define the OS macro for gfortran @@ -316,7 +333,15 @@ def _get_fortran_flags( flags.append(os_macro) elif fc in ["ifort", "mpiifort"]: if osname == "win32": - flags += ["heap-arrays:0", "fpe:0", "traceback", "nologo"] + flags += [ + "heap-arrays:0", + "fpe:0", + "traceback", + "nologo", + "Qdiag-disable:7416", + "Qdiag-disable:7025", + "Qdiag-disable:5268", + ] if debug: flags += ["debug:full", "Zi"] if double: @@ -329,7 +354,14 @@ def _get_fortran_flags( flags.remove("fPIC") if debug: flags += ["g"] - flags += ["no-heap-arrays", "fpe0", "traceback"] + flags += [ + "no-heap-arrays", + "fpe0", + "traceback", + "Qdiag-disable:7416", + "Qdiag-disable:7025", + "Qdiag-disable:5268", + ] if double: flags += ["r8", "autodouble"] elif fc in ("ftn",): diff --git a/pymake/utils/_meson_build.py b/pymake/utils/_meson_build.py index 447c36ab..0a18d455 100644 --- a/pymake/utils/_meson_build.py +++ b/pymake/utils/_meson_build.py @@ -16,12 +16,8 @@ _get_osname, _get_prepend, ) -from ._file_utils import _get_extra_exclude_files, _get_extrafiles_common_path -from ._Popen_wrapper import ( - _process_Popen_command, - _process_Popen_communicate, - _process_Popen_initialize, -) +from ._file_utils import _get_extrafiles_common_path +from .usgsprograms import usgs_program_data @contextmanager @@ -40,6 +36,7 @@ def _set_directory(path: Path): """ origin = Path().absolute() try: + Path(path).mkdir(exist_ok=True, parents=True) os.chdir(path) yield finally: @@ -77,8 +74,8 @@ def meson_build( return code """ - meson_test_path = os.path.join(mesondir, "meson.build") - if os.path.isfile(meson_test_path): + meson_test_path = Path(mesondir) / "meson.build" + if meson_test_path.is_file(): # setup meson returncode = meson_setup(mesondir, fc=fc, cc=cc, appdir=appdir) # build and install executable(s) using meson @@ -197,6 +194,7 @@ def meson_setup( os.path.abspath(appdir), os.path.abspath(mesondir) ) command_list.append(f"--libdir={libdir}") + command_list.append(f"--bindir={libdir}") if os.path.isdir(build_dir): command_list.append("--wipe") @@ -208,7 +206,7 @@ def meson_setup( # evaluate return code if returncode != 0: - print(f"meson install failed on '{' '.join(command)}'") + print(f"meson setup failed on '{command}'") return returncode @@ -243,7 +241,7 @@ def meson_install( # evaluate return code if returncode != 0: - print(f"meson setup failed on '{' '.join(command)}'") + print(f"meson install failed on '{command}'") return returncode @@ -436,6 +434,13 @@ def _create_main_meson_build( """ appdir = os.path.relpath(os.path.dirname(target), mesondir) target = os.path.splitext((os.path.basename(target)))[0] + osname = _get_osname() + + # get target version number + try: + target_version = usgs_program_data.get_version(target) + except: + target_version = None # get main program file from list of source files mainfile = _get_main(srcfiles) @@ -488,7 +493,11 @@ def _create_main_meson_build( sharedobject=sharedobject, verbose=verbose, ) - preprocess = _preprocess_file(srcfiles, meson=True) + if osname == "win32" and fc in ("ifort",): + meson_ext_flag = False + else: + meson_ext_flag = True + preprocess = _preprocess_file(srcfiles, meson=meson_ext_flag) if preprocess: if fc == "gfortran": fflags_meson.append("-cpp") @@ -524,14 +533,25 @@ def _create_main_meson_build( ) optlevel_int = int(optlevel.replace("-O", "").replace("/O", "")) - main_meson_file = os.path.join(mesondir, "meson.build") + main_meson_file = Path(mesondir) / "meson.build" + if verbose: + print(f"Creating main meson.build file {main_meson_file}") with open(main_meson_file, "w") as f: line = f"project(\n\t'{target}',\n" for language in languages: line += f"\t'{language}',\n" - line += "\tmeson_version: '>= 0.59.0',\n" + if target_version is not None: + line += f"\tversion: '{target_version}',\n" + line += "\tmeson_version: '>= 1.1.0',\n" line += "\tdefault_options: [\n\t\t'b_vscrt=static_from_buildtype',\n" - line += f"\t\t'optimization={optlevel_int}'\n" + line += f"\t\t'optimization={optlevel_int}',\n" + line += "\t\t'debug=" + if debug: + line += "true',\n" + else: + line += "false',\n" + if target in ("mf6", "libmf6", "zbud6"): + line += "\t\t'fortran_std=f2008'\n" line += "\t])\n\n" f.write(line) @@ -598,7 +618,7 @@ def _create_main_meson_build( # get list of include directories include_text = "" - if "cpp" in languages: + if "cpp" in languages or "c" in languages: include_dirs = [] for key, value in source_path_dict.items(): for root, dirs, files in os.walk(value): @@ -616,10 +636,17 @@ def _create_main_meson_build( f.write(line) # add build command - line = ( - f"executable('{target}', sources{include_text}" - + f", install: true, install_dir: '{appdir}')\n\n" - ) + if sharedobject: + line = ( + f"library('{target}', sources{include_text}" + + ", install: true, name_prefix: '', " + + f"install_dir: '{appdir}')\n\n" + ) + else: + line = ( + f"executable('{target}', sources{include_text}" + + f", install: true, install_dir: '{appdir}')\n\n" + ) f.write(line) return main_meson_file, fc_meson, cc_meson diff --git a/pymake/utils/usgsprograms.txt b/pymake/utils/usgsprograms.txt index 698b861c..9f147906 100644 --- a/pymake/utils/usgsprograms.txt +++ b/pymake/utils/usgsprograms.txt @@ -11,13 +11,16 @@ gridgen , 1.0.02 , True , https://water.usgs.gov/water-resources/software crt , 1.3.1 , True , https://water.usgs.gov/ogw/CRT/CRT_1.3.1.zip , CRT_1.3.1 , SOURCE , True , False , False sutra , 3.0 , True , https://water.usgs.gov/water-resources/software/sutra/SUTRA_3_0_0.zip , SutraSuite , SUTRA_3_0/source , True , False , False mf2000 , 1.19.01, True , https://water.usgs.gov/nrp/gwsoftware/modflow2000/mf2k1_19_01.tar.gz , mf2k.1_19 , src , True , False , False -mf2005 , 1.12.00, True , https://github.com/MODFLOW-USGS/mf2005/releases/download/v.1.12.00/MF2005.1_12u.zip , MF2005.1_12u , src , True , True , False -mfusg , 1.5 , True , https://water.usgs.gov/water-resources/software/MODFLOW-USG/mfusg1_5.zip , mfusg1_5 , src , True , True , False +mf2005 , 1.12.00, True , https://github.com/MODFLOW-USGS/mf2005/releases/download/v.1.12.00/MF2005.1_12u.zip , MF2005.1_12u , src , True , False , False +mfusg , 1.5 , True , https://water.usgs.gov/water-resources/software/MODFLOW-USG/mfusg1_5.zip , mfusg1_5 , src , True , False , False zonbudusg , 1.5 , True , https://water.usgs.gov/water-resources/software/MODFLOW-USG/mfusg1_5.zip , mfusg1_5 , src/zonebudusg , True , False , False swtv4 , 4.00.05, True , https://water.usgs.gov/water-resources/software/SEAWAT/swt_v4_00_05.zip , swt_v4_00_05 , source , False , True , False mp6 , 6.0.1 , True , https://water.usgs.gov/water-resources/software/MODPATH/modpath.6_0_01.zip , modpath.6_0 , src , True , False , False -mflgr , 2.0.0 , True , https://water.usgs.gov/ogw/modflow-lgr/modflow-lgr-v2.0.0/mflgrv2_0_00.zip , mflgr.2_0 , src , True , True , False +mflgr , 2.0.0 , True , https://water.usgs.gov/ogw/modflow-lgr/modflow-lgr-v2.0.0/mflgrv2_0_00.zip , mflgr.2_0 , src , True , False , False zonbud3 , 3.01 , True , https://water.usgs.gov/water-resources/software/ZONEBUDGET/zonbud3_01.exe , Zonbud.3_01 , Src , True , False , False mfnwt1.1.4 , 1.1.4 , False , https://water.usgs.gov/water-resources/software/MODFLOW-NWT/MODFLOW-NWT_1.1.4.zip , MODFLOW-NWT_1.1.4 , src , True , False , False -mfnwt , 1.3.0 , True , https://water.usgs.gov/water-resources/software/MODFLOW-NWT/MODFLOW-NWT_1.3.0.zip , MODFLOW-NWT , src , True , True , False +mfnwt , 1.3.0 , True , https://water.usgs.gov/water-resources/software/MODFLOW-NWT/MODFLOW-NWT_1.3.0.zip , MODFLOW-NWT , src , True , False , False mfusg_gsi , 2.1.1 , True , https://www.gsienv.com/wp-content/uploads/2023/04/USG-Transport-V_2.1.1.zip , USGT-v2-1-1-source , . , True , False , False +mf6dev , 6.5.0.dev0 , False , https://github.com/MODFLOW-USGS/modflow6/archive/refs/heads/develop.zip , modflow6-develop , src , True , False , False +zbud6dev , 6.5.0.dev0 , False , https://github.com/MODFLOW-USGS/modflow6/archive/refs/heads/develop.zip , modflow6-develop , utils/zonebudget/src, True , False , False +libmf6dev , 6.5.0.dev0 , False , https://github.com/MODFLOW-USGS/modflow6/archive/refs/heads/develop.zip , modflow6-develop , srcbmi , True , False , True \ No newline at end of file