diff --git a/package/build_force b/package/build_force index 88f20949..d37806a1 100755 --- a/package/build_force +++ b/package/build_force @@ -57,7 +57,7 @@ function find_raven { # Function to find RAVEN plugins given a list of possible locations function find_plugin { for loc in ${LOCATIONS[@]} $RAVEN_LOC; do - for dir in $(find $loc/plugins -maxdepth 1 -type d -iname $1); do + for dir in $(find $loc -maxdepth 3 -type d -iname $1 2> /dev/null); do echo $(realpath $dir) return done @@ -125,7 +125,10 @@ echo " ... RAVEN location: $RAVEN_LOC" echo " ... HERON location: $HERON_LOC" echo " ... TEAL location: $TEAL_LOC" echo " ... Destination: $SCRIPT_DIR/force_install/docs" -sh $SCRIPT_DIR/make_docs --raven-dir $RAVEN_LOC --heron-dir $HERON_LOC --teal-dir $TEAL_LOC --dest $SCRIPT_DIR/force_install/docs +# The --no-build flag is used to avoid building the documentation. This is because the documentation +# requires activatation of the raven_libraries conda environment, otherwise the HERON documentation +# build will fail. Rebuild the documentation before building the FORCE package! +sh $SCRIPT_DIR/make_docs --raven-dir $RAVEN_LOC --heron-dir $HERON_LOC --teal-dir $TEAL_LOC --dest $SCRIPT_DIR/force_install/docs --no-build # Copy over relevant examples echo "Copying over the FORCE examples" diff --git a/package/copy_examples b/package/copy_examples index 92275713..7a7572e8 100755 --- a/package/copy_examples +++ b/package/copy_examples @@ -44,13 +44,13 @@ for ex in ${EXAMPLES[@]}; do done # Clean up the copied examples, removing files and directories created when running the tests. -FILES_TO_REMOVE=("tests" "moped_input.xml" "outer.xml" "inner.xml" "cashflow.xml") +FILES_TO_REMOVE=("tests" "moped_input.xml" "outer.xml" "inner.xml" "cash.xml" "*.lib") DIRS_TO_REMOVE=("__pycache__" "gold" "*_o") for filename in ${FILES_TO_REMOVE[@]}; do - find $EXAMPLES_DIR -name $filename -exec rm {} \; + find $EXAMPLES_DIR -name $filename -exec rm {} \; 2>/dev/null done for dirname in ${DIRS_TO_REMOVE[@]}; do - find $EXAMPLES_DIR -type d -name $dirname -exec rm -r {} \; + find $EXAMPLES_DIR -type d -name $dirname -exec rm -r {} \; 2>/dev/null done # If building on Mac, replace the %HERON_DATA% magic string with a relative path to the data @@ -84,12 +84,25 @@ if [[ "$OSTYPE" == "darwin"* ]]; then WORKBENCH_APP=$(find /Applications -type d -name "Workbench-*.app" | head -n 1) XML2EDDI=$(realpath $WORKBENCH_APP/Contents/rte/util/xml2eddi.py) else - # Use the readlink command to get the full path to the Workbench executable - WORKBENCH_BIN=$(dirname $(readlink -f $(which Workbench))) - XML2EDDI=$(realpath $WORKBENCH_BIN/../rte/util/xml2eddi.py) + if ! command -v Workbench &> /dev/null; then + # If Workbench isn't in the system PATH. Look in typical installation locations for Workbench to + # find the xml2eddi.py script. Common locatoins are /c/Workbench-5.4.1/ and $HOME/Workbench-5.4.1/. + if [ -d "/c/Workbench-5.4.1" ]; then + XML2EDDI="/c/Workbench-5.4.1/rte/util/xml2eddi.py" + elif [ -d "$HOME/Workbench-5.4.1" ]; then + XML2EDDI="$HOME/Workbench-5.4.1/rte/util/xml2eddi.py" + else + XML2EDDI="" # Workbench not found + fi + else + # Workbench is in the system PATH. Use the location of the Workbench executable to find the xml2eddi.py script. + XML2EDDI=$(realpath $(command -v Workbench)/../rte/util/xml2eddi.py) + fi fi -if [ -x "$XML2EDDI" ]; then + +# If XML2EDDI is not empty, convert the HERON workshop tests to .heron files +if [ -n "$XML2EDDI" ]; then for ex in $(find $EXAMPLES_DIR/workshop -name "heron_input*.xml"); do - python $XML2EDDI $ex + python $XML2EDDI $ex > ${ex%.xml}.heron done fi diff --git a/package/inno_package.iss b/package/inno_package.iss index d699692d..bc73c800 100644 --- a/package/inno_package.iss +++ b/package/inno_package.iss @@ -51,6 +51,9 @@ Name: "{autoprograms}\FORCE\examples"; Filename: "{app}\examples" Name: "{autodesktop}\HERON"; Filename: "{app}\heron.exe"; Tasks: desktopicon Name: "{autodesktop}\RAVEN"; Filename: "{app}\raven_framework.exe"; Tasks: desktopicon Name: "{autodesktop}\TEAL"; Filename: "{app}\teal.exe"; Tasks: desktopicon +; Add desktop icons for the documentation and examples directories +Name: "{autodesktop}\FORCE Documentation"; Filename: "{app}\docs"; Tasks: desktopicon +Name: "{autodesktop}\FORCE Examples"; Filename: "{app}\examples"; Tasks: desktopicon [Registry] ; File association for .heron files @@ -95,6 +98,7 @@ procedure CurStepChanged(CurStep: TSetupStep); var DefaultAppsFilePath: string; DefaultAppsContent: string; + WorkbenchConfigPath: string; ResultCode: Integer; begin if (CurStep = ssPostInstall) and (WorkbenchPath <> '') then @@ -108,7 +112,7 @@ begin // Associate .heron files with the Workbench executable RegWriteStringValue(HKEY_CURRENT_USER, 'Software\Classes\FORCE.heron\shell\open\command', '', '"' + WorkbenchPath + '" "%1"'); - DefaultAppsFilePath := ExtractFilePath(WorkbenchPath) + 'default.apps.json'; + DefaultAppsFilePath := ExtractFilePath(WorkbenchPath) + 'default.apps.son'; DefaultAppsContent := 'applications {' + #13#10 + ' HERON {' + #13#10 + @@ -124,9 +128,16 @@ begin ' }' + #13#10 + ' }'; + // Save the default.apps.son file in the Workbench base directory if not SaveStringToFile(DefaultAppsFilePath, DefaultAppsContent, False) then begin - MsgBox('Failed to create default.apps.json in the Workbench directory.', mbError, MB_OK); + MsgBox('Failed to create default.apps.son in the Workbench directory.', mbError, MB_OK); + end; + + // Save the path to the Workbench executable in a file at {app}/.workbench. + if not SaveStringToFile(ExpandConstant('{app}') + '\.workbench', WorkbenchPath, False) then + begin + MsgBox('Failed to save the path to the Workbench executable.', mbError, MB_OK); end; end; end; diff --git a/package/make_docs b/package/make_docs index 34ef836b..b4a6d336 100755 --- a/package/make_docs +++ b/package/make_docs @@ -74,16 +74,19 @@ echo "FORCE documentation directory: $DOC_DIR" mkdir -p "$DOC_DIR" # Build the documentation for the FORCE tools -for loc in RAVEN_DIR HERON_DIR TEAL_DIR; do +# for loc in RAVEN_DIR HERON_DIR TEAL_DIR; do +for loc in HERON_DIR TEAL_DIR; do pushd "${!loc}/doc" > /dev/null + echo $(pwd) # If the build flag is set, build the documentation. if [ $NO_BUILD -eq 0 ]; then echo "Building documentation for $(basename ${!loc})" - # Either a Makefile or a make_docs.sh script should be present in the doc directory - if [ -f Makefile ]; then + if [[ -f "Makefile" ]] && command -v "make" >/dev/null 2>&1; then make - elif [ -f make_docs.sh ]; then + elif [[ -f "make_docs.bat" ]] && [[ $OSTYPE == "msys" ]]; then + ./make_docs.bat + elif [[ -f "make_docs.sh" ]]; then bash make_docs.sh else echo "ERROR: No Makefile or make_docs.sh script found in $(basename ${!loc}) doc directory." diff --git a/package/ui/controllers/file_selection.py b/package/ui/controllers/file_selection.py index 6f026b89..f44d12d1 100644 --- a/package/ui/controllers/file_selection.py +++ b/package/ui/controllers/file_selection.py @@ -1,8 +1,6 @@ from typing import Optional import os -import tkinter as tk import argparse -from collections import namedtuple from .file_location_persistence import FileLocationPersistence from .file_dialog import FileDialogController diff --git a/package/ui/models/main.py b/package/ui/models/main.py index 29ba0976..c68f6842 100644 --- a/package/ui/models/main.py +++ b/package/ui/models/main.py @@ -2,6 +2,7 @@ from typing import Callable + class Model: """ Runs a function in a separate thread """ def __init__(self, func: Callable, **kwargs): diff --git a/package/ui/utils.py b/package/ui/utils.py index 2e5a8e5f..8f516c81 100644 --- a/package/ui/utils.py +++ b/package/ui/utils.py @@ -1,67 +1,152 @@ -import xml.etree.ElementTree as ET import os +import pathlib import subprocess import platform import shutil +import tkinter as tk +from tkinter import messagebox, filedialog -def check_parallel(path) -> bool: +def find_workbench() -> pathlib.Path: """ - Checks if a ravenframework input file uses parallel processing. This poses a problem for the executable - on MacOS and possibly Linux. - - @In, path, str, path to the input file - @Out, is_parallel, bool, True if parallel processing is used, False otherwise + Finds the NEAMS Workbench executable. A ".workbench" file in the FORCE app's main directory tracks + the location of the Workbench executable. If that file doesn't exist, we look in common directories + to find Workbench ourselves. If we still can't find it, we ask the user to locate it manually, if + desired. """ - is_parallel = False - - tree = ET.parse(path) - tree.find() - - return is_parallel + workbench_path = None + # Check if a ".workbench" file exists in the main build directory (same directory as the heron and + # raven_framework executables). That should be 2 directories up from the directory of this file. + current_file_dir = pathlib.Path(__file__).parent + workbench_file = current_file_dir.parent.parent / ".workbench" + workbench_file_exists = workbench_file.exists() + if workbench_file_exists: + with open(workbench_file, 'r') as f: + workbench_path = f.read().strip() + if not os.path.exists(workbench_path): + # If the path in the .workbench file is invalid, delete the file so we don't keep trying + # to use it. + workbench_path = None + os.remove(workbench_file) + else: + return workbench_path -def find_workbench(): - """ - Finds the NEAMS Workbench executable - """ - workbench_path = None + # If that .workbench file doesn't exist, we can look around for the Workbench executable if platform.system() == "Windows": - if os.environ.get('WORKBENCH_PATH'): - workbench_path = os.environ['WORKBENCH_PATH'] + if wb_path := os.environ.get('WORKBENCH_PATH', None): + workbench_path = wb_path + elif wb_path := shutil.which('Workbench'): + workbench_path = wb_path else: - workbench_path = shutil.which('workbench') + # Manually search through a few common directories for the Workbench installation + for path in ["$HOMEDRIVE\\", "$PROGRAMFILES", "$HOME", "$APPDATA", "$LOCALAPPDATA"]: + path = os.path.expandvars(path) + if not os.path.exists(path): + continue + for file in os.listdir(path): + if file.startswith("Workbench"): + wb_path = os.path.join(path, file, "bin", "Workbench.exe") + if os.path.exists(wb_path): + workbench_path = wb_path + break elif platform.system() == "Darwin": # Look in the /Applications directory for a directory starting with "Workbench" for app in os.listdir("/Applications"): if app.startswith("Workbench") and os.path: workbench_path = os.path.join("/Applications", app, "Contents/MacOS/Workbench") break - else: - print("ERROR: Could not find the NEAMS Workbench application in the /Applications directory. " - "Has Workbench been installed?") - else: # Linux, not yet supported - print("Automatic connection of FORCE tools to the NEAMS Workbench is not yet supported on Linux.") + + # If we still haven't found Workbench, let the user help us out. Throw up a tkinter warning dialog to + # ask the user to locate the Workbench executable. + if workbench_path is None: + root = tk.Tk() + root.withdraw() + + response = messagebox.askyesno( + "NEAMS Workbench could not be found!", + "The NEAMS Workbench executable could not be found. Would you like to locate it manually?" + ) + if response: + workbench_path = filedialog.askopenfilename( + title="Locate NEAMS Workbench", + filetypes=[("Workbench Executable", "*.exe")] + ) + if workbench_path: + with open(workbench_file, 'w') as f: + f.write(workbench_path) + + if isinstance(workbench_path, str): + workbench_path = pathlib.Path(workbench_path) + return workbench_path +def create_workbench_heron_default(workbench_path: pathlib.Path): + """ + Creates a configuration file for Workbench so it knows where HERON is located. + @ In, workbench_path, pathlib.Path, the path to the NEAMS Workbench executable + """ + # First, we need to find the HERON executable. This will be "heron.exe" on Windows + # and just "heron" on MacOS and Linux. It should be located 2 directories up from the + # directory of this file. + current_file_dir = pathlib.Path(__file__).parent + heron_path = current_file_dir.parent.parent / "heron" + if platform.system() == "Windows": + heron_path = heron_path.with_suffix(".exe") + # If the HERON executable doesn't exist, we can't create the Workbench configuration file + if not heron_path.exists(): + print(f"ERROR: Could not find the HERON executable in the directory {heron_path.parent}.") + return + + # Create the configuration file for Workbench + workbench_root_dir = workbench_path.parent.parent + workbench_config_file = workbench_root_dir / "default.apps.son" + if workbench_config_file.exists(): + # File already exists, but does a HERON entry already exist? See if HERON mentioned in the + # file. This isn't as robust as actually parsing the file, but it should work for now. + with open(workbench_config_file, 'r') as f: + for line in f: + if "heron" in line.lower(): + return + # If the file doesn't exist or doesn't mention HERON, we need to add it + print("Adding HERON configuration to NEAMS Workbench", workbench_config_file) + with open(workbench_config_file, 'a') as f: + f.write("\napplications {\n" + " HERON {\n" + " configurations {\n" + " default {\n" + " options {\n" + " shared {\n" + f" \"Executable\"=\"{heron_path}\"\n" + " }\n" + " }\n" + " }\n" + " }\n" + " }\n" + "}\n") + + def run_in_workbench(file: str | None = None): """ Opens the given file in the NEAMS Workbench @ In, file, str, optional, the file to open in NEAMS Workbench """ - # Find the workbench executable + # Find the Workbench executable workbench_path = find_workbench() if workbench_path is None: print("ERROR: Could not find the NEAMS Workbench executable. Please set the " "WORKBENCH_PATH environment variable or add it to the system path.") raise RuntimeError - # Open the file in the workbench - command = workbench_path + # Point Workbench to the HERON executable if it's not already configured + create_workbench_heron_default(workbench_path) + + # Open the file in Workbench # Currently, we're only able to open the MacOS version of Workbench by opening the app itself. # This does not accept a file as an argument, so users will need to open Workbench, then open # the desired file manually from within the app. + command = str(workbench_path) if file is not None and platform.system() == "Windows": command += ' ' + file diff --git a/package/ui/views/file_selection.py b/package/ui/views/file_selection.py index 33c9c062..929637c9 100644 --- a/package/ui/views/file_selection.py +++ b/package/ui/views/file_selection.py @@ -52,10 +52,10 @@ def __init__(self, super().__init__(master) self.file_title = tk.Label(self, text=label) self.file_title.grid(row=0, column=0, columnspan=2, sticky='w') - self.browse_button = tk.Button(self, text='Browse') + self.browse_button = tk.Button(self, text='Browse', width=10, padx=5) self.browse_button.grid(row=1, column=0, sticky='w') self.filename = tk.StringVar() self.filename.set("Select a file") # Default filename is "Select a file", i.e. no file selected - self.filename_label = tk.Label(self, textvariable=self.filename) - self.filename_label.grid(row=1, column=1, sticky='w') + self.filename_label = tk.Label(self, textvariable=self.filename, bg="white", anchor='w', padx=10, pady=3) + self.filename_label.grid(row=1, column=1, sticky='w', padx=5) self.grid_columnconfigure(1, weight=1) diff --git a/package/ui/views/run_abort.py b/package/ui/views/run_abort.py index 2c61d5c9..2d1087a2 100644 --- a/package/ui/views/run_abort.py +++ b/package/ui/views/run_abort.py @@ -11,10 +11,12 @@ def __init__(self, master, **kwargs): @Out, None """ super().__init__(master, **kwargs) - self.abort_button = tk.Button(self, text='Abort') - self.abort_button.grid(row=0, column=0, sticky='w') + button_width = 10 + self.abort_button = tk.Button(self, text='Abort', width=button_width) + self.abort_button.grid(row=0, column=0, sticky='w', padx=5) - self.run_button = tk.Button(self, text='Run') - self.run_button.grid(row=0, column=1, sticky='w') + self.run_button = tk.Button(self, text='Run', width=button_width) + self.run_button.grid(row=0, column=1, sticky='w', padx=5) - self.grid_columnconfigure(1, weight=1) + self.grid_columnconfigure(0, minsize=50) + self.grid_columnconfigure(1, weight=1, minsize=50) diff --git a/package/ui/views/text_output.py b/package/ui/views/text_output.py index 3448b653..23977e37 100644 --- a/package/ui/views/text_output.py +++ b/package/ui/views/text_output.py @@ -12,10 +12,11 @@ def __init__(self, master, **kwargs): @Out, None """ super().__init__(master, **kwargs) - self.show_hide_button = tk.Button(self, text='Hide Ouptut') + self.show_hide_button = tk.Button(self, text='Hide Ouptut', pady=5, width=15) self.show_hide_button.grid(row=0, column=0, sticky='w') self.text = ScrolledText(self, state=tk.DISABLED) self.is_showing = True # To use with show/hide button self.text.grid(row=1, column=0, sticky='nsew') + self.grid_rowconfigure(0, minsize=50) self.grid_rowconfigure(1, weight=1) self.grid_columnconfigure(0, weight=1)