Skip to content

Commit

Permalink
Merge pull request #37 from j-bryan/package_build
Browse files Browse the repository at this point in the history
Create easily installable packages of FORCE tools
  • Loading branch information
dylanjm authored Oct 17, 2024
2 parents e555bc0 + 6ef0998 commit 6ef7bd9
Show file tree
Hide file tree
Showing 34 changed files with 1,914 additions and 1 deletion.
10 changes: 9 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,19 @@ write_inner.py
.ravenStatus
romMeta.xml

# Ignore built executables
build
force_install
*.app

# Ignore certain GUI files
.forceuirc

# Allow these files
!FORCE_logo-bw.png
!FORCE_logo-color.png


# python stuff
__pycache__
*.pyc
*.pyc
72 changes: 72 additions & 0 deletions package/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# FORCE One-Step Installer Creation
FORCE one-step installers are created using the `cx_Freeze` python package after creating a python environment using either `venv` or `conda`.
A computer with the same operating system and architecture as the target operating system must be used to generate an installer, i.e. use a Windows machine to generate a Windows installer, a Mac (Intel) to generate a Mac installer, etc.
Note that installers generated with Apple computers with M-series chips will not be backwards compatible with Intel-based Apple computers.

Windows and macOS are the only operating systems currently supported.
Linux users are encouraged to use pip-installed or source built versions of the RAVEN, HERON, and TEAL software packages.

## 1. Build FORCE executables
Create a conda environment `force_build_310`, install the RAVEN, HERON, and TEAL pip packages, and build the FORCE executables using the script `build_force.sh`.
The path to the conda executable must be provided using the `--conda-defs` argument.
```console
./build_force.sh --conda-defs <path to conda directory>/etc/profile.d/conda.sh
```

## 2. Add IPOPT to build directory (Windows only)
Download the IPOPT Windows binary:
https://github.com/coin-or/Ipopt/releases

Extract the downloaded zip directory and copy its contents to the raven_install directory, ensuring to replace the version numbers of IPOPT as needed.
```console
cd force_install
unzip ~/Downloads/Ipopt-3.14.12-win64-msvs2019-md.zip
mv Ipopt-3.14.12-win64-msvs2019-md local
cd ..
```

## 3. Copy examples and build/copy the RAVEN, HERON, and TEAL documentation
Adding examples and documentation to the one-step installer requires having the source installation present on the build machine, with the `raven_libraries` conda environment already created.
```console
conda activate raven_libraries
./copy_examples.sh --raven-dir /path/to/raven --heron-dir /path/to/HERON
cp -R examples force_install/examples
./make_docs.sh --raven-dir /path/to/raven --heron-dir /path/to/HERON --teal-dir /path/to/TEAL
cp -R docs force_install/docs
```
When running the `make_docs.sh` script, the optional `--no-build` flag may be added if the desired documentation PDFs have already been built, and you do not wish to rebuild the documents.
If using the `--no-build` option, there is no need to have the `raven_libraries` active.
```console
./make_docs.sh --no-build --raven-dir /path/to/raven --heron-dir /path/to/HERON --teal-dir /path/to/TEAL
cp -R docs force_install/docs
```

## 4. Get NEAMS Workbench installer
The installers for the NEAMS Workbench software can be found here:
https://code.ornl.gov/neams-workbench/downloads/-/tree/5.4.1?ref_type=heads

Download `Workbench-5.4.1.exe` for Windows and `Workbench-5.4.1.dmg` for macOS.
Place this file in the `force_install` directory on Windows or the current directory on macOS.

Windows:
```console
cp ~/Downloads/Workbench-5.4.1.exe force_install
```

macOS:
```console
cp ~/Downloads/Workbench-5.4.1.dmg .
```

## 5. Create the installer
### Windows
The Windows installer is created using Inno Setup.
Run the `inno_package.iss` script from the Inno Setup application.
The resulting .exe installer file can be found in the `inno_output` directory.

### macOS
Run the macOS build script
```console
./build_mac_app.sh
```
The disk image `FORCE.dmg` contains applications for both FORCE and Workbench.
42 changes: 42 additions & 0 deletions package/build_force.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/bin/bash

# Have users point to the location of their conda installation so we can properly activate the
# conda environment that is being made. Use the "--conda-defs" option to specify this path.
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
--conda-defs)
CONDA_DEFS="$2"
shift
shift
source $CONDA_DEFS
;;
*)
echo "Unknown option: $1"
exit 1
;;
esac
done

# Establish conda environment
conda create -n force_build_310 python=3.10 -y
conda activate force_build_310

# Check that the conda environment is active. If not, exit.
if [[ $CONDA_DEFAULT_ENV != "force_build_310" ]]; then
echo "Conda environment not activated. Maybe the path to the conda installation is incorrect?"
echo "Provided conda path: $CONDA_DEFS"
exit 1
fi

