Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial Python App Packaging #10716

Merged
merged 19 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release_linux.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Releases
name: Linux Releases

on:
push:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release_mac.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Releases
name: Mac Releases

on:
push:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release_windows.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Releases
name: Windows Releases

on:
push:
Expand Down
9 changes: 8 additions & 1 deletion cmake/PythonCopyStandardLib.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
# this script must be called with two args:
# 1 - the path to the EnergyPlus executable in the install-tree, which is used to determine where to copy the library
# since this is in the install-tree, you'll need to use a cmake generator expression
# 2 - name of the folder to create to store the copied in python standard library, usually python_standard_library
# 2 - name of the folder to create to store the copied in python standard library, usually python_lib
import ctypes
import os
import platform
Expand Down Expand Up @@ -117,6 +117,13 @@ def find_libs(dir_path):
dll_dir = os.path.join(python_root_dir, 'DLLs')
shutil.copytree(dll_dir, target_dir, dirs_exist_ok=True)

# And also on Windows, we now need the grab the Tcl/Tk folder that contains config, scripts, and blobs
if platform.system() == 'Windows':
python_root_dir = os.path.dirname(standard_lib_dir)
tcl_dir = os.path.join(python_root_dir, 'tcl')
shutil.copytree(tcl_dir, target_dir, dirs_exist_ok=True)
# TODO: Need to do this on Mac as well, see the bottom of cmake/PythonFixUpOnMac.cmake for more info

# then I'm going to try to clean up any __pycache__ folders in the target dir to reduce installer size
for root, dirs, _ in os.walk(target_dir):
for this_dir in dirs:
Expand Down
17 changes: 17 additions & 0 deletions cmake/PythonFixUpOnMac.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,20 @@ foreach(PREREQ IN LISTS PREREQUISITES)
execute_process(COMMAND "install_name_tool" -change "${PREREQ}" "@loader_path/${LIB_INT_FILENAME}" "${LOCAL_PYTHON_LIBRARY}")
endif()
endforeach()

##############3
# we need to look into the tkinter binary's runtime dependencies, copy over the tcl and tk dylibs, update them with install_name_tool, and make sure they all get signed, like this:

# libtcl
# cp /opt/homebrew/opt/tcl-tk/lib/libtcl8.6.dylib /path/to/python_lib/lib-dynload/
# install_name_tool -change "/opt/homebrew/opt/tcl-tk/lib/libtcl8.6.dylib" "@loader_path/libtcl8.6.dylib" /path/to/python_lib/lib-dynload/_tkinter.cpython-312-darwin.so

# Do the same for libtk
# cp /opt/homebrew/opt/tcl-tk/lib/libtk8.6.dylib /path/to/python_lib/lib-dynload/
# install_name_tool -change "/opt/homebrew/opt/tcl-tk/lib/libtk8.6.dylib" "@loader_path/libtk8.6.dylib" /path/to/python_lib/lib-dynload/_tkinter.cpython-312-darwin.so

# Resign _tkinter
# codesign -vvvv -s "Developer ID Application: National ..." -f --timestamp -i "org.nrel.EnergyPlus" -o runtime /path/to/python_lib/lib-dynload/_tkinter.cpython-312-darwin.so
# codesign -vvvv -s "Developer ID Application: National ..." -f --timestamp -i "org.nrel.EnergyPlus" -o runtime /path/to/python_lib/lib-dynload/libtcl8.6.dylib
# codesign -vvvv -s "Developer ID Application: National ..." -f --timestamp -i "org.nrel.EnergyPlus" -o runtime /path/to/python_lib/lib-dynload/libtk8.6.dylib
##############3
6 changes: 3 additions & 3 deletions cmake/install_codesign_script.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Pre-conditions:

This script will codesign the ``FILES_TO_SIGN``, as well as the globbed copied Python .so and the root dylibs (such as ``libintl8.dylib``)

* ``python_standard_lib/lib-dynload/*.so``
* ``python_lib/lib-dynload/*.so``
* ``lib*.dylib``

To do so, it uses the `CodeSigning`_ functions :cmake:command:`codesign_files_macos`
Expand Down Expand Up @@ -113,11 +113,11 @@ foreach(path ${_all_root_dylibs})
endif()
endforeach()

file(GLOB PYTHON_SOS "${CMAKE_INSTALL_PREFIX}/python_standard_lib/lib-dynload/*.so")
file(GLOB PYTHON_SOS "${CMAKE_INSTALL_PREFIX}/python_lib/lib-dynload/*.so")

print_relative_paths(PREFIX "FULL_PATHS=" ABSOLUTE_PATHS ${FULL_PATHS})
print_relative_paths(PREFIX "ROOT_DYLIBS=" ABSOLUTE_PATHS ${ROOT_DYLIBS})
print_relative_paths(PREFIX "PYTHON_SOS, in ${CMAKE_INSTALL_PREFIX}/python_standard_lib/lib-dynload/=" ABSOLUTE_PATHS ${PYTHON_SOS} NAME_ONLY)
print_relative_paths(PREFIX "PYTHON_SOS, in ${CMAKE_INSTALL_PREFIX}/python_lib/lib-dynload/=" ABSOLUTE_PATHS ${PYTHON_SOS} NAME_ONLY)

include(${CMAKE_CURRENT_LIST_DIR}/CodeSigning.cmake)
codesign_files_macos(
Expand Down
11 changes: 11 additions & 0 deletions scripts/dev/create_shortcut.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
param (
[string]$TargetPath,
[string]$ShortcutPath,
[string]$Arguments
)

$WScriptShell = New-Object -ComObject WScript.Shell
$Shortcut = $WScriptShell.CreateShortcut($ShortcutPath)
$Shortcut.TargetPath = $TargetPath
$Shortcut.Arguments = $Arguments
$Shortcut.Save()
15 changes: 13 additions & 2 deletions src/EnergyPlus/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,8 @@ set(SRC
Pumps.hh
PurchasedAirManager.cc
PurchasedAirManager.hh
PythonEngine.cc
PythonEngine.hh
RefrigeratedCase.cc
RefrigeratedCase.hh
ReportCoilSelection.cc
Expand Down Expand Up @@ -965,15 +967,24 @@ if(LINK_WITH_PYTHON)
add_custom_command(
TARGET energyplusapi
POST_BUILD # TODO: I don't think we want to quote the generator expression
COMMAND ${Python_EXECUTABLE} "${PROJECT_SOURCE_DIR}/cmake/PythonCopyStandardLib.py" "$<TARGET_FILE:energyplusapi>" "python_standard_lib")
COMMAND ${Python_EXECUTABLE} "${PROJECT_SOURCE_DIR}/cmake/PythonCopyStandardLib.py" "$<TARGET_FILE:energyplusapi>" "python_lib"
COMMAND ${CMAKE_COMMAND} -E env --unset=PIP_REQUIRE_VIRTUALENV
${Python_EXECUTABLE} -m pip install --target="$<TARGET_FILE_DIR:energyplusapi>/python_lib" --upgrade energyplus-launch==3.7.2
)
endif()

if(BUILD_PACKAGE)
# if we are building package, we need to drop in some API/Plugin stuff
if(LINK_WITH_PYTHON)
# we'll want to grab the standard lib for python plugins
# TODO: I don't think we want to quote the generator expression
install(DIRECTORY "$<TARGET_FILE_DIR:energyplus>/python_standard_lib/" DESTINATION "./python_standard_lib")
install(DIRECTORY "$<TARGET_FILE_DIR:energyplus>/python_lib/" DESTINATION "./python_lib")
if(WIN32)
# on Windows, with Build Package, and also Link With Python, we can also drop in shortcuts to the new auxiliary CLI
# NOTE: The install command COMPONENTS should line up so that they are run at the same packaging step
install(CODE "execute_process(COMMAND powershell.exe -ExecutionPolicy Bypass -File ${PROJECT_SOURCE_DIR}/scripts/dev/create_shortcut.ps1 -TargetPath \"$<TARGET_FILE:energyplus>\" -ShortcutPath \"$<TARGET_FILE_DIR:energyplus>/EPLaunchPython.lnk\" -Arguments \"auxiliary eplaunch\")" COMPONENT Auxiliary)
install(FILES $<TARGET_FILE_DIR:energyplus>/EPLaunchPython.lnk DESTINATION "./" COMPONENT Auxiliary)
endif()
Comment on lines +982 to +987
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An interesting choice to call a powershell script to create it in the the installed package.

I suppose this could be helpful even in the tar.gz installer.

I would have expected a modification or an addition around here for the Start Menu though, no?

component.addOperation("CreateShortcut", "@TargetDir@/EP-Launch.exe", target_dir + "/EP-Launch.lnk");

Copy link
Contributor

@jmarrec jmarrec Sep 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, we may want an x-energyplus.xml and a eplaunch.desktop file for debian?

and adjusting or adding to

component.addElevatedOperation("RegisterFileType", "idf", `${targetDir}\\EP-Launch.exe %1`, "EnergyPlus Input Data File", "text/plain");
component.addElevatedOperation("RegisterFileType", "imf", `${targetDir}\\EP-Launch.exe %1`, "EnergyPlus Input Macro File", "text/plain");
component.addElevatedOperation("RegisterFileType", "epg", `${targetDir}\\EP-Launch.exe %1`, "EnergyPlus Group File", "text/plain");

x-energyplus.xml would probably be something like

<?xml version="1.0" encoding="UTF-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">

  <mime-type type="application/x-energyplus">
    <sub-class-of type="text/plain"/>
    <comment>EnergyPlus Input Data File</comment>
    <comment xml:lang="fr">Fichier d'entrées EnergyPlus</comment>
    <glob pattern="*.idf" />
  </mime-type>

 //// deal with imf and epg

</mime-info>

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, launchers for Mac/Linux would be nice, but I think we'll leave that for another day. Let's let the interested parties run it on Windows and get things polished up before throwing out too many launchers. :)

endif()
# we'll want to always provide the C API headers
install(FILES ${API_HEADERS} DESTINATION "./include/EnergyPlus/api")
Expand Down
77 changes: 59 additions & 18 deletions src/EnergyPlus/CommandLineInterface.cc
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@
#include <EnergyPlus/PluginManager.hh>
#include <EnergyPlus/UtilityRoutines.hh>

#if LINK_WITH_PYTHON
#include <EnergyPlus/PythonEngine.hh>
#endif

namespace EnergyPlus {

namespace CommandLineInterface {
Expand Down Expand Up @@ -230,6 +234,61 @@ Built on Platform: {}
// bool debugCLI = false;
app.add_flag("--debug-cli", debugCLI, "Print the result of the CLI assignments to the console and exit")->group(""); // Empty group to hide it

#if LINK_WITH_PYTHON
#if __APPLE__
// for now on Apple we are not providing the command line interface to EP-Launch due to packaging issues
// once that is fixed, this __APPLE__ block will be removed and we'll just have this on all platforms
#else
auto *auxiliaryToolsSubcommand = app.add_subcommand("auxiliary", "Run Auxiliary Python Tools");
auxiliaryToolsSubcommand->require_subcommand(); // should default to requiring 1 or more additional args?

std::vector<std::string> python_fwd_args;
auto *epLaunchSubCommand = auxiliaryToolsSubcommand->add_subcommand("eplaunch", "EnergyPlus Launch");
epLaunchSubCommand->add_option("args", python_fwd_args, "Extra Arguments forwarded to EnergyPlus Launch")->option_text("ARG ...");
epLaunchSubCommand->positionals_at_end(true);
epLaunchSubCommand->footer("You can pass extra arguments after the eplaunch keyword, they will be forwarded to EnergyPlus Launch.");

epLaunchSubCommand->callback([&state, &python_fwd_args] {
EnergyPlus::Python::PythonEngine engine(state);
// There's probably better to be done, like instantiating the pythonEngine with the argc/argv then calling PyRun_SimpleFile but whatever
std::string cmd = R"python(import sys
sys.argv.clear()
sys.argv.append("energyplus")
)python";
for (const auto &arg : python_fwd_args) {
cmd += fmt::format("sys.argv.append(\"{}\")\n", arg);
}

fs::path programDir = FileSystem::getParentDirectoryPath(FileSystem::getAbsolutePath(FileSystem::getProgramPath()));
fs::path const pathToPythonPackages = programDir / "python_lib";
std::string sPathToPythonPackages = std::string(pathToPythonPackages.string());
std::replace(sPathToPythonPackages.begin(), sPathToPythonPackages.end(), '\\', '/');
cmd += fmt::format("sys.path.insert(0, \"{}\")\n", sPathToPythonPackages);

std::string tclConfigDir = "";
for (auto &p : std::filesystem::directory_iterator(pathToPythonPackages)) {
if (p.is_directory()) {
std::string dirName = p.path().filename().string();
if (dirName.find("tcl", 0) == 0 && dirName.find(".", 0) > 0) {
tclConfigDir = dirName;
break;
}
}
}
cmd += "from os import environ\n";
cmd += fmt::format("environ[\'TCL_LIBRARY\'] = \"{}/{}\"\n", sPathToPythonPackages, tclConfigDir);

cmd += R"python(
from eplaunch.tk_runner import main_gui
main_gui()
)python";
// std::cout << "Trying to execute this python snippet: " << std::endl << cmd << std::endl;
engine.exec(cmd);
exit(0);
});
#endif
#endif

app.footer("Example: energyplus -w weather.epw -r input.idf");

const bool eplusRunningViaAPI = state.dataGlobal->eplusRunningViaAPI;
Expand Down Expand Up @@ -695,33 +754,16 @@ state.dataStrGlobals->inputFilePath='{:g}',
// Duplicate the kind of reading the Windows "GetINISetting" would
// do.

// REFERENCES:
// na

// Using/Aliasing
using namespace EnergyPlus;
using namespace DataStringGlobals;

// Locals
// SUBROUTINE ARGUMENT DEFINITIONS:

// SUBROUTINE PARAMETER DEFINITIONS:

// INTERFACE BLOCK SPECIFICATIONS
// na

// DERIVED TYPE DEFINITIONS
// na

// SUBROUTINE LOCAL VARIABLE DECLARATIONS:

std::string Param;
std::string::size_type ILB;
std::string::size_type IRB;
std::string::size_type IEQ;
std::string::size_type IPAR;
std::string::size_type IPOS;
std::string::size_type ILEN;

// Formats

Expand All @@ -731,7 +773,6 @@ state.dataStrGlobals->inputFilePath='{:g}',

Param = KindofParameter;
strip(Param);
ILEN = len(Param);
inputFile.rewind();
bool Found = false;
bool NewHeading = false;
Expand Down
17 changes: 15 additions & 2 deletions src/EnergyPlus/PluginManager.cc
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,19 @@
#include <nlohmann/json.hpp>

#if LINK_WITH_PYTHON

#ifdef _DEBUG
// We don't want to try to import a debug build of Python here
// so if we are building a Debug build of the C++ code, we need
// to undefine _DEBUG during the #include command for Python.h.
// Otherwise it will fail
#undef _DEBUG
#include <Python.h>
#define _DEBUG
#else
#include <Python.h>
#endif

#include <fmt/format.h>
template <> struct fmt::formatter<PyStatus>
{
Expand Down Expand Up @@ -467,7 +480,7 @@ PluginManager::PluginManager(EnergyPlusData &state) : eplusRunningViaPythonAPI(s
} else {
programDir = FileSystem::getParentDirectoryPath(FileSystem::getAbsolutePath(FileSystem::getProgramPath()));
}
fs::path const pathToPythonPackages = programDir / "python_standard_lib";
fs::path const pathToPythonPackages = programDir / "python_lib";

initPython(state, pathToPythonPackages);

Expand All @@ -478,7 +491,7 @@ PluginManager::PluginManager(EnergyPlusData &state) : eplusRunningViaPythonAPI(s
PyRun_SimpleString("import sys"); // allows us to report sys.path later

// we also need to set an extra import path to find some dynamic library loading stuff, again make it relative to the binary
addToPythonPath(state, programDir / "python_standard_lib/lib-dynload", false);
addToPythonPath(state, programDir / "python_lib/lib-dynload", false);

// now for additional paths:
// we'll always want to add the program executable directory to PATH so that Python can find the installed pyenergyplus package
Expand Down
14 changes: 4 additions & 10 deletions src/EnergyPlus/PluginManager.hh
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,9 @@
#include <EnergyPlus/EnergyPlus.hh>

#if LINK_WITH_PYTHON
#ifdef _DEBUG
// We don't want to try to import a debug build of Python here
// so if we are building a Debug build of the C++ code, we need
// to undefine _DEBUG during the #include command for Python.h.
// Otherwise it will fail
#undef _DEBUG
#include <Python.h>
#define _DEBUG
#else
#include <Python.h>
#ifndef PyObject_HEAD
struct _object;
using PyObject = _object;
#endif
#endif

Expand Down Expand Up @@ -175,6 +168,7 @@ namespace PluginManagement {
#endif
};

// TODO: Make this use PythonEngine so we don't duplicate code
class PluginManager
{
public:
Expand Down
Loading
Loading