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

[WIP] Add a command to install CyIpopt #1474

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
93 changes: 93 additions & 0 deletions idaes/commands/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,16 @@
__author__ = "John Eslick"

import os
import sys
import logging
import subprocess

import click

import idaes
import idaes.commands.util.download_bin
from idaes.commands import cb
from pyomo.common.dependencies import attempt_import

_log = logging.getLogger("idaes.commands.extensions")

Expand Down Expand Up @@ -211,3 +214,93 @@ def extensions_license():
@cb.command(name="extensions-version", help="show license info for binary extensions")
def extensions_version():
print_extensions_version()


@cb.command(
name="install-cyipopt",
help="[BETA] Install CyIpopt and link to IDAES IPOPT library",
)
def install_cyipopt():
_, cyipopt_available = attempt_import("cyipopt")
click.echo(
"WARNING: The install-cyipopt command is beta functionality and may not work"
" on all platforms."
)
if cyipopt_available:
click.echo(
"CyIpopt is already available in the current Python environment."
" Please uninstall CyIpopt before running this command. Note that"
" you may need to clear your pip cache to re-install with IDAES"
" binaries."
)
return
pkgconfig_path = os.pathsep.join(
[
# Prepend IDAES's pkgconfig directory to this path so we always
# build cyipopt against IDAES binaries (if they exist) when using this
# command.
os.path.join(idaes.bin_directory, "lib", "pkgconfig"),
os.getenv("PKG_CONFIG_PATH", ""),
]
)
subprocess_environ = dict(os.environ, PKG_CONFIG_PATH=pkgconfig_path)
# TODO: Possibly set IPOPTWINDIR on Windows? I haven't been able to get this to
# work on GHA, so I'd like some advice from somebody who has built CyIpopt
# on Windows before staring to make assumptions. -RBP
ret = subprocess.run(
["pip", "install", "cyipopt"],
env=subprocess_environ,
)
if ret.returncode == 1:
# CyIpopt wheels don't build on Python 3.9 (see
# https://github.com/mechmotum/cyipopt/issues/225), so we have this workaround
# in place as an alternative. If #225 gets resolved or we stop supporting 3.9,
# and no other cases come up where `pip install cyipopt` fails, we can remove
# this code. -RBP
click.echo(
"WARNING: Command `pip install cyipopt` returned 1. Attempting to install"
" from source"
)
orig_cwd = os.getcwd()
try:
os.chdir(idaes.bin_directory)
cyipopt_dir = os.path.join(idaes.bin_directory, "cyipopt")
if os.path.exists(cyipopt_dir):
raise RuntimeError(
f"{cyipopt_dir} already exists. Please remove and try again."
)
subprocess.run(
[
"git",
"clone",
"https://github.com/mechmotum/cyipopt.git",
"--branch=v1.4.1",
],
)
os.chdir(cyipopt_dir)
ret = subprocess.run(
["python", "setup.py", "develop"], env=subprocess_environ
)
finally:
os.chdir(orig_cwd)
if ret.returncode == 1:
raise RuntimeError("Error installing CyIpopt from source.")
click.echo("Installed CyIpopt in the current Python environment.")
libdir = os.path.join(idaes.bin_directory, "lib")
if sys.platform == "nt":
lib_envvar = "PATH"
elif sys.platform == "darwin":
lib_envvar = "DYLD_LIBRARY_PATH"
else:
lib_envvar = "LD_LIBRARY_PATH"
homedir = os.getenv("HOME", "")
bashrc_file = os.path.join(homedir, ".bashrc")
zshrc_file = os.path.join(homedir, ".zshrc")
click.echo(
f"Note: To use CyIpopt, you may need to add {libdir} to your {lib_envvar}"
" environment variable. That is, add the following line to your shell"
f" configuration file (e.g. {bashrc_file} for Bash or {zshrc_file} for Zsh):"
# Or $PROFILE for PowerShell?
)
export_rhs = os.pathsep.join(["$"+lib_envvar, libdir])
click.echo(f"\n export {lib_envvar}={export_rhs}\n")
15 changes: 14 additions & 1 deletion idaes/commands/util/download_bin.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,8 +402,21 @@ def download_binaries(
_verify_checksums(checksum, pname, ptar, ftar)

# Extract solvers
tars_to_unpack = list(ptar)
idaes_local_tarname = f"idaes-local-{platform}.tar.gz"
if "darwin" in idaes_local_tarname:
# We neglected to change the name of the idaes-local tar.gz file from *aarch64
# to *arm64 on ARM Mac, so we have to manually patch the name here. I believe
# this is fixed in the idaes-ext main branch, so this can be removed when
# we update to binaries compiled from this branch. -RBP
idaes_local_tarname = idaes_local_tarname.replace("aarch64", "arm64")
idaes_local_tarname = os.path.join(to_path, idaes_local_tarname)
# In addition to the tar.gz files we downloaded, we want to unpack the idaes-local
# tar.gz file. This is included in idaes-solvers, so we can unpack it in the same
# directory after we've unpacked idaes-solvers.
tars_to_unpack.append(idaes_local_tarname)
links = {}
for n, p in zip(pname, ptar):
for p in tars_to_unpack:
_log.debug(f"Extracting files in {p} to {to_path}")
with tarfile.open(p, "r") as f:
_verify_tar_member_targets(f, to_path, links)
Comment on lines +405 to 422
Copy link
Member Author

Choose a reason for hiding this comment

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

Here we unpack idaes-local* into .idaes/bin, but I think there is no real reason not to unpack it into .idaes, in which case we would end up with bin, lib, include, and share subdirectories of .idaes.

A downside of unpacking idaes-local* is that now we actually distribute two copies of libipopt.so: one in .idaes/bin, and one in idaes/bin/lib... This will be fixed in new versions of idaes-ext.

Expand Down
27 changes: 23 additions & 4 deletions idaes/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -752,14 +752,33 @@ def setup_environment(bin_directory, use_idaes_solvers):
return
oe = orig_environ
if use_idaes_solvers:
os.environ["PATH"] = os.pathsep.join([bin_directory, oe.get("PATH", "")])
os.environ["PATH"] = os.pathsep.join(
# As below, we add idaes/bin/lib to the path so we can (hopefully)
# pick up the ipopt libraries on Windows.
[bin_directory, os.path.join(bin_directory, "lib"), oe.get("PATH", "")]
)
else:
os.environ["PATH"] = os.pathsep.join([oe.get("PATH", ""), bin_directory])
os.environ["PATH"] = os.pathsep.join(
[oe.get("PATH", ""), bin_directory, os.path.join(bin_directory, "lib")]
)
if os.name != "nt": # If not Windows set lib search path, Windows uses PATH
os.environ["LD_LIBRARY_PATH"] = os.pathsep.join(
[oe.get("LD_LIBRARY_PATH", ""), bin_directory]
[
oe.get("LD_LIBRARY_PATH", ""),
bin_directory,
# We add .idaes/bin/lib to LD_LIBRARY_PATH to pick up any libraries
# we introduced by unpacking the idaes-local tar.gz file. This
# directory should be changed when/if the IDAES extensions directory
# structure changes (e.g. to .idaes/lib). -RBP
os.path.join(bin_directory, "lib"),
]
Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure this is necessary (or at all beneficial), as the CyIpopt library appears to use the value of the appropriate variable at Python-start-up-time, rather than (somehow) using os.environ.

)
# This is for macOS, but won't hurt other UNIX
os.environ["DYLD_LIBRARY_PATH"] = os.pathsep.join(
[oe.get("DYLD_LIBRARY_PATH", ""), bin_directory]
[
oe.get("DYLD_LIBRARY_PATH", ""),
bin_directory,
# As above, we are picking up the unpacked idaes-local file here.
os.path.join(bin_directory, "lib"),
]
)
Loading