pip install cx_Freeze
pip install raven-framework heron-ravenframework teal-ravenframework
# If on macOS, use conda to install ipopt
if [[ "$OSTYPE" == "darwin"* ]]; then
# Note: The PyPI version of ipopt is not maintained and is severl major version
# behind the conda-forge distribution.
conda install -c conda-forge ipopt -y
fi

# Build the FORCE executables
python setup.py install_exe --install-dir force_install
67 changes: 67 additions & 0 deletions package/build_mac_app.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/bin/bash

# Set up the FORCE application bundle
# We'll set up the app so that some FORCE launcher script is the main executable, and the RAVEN,
# HERON, and TEAL executables are in the Resources directory.
# Build the initial app from the force_launcher.scpt AppleScript
osacompile -o FORCE.app force_launcher.scpt
# Now copy over the force_install directory contents to the application's Resources directory
cp -Rp force_install/* FORCE.app/Contents/Resources/
# Overwrite the app's icon with the FORCE icon
cp icons/FORCE.icns FORCE.app/Contents/Resources/applet.icns

# Create a new disk image
hdiutil create -size 5g -fs HFS+ -volname "FORCE" -o force_build.dmg

# Mount the new disk image
hdiutil attach force_build.dmg -mountpoint /Volumes/FORCE

# Mount the existing .dmg file file Workbench
hdiutil attach Workbench-5.4.1.dmg -mountpoint /Volumes/Workbench

# Add the workshop tests and data directories to FORCE so that Workbench's autocomplete works for workshop examples
mkdir FORCE.app/Contents/Resources/tests
mkdir FORCE.app/Contents/Resources/examples
cp -Rp examples/workshop FORCE.app/Contents/Resources/tests/
cp -Rp examples/data FORCE.app/Contents/Resources/examples/

# Move the FORCE app to the disk image
cp -Rp FORCE.app /Volumes/FORCE/
cp -Rp /Volumes/Workbench/Workbench-5.4.1.app /Volumes/FORCE/

# Move the "examples" and "docs" directories from the FORCE app bundle to the top level of the disk
# image to make them more accessible.
cp -Rp examples /Volumes/FORCE/
cp -Rp docs /Volumes/FORCE/

# Move the "examples" and "docs" directories from the FORCE app bundle to the top level of the disk
# image to make them more accessible.
if [ -d FORCE.app/Contents/Resources/examples ]; then
mv /Volumes/FORCE/FORCE.app/Contents/Resources/examples /Volumes/FORCE/
else
echo "WARNING: No examples directory found in FORCE.app bundle"
fi
if [ -d FORCE.app/Contents/Resources/docs ]; then
mv FORCE.app/Contents/Resources/docs /Volumes/FORCE/
else
echo "WARNING: No docs directory found in FORCE.app bundle"
fi

# Add .son file to Workbench app to provide a default HERON configuration
cp default.apps.son /Volumes/FORCE/Workbench-5.4.1.app/Contents/

# Create a symlink to the Applications directory in the app's build directory
ln -s /Applications /Volumes/FORCE/Applications

# Unmount all the disk images
hdiutil detach /Volumes/Workbench
hdiutil detach /Volumes/FORCE

# Convert to read-only compressed image
if [ -f FORCE.dmg ]; then
rm FORCE.dmg
fi
hdiutil convert force_build.dmg -format UDZO -o FORCE.dmg

# Remove the temporary disk image
rm force_build.dmg
79 changes: 79 additions & 0 deletions package/copy_examples.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/bin/bash
# Copies examples from the test directories of RAVEN and HERON to provide examples for users using
# the standalone install version of FORCE.

# Get the RAVEN and HERON locations as arguments "--raven-dir" and "--heron-dir"
# The destination directory is "examples" in the current directory but may be changed with the
# "--dest" argument.
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
EXAMPLES_DIR="$SCRIPT_DIR/examples"

while [[ $# -gt 0 ]]
do
key="$1"
case $key in
--raven-dir)
RAVEN_DIR="$2"
shift
shift
;;
--heron-dir)
HERON_DIR="$2"
shift
shift
;;
--dest)
EXAMPLES_DIR="$2"
shift
shift
;;
*)
echo "Unknown option: $1"
exit 1
;;
esac
done

# The examples we want to copy are the RAVEN user_guide tests, the HERON workshop tests, and the
# HERON data directory which contains time series models for those workshop tests.
EXAMPLES=($RAVEN_DIR/tests/framework/user_guide $HERON_DIR/data $HERON_DIR/tests/workshop)
mkdir -p $EXAMPLES_DIR

for ex in ${EXAMPLES[@]}; do
cp -R "$ex" "$EXAMPLES_DIR"
done

# Clean up the copied examples, removing files and directories created when running the tests.
DIRS_TO_REMOVE=("__pycache__" "gold" "*_o")
for dirname in ${DIRS_TO_REMOVE[@]}; do
find $EXAMPLES_DIR -type d -name $dirname -exec rm -r {} \; 2>/dev/null
done
FILES_TO_REMOVE=("tests" "moped_input.xml" "outer.xml" "inner.xml" "cash.xml" "*.lib" "write_inner.py" "*.heron" "*.heron.xml")
for filename in ${FILES_TO_REMOVE[@]}; do
find $EXAMPLES_DIR -name $filename -exec rm {} \; 2>/dev/null
done

# If building on Mac, replace the %HERON_DATA% magic string with a relative path to the data
# directory. This is a little hacky but the %HERON_DATA% magic string doesn't look everywhere for
# the data directory. This is only an issue for the Mac standalone install. HERON will find the
# data directory correctly on Windows.
# DATA_DIR=$EXAMPLES_DIR/data
# if [[ "$OSTYPE" == "darwin"* ]]; then
# # Find all XML files recursively from the current directory
# find $EXAMPLES_DIR/workshop -type f -name "*.xml" | while read -r file; do
# # Check if the file contains the %HERON_DATA% magic string. If not, skip this file.
# grep -q "%HERON_DATA%" "$file" || continue

# # Get the directory of the current XML file
# FILE_DIR=$(dirname "$file")

# # Calculate the relative path from the XML file directory to the data directory
# echo "FILE_DIR: $FILE_DIR DATA_DIR: $DATA_DIR"
# RELATIVE_PATH=$(python -c "import os.path; print(os.path.relpath('$DATA_DIR', '$FILE_DIR'))")
# # RELATIVE_PATH=$(realpath -s --relative-to="$FILE_DIR" "$DATA_DIR")
# echo $RELATIVE_PATH

# # Use sed to replace %HERON_DATA% with the relative path to the data directory
# sed -i '' "s|%HERON_DATA%|$RELATIVE_PATH|g" "$file"
# done
# fi
13 changes: 13 additions & 0 deletions package/default.apps.son
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
applications {
HERON {
configurations {
default {
options {
shared {
Executable="/Applications/FORCE.app/Contents/Resources/heron"
}
}
}
}
}
}
28 changes: 28 additions & 0 deletions package/force_launcher.scpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
set options to {"HERON", "RAVEN", "TEAL", "Quit"}

set selectedOption to choose from list options with title "FORCE Launcher" with prompt "Which FORCE application would you like to use?" default items {"HERON"}

if selectedOption is false then
display dialog "No option selected. Exiting..." buttons {"OK"} default button "OK"
else
set selectedOption to item 1 of selectedOption
if selectedOption is "HERON" then
set filePathName to quoted form of "/Applications/FORCE.app/Contents/Resources/heron"
else if selectedOption is "RAVEN" then
set filePathName to quoted form of "/Applications/FORCE.app/Contents/Resources/raven_framework"
else if selectedOption is "TEAL" then
set filePathName to quoted form of "/Applications/FORCE.app/Contents/Resources/teal"
else if selectedOption is "Quit" then
display dialog "Exiting..." buttons {"OK"} default button "OK"
return
end if
-- do shell script filePathName
try
do shell script "test -e " & filePathName
-- If the test passes, the file exists and we proceed with the script
do shell script filePathName
on error
-- If the file doesn't exist, display an error message
display dialog "The file at " & filePathName & " does not exist." buttons {"OK"} default button "OK"
end try
end if
46 changes: 46 additions & 0 deletions package/heron.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env python
# Copyright 2017 Battelle Energy Alliance, LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import re
import sys
from HERON.src.main import main
from ui import run_from_gui
from utils import add_local_bin_to_path


if __name__ == '__main__':
# Adds the "local/bin" directory to the system path in order to find ipopt and other executables
add_local_bin_to_path()

# Parse the command line arguments
import argparse
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
parser = argparse.ArgumentParser(description='HERON')
parser.add_argument('-w', action='store_true', default=False, required=False, help='Run in the GUI')
parser.add_argument('--definition', action="store_true", dest="definition", help='HERON input file definition compatible with the NEAMS Workbench')
parser.add_argument('input', nargs='?', help='HERON input file')
args, unknown = parser.parse_known_args()

# if the input file is not an xml file, assume it's an unknown argument
if args.input and not args.input.endswith('.xml'):
unknown.insert(0, args.input)
args.input = None
# remove the -w argument from sys.argv so it doesn't interfere with HERON's argument parsing
if args.w:
sys.argv.remove('-w')

if (args.w or not args.input) and not args.definition: # if asked to or if no file is passed, run the GUI
run_from_gui(main)
else:
main()
Loading

0 comments on commit 6ef7bd9

Please sign in to comment.