├── project ├── .gitignore ├── build.properties ├── metals.sbt ├── plugins.sbt └── Dependencies.scala ├── examples └── python-package │ ├── dummy │ └── __init__.py │ ├── setup.py │ └── setup.cfg ├── .scalafix.conf ├── .scalafmt.conf ├── .gitignore ├── src ├── main │ └── scala │ │ └── ai │ │ └── kien │ │ └── python │ │ ├── Defaults.scala │ │ ├── PythonConfig.scala │ │ └── Python.scala └── test │ └── scala │ └── ai │ └── kien │ └── python │ └── PythonTest.scala ├── LICENSE ├── .github └── workflows │ └── ci.yml ├── docs ├── readme.md └── details.md └── README.md /project/.gitignore: -------------------------------------------------------------------------------- 1 | project/ 2 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.11.7 2 | -------------------------------------------------------------------------------- /examples/python-package/dummy/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.1.0' 2 | -------------------------------------------------------------------------------- /examples/python-package/setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | setuptools.setup() 3 | -------------------------------------------------------------------------------- /.scalafix.conf: -------------------------------------------------------------------------------- 1 | rules = [ 2 | OrganizeImports 3 | ] 4 | OrganizeImports.groupedImports = AggressiveMerge 5 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "3.10.1" 2 | align.preset = more 3 | maxColumn = 100 4 | runner.dialect = scala213source3 5 | -------------------------------------------------------------------------------- /examples/python-package/setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = dummy 3 | version = attr: dummy.__version__ 4 | 5 | [options] 6 | packages = find: 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .idea/ 3 | .vscode/ 4 | .metals/ 5 | .bsp/ 6 | .bloop/ 7 | 8 | __pycache__/ 9 | .ipynb_checkpoints/ 10 | 11 | .DS_Store 12 | *.worksheet.sc 13 | 14 | hs_err*.log 15 | -------------------------------------------------------------------------------- /project/metals.sbt: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT! This file is auto-generated. 2 | 3 | // This file enables sbt-bloop to create bloop config files. 4 | 5 | addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.5.3") 6 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.8.0") 2 | addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.11.2") 3 | addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.14.4") 4 | -------------------------------------------------------------------------------- /src/main/scala/ai/kien/python/Defaults.scala: -------------------------------------------------------------------------------- 1 | package ai.kien.python 2 | 3 | import scala.sys.process.Process 4 | import scala.util.Try 5 | 6 | private[python] object Defaults { 7 | def callProcess(cmd: Seq[String]) = Try(Process(cmd).!!.trim) 8 | 9 | def getEnv(k: String) = Option(System.getenv(k)) 10 | } 11 | -------------------------------------------------------------------------------- /project/Dependencies.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | object Dependencies { 4 | lazy val scalaCollectionCompat = 5 | "org.scala-lang.modules" %% "scala-collection-compat" % "2.12.0" 6 | lazy val scalapy = "dev.scalapy" %% "scalapy-core" % "0.5.3" 7 | lazy val scalaTest = "org.scalatest" %% "scalatest" % "3.2.19" 8 | } 9 | -------------------------------------------------------------------------------- /src/test/scala/ai/kien/python/PythonTest.scala: -------------------------------------------------------------------------------- 1 | package ai.kien.python 2 | 3 | import me.shadaj.scalapy.py 4 | import me.shadaj.scalapy.py.{PyQuote, SeqConverters} 5 | import org.scalatest.funsuite.AnyFunSuite 6 | 7 | class PythonTest extends AnyFunSuite { 8 | Python().scalapyProperties.get.foreach { case (k, v) => System.setProperty(k, v) } 9 | 10 | test("ScalaPy runs successfully") { 11 | py.Dynamic.global.list(Seq(1, 2, 3).toPythonCopy) 12 | py"'Hello from ScalaPy!'" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/scala/ai/kien/python/PythonConfig.scala: -------------------------------------------------------------------------------- 1 | package ai.kien.python 2 | 3 | import scala.util.Try 4 | 5 | private[python] class PythonConfig( 6 | pythonConfig: String, 7 | callProcess: Seq[String] => Try[String] = Defaults.callProcess 8 | ) { 9 | def callPythonConfig(cmd: String*): Try[String] = callProcess(pythonConfig +: cmd) 10 | 11 | lazy val ldflags: Try[String] = 12 | callPythonConfig("--ldflags", "--embed") 13 | .recoverWith { case _ => callPythonConfig("--ldflags") } 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Dang Trung Kien 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | tags: 7 | - v* 8 | pull_request: 9 | 10 | jobs: 11 | test: 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: [ubuntu-latest, windows-latest] 17 | jdk: [8] 18 | python: [3.8, 3.9, "3.10", "3.11", "3.12"] 19 | steps: 20 | - uses: actions/checkout@master 21 | - uses: coursier/cache-action@v6.3 22 | - uses: coursier/setup-action@v1 23 | with: 24 | jvm: ${{ matrix.jdk }} 25 | apps: sbtn sbt 26 | - name: Set up Python ${{ matrix.python }} 27 | uses: actions/setup-python@v2 28 | with: 29 | python-version: ${{ matrix.python }} 30 | - name: Test 31 | run: sbt +test 32 | shell: bash 33 | publish: 34 | runs-on: ubuntu-latest 35 | needs: [test] 36 | if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) 37 | steps: 38 | - uses: actions/checkout@v5 39 | with: 40 | fetch-depth: 0 41 | - uses: actions/setup-java@v5 42 | with: 43 | distribution: temurin 44 | java-version: 8 45 | cache: sbt 46 | - uses: sbt/setup-sbt@v1 47 | - run: sbt ci-release 48 | env: 49 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 50 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 51 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 52 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 53 | -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Overview 4 | 5 | The canonical use case is to help set up [`ScalaPy`](https://scalapy.dev/) to point to a specific Python installation by attempting to infer the correct configuration properties used by `ScalaPy` during the initialization of the embedded Python interpreter. This could potentially see usage outside of `ScalaPy` too since these properties are relevant to embedded Python in general. 6 | 7 | ## Usage 8 | 9 | By default `Python` checks for the `python3` executable (or `python` if `python3` is not found) on `PATH` 10 | 11 | ```scala mdoc 12 | import ai.kien.python.Python 13 | 14 | val python = Python() 15 | 16 | python.nativeLibrary 17 | 18 | python.nativeLibraryPaths 19 | 20 | python.scalapyProperties 21 | 22 | python.ldflags 23 | ``` 24 | 25 | You can point it towards a specific Python installation by passing the path to the interpreter executable to `Python` 26 | 27 | ```scala mdoc:nest 28 | val python = Python("@PYTHON@") 29 | 30 | python.nativeLibrary 31 | 32 | python.nativeLibraryPaths 33 | 34 | python.scalapyProperties 35 | 36 | python.ldflags 37 | ``` 38 | 39 | See `docs/details.md` to see the full list of these properties and what they mean. 40 | 41 | `scalapyProperties` contains the system properties used by `ScalaPy`. For example, to set up `ScalaPy` to use the Python located at `@PYTHON@` in [`Ammonite`](https://ammonite.io/) or [`Almond`](https://almond.sh/) run 42 | 43 | ```scala 44 | import $ivy.`ai.kien::python-native-libs:` 45 | import $ivy.`me.shadaj::scalapy-core:` 46 | 47 | import ai.kien.python.Python 48 | 49 | Python("@PYTHON@").scalapyProperties.fold( 50 | ex => println(s"Error while getting ScalaPy properties: $ex"), 51 | props => props.foreach { case(k, v) => System.setProperty(k, v) } 52 | ) 53 | 54 | import me.shadaj.scalapy.py 55 | 56 | println(py.module("sys").version) 57 | ``` 58 | -------------------------------------------------------------------------------- /docs/details.md: -------------------------------------------------------------------------------- 1 | # Details 2 | 3 | ## `scalapy.python.library`/`nativeLibrary` 4 | 5 | Name of the Python shared library, *i.e* the Python shared library file name (*e.g* `libpython3.9m.dylib`, `libpython3.7.so`) without the `lib` prefix and the file extension. 6 | 7 | The Python shared library take the form of `python{ldversion}` where `ldversion` is `{major}.{minor}{abiflags}`. There are 2 ways to get the name of the Python shared library 8 | 9 | ```python 10 | from sysconfig import get_config_var 11 | 12 | ldversion = get_config_var("LDVERSION") 13 | 14 | python_native_lib = f"python{ldversion}" 15 | ``` 16 | 17 | or 18 | 19 | ```python 20 | from sys import abiflags 21 | import sysconfig 22 | 23 | version = sysconfig.get_python_version() 24 | 25 | python_native_lib = f"python{version}{abiflags}" 26 | ``` 27 | 28 | We went with the 2nd method since it is more portable. 29 | 30 | ## `jna.library.path`/`nativeLibraryPaths` 31 | 32 | The directory where the Python shared library lives. It is located in either `f"{sys.base_prefix}/lib"` or config dir `sysconfig.get_config_var('LIBPL')`. For each Python installation, which one of these 2 directories actually contains the shared library is almost arbitrary. We decided to just use both with priority given to the config dir `f"{sysconfig.get_config_var('LIBPL')}:{sys.base_prefix}/lib"`. 33 | 34 | ## `scalapy.python.programname`/`executable` 35 | 36 | The path to the Python interpreter executable, used as the input to `Py_SetProgramName`. The Python doc recommends running `Py_SetProgramName` prior to `Py_Initialize` so that the correct Python run-time libraries relative to the interpreter executable can be found (`prefix`, `exec_prefix`, ...) 37 | 38 | https://docs.python.org/3/extending/embedding.html#very-high-level-embedding 39 | https://docs.python.org/3/c-api/init.html#c.Py_SetProgramName 40 | 41 | This enables using ScalaPy with virtualenv. Since a Python installation inside virtualenv shares the same shared library (`libpython...so`) with the base installation, without `Py_SetProgramName`, there's no way to point ScalaPy towards the runtime directories set by virtualenv. 42 | 43 | ```python 44 | import sys 45 | 46 | sys.executable 47 | ``` 48 | 49 | ## `ldflags` 50 | 51 | The recommended linker options for embedding the Python interpreter into another application, mostly extracted from the outputs of 52 | 53 | `pythonX.Y-config --ldflags` for python `3.7` and 54 | 55 | `pythonX.Y-config --ldflags --embed` for python `3.8+`, 56 | 57 | with added library paths from `nativeLibraryPaths` 58 | 59 | https://docs.python.org/3/extending/embedding.html#compiling-and-linking-under-unix-like-systems 60 | 61 | The full path to `pythonX.Y-config` is `f"{sys.base_prefix}/bin/python{ldversion}-config"`. 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-native-libs 2 | 3 | Helpers for setting up an embedded Python interpreter 4 | 5 | ![Build Status](https://github.com/kiendang/python-native-libs/actions/workflows/ci.yml/badge.svg) 6 | [![Maven Central](https://img.shields.io/maven-central/v/ai.kien/python-native-libs_2.13.svg)](https://maven-badges.herokuapp.com/maven-central/ai.kien/python-native-libs_2.13) 7 | 8 | ## Overview 9 | 10 | The canonical use case is to help set up [`ScalaPy`](https://scalapy.dev/) to point to a specific Python installation by attempting to infer the correct configuration properties used by `ScalaPy` during the initialization of the embedded Python interpreter. This could potentially see usage outside of `ScalaPy` too since these properties are relevant to embedded Python in general. 11 | 12 | ## Usage 13 | 14 | By default `Python` checks for the `python3` executable (or `python` if `python3` is not found) on `PATH` 15 | 16 | ```scala 17 | import ai.kien.python.Python 18 | 19 | val python = Python() 20 | // python: Python = ai.kien.python.Python@5eb35f5d 21 | 22 | python.nativeLibrary 23 | // res0: util.Try[String] = Success(value = "python3.9") 24 | 25 | python.nativeLibraryPaths 26 | // res1: util.Try[Seq[String]] = Success( 27 | // value = ArraySeq( 28 | // "/usr/local/opt/python@3.9/Frameworks/Python.framework/Versions/3.9/lib/python3.9/config-3.9-darwin", 29 | // "/usr/local/opt/python@3.9/Frameworks/Python.framework/Versions/3.9/lib" 30 | // ) 31 | // ) 32 | 33 | python.scalapyProperties 34 | // res2: util.Try[Map[String, String]] = Success( 35 | // value = Map( 36 | // "jna.library.path" -> "/usr/local/opt/python@3.9/Frameworks/Python.framework/Versions/3.9/lib/python3.9/config-3.9-darwin:/usr/local/opt/python@3.9/Frameworks/Python.framework/Versions/3.9/lib", 37 | // "scalapy.python.library" -> "python3.9", 38 | // "scalapy.python.programname" -> "/usr/local/opt/python@3.9/bin/python3.9" 39 | // ) 40 | // ) 41 | 42 | python.ldflags 43 | // res3: util.Try[Seq[String]] = Success( 44 | // value = ArraySeq( 45 | // "-L/usr/local/opt/python@3.9/Frameworks/Python.framework/Versions/3.9/lib/python3.9/config-3.9-darwin", 46 | // "-L/usr/local/opt/python@3.9/Frameworks/Python.framework/Versions/3.9/lib", 47 | // "-lpython3.9", 48 | // "-ldl", 49 | // "-framework", 50 | // "CoreFoundation" 51 | // ) 52 | // ) 53 | ``` 54 | 55 | You can point it towards a specific Python installation by passing the path to the interpreter executable to `Python` 56 | 57 | ```scala 58 | val python = Python("/usr/bin/python3") 59 | // python: Python = ai.kien.python.Python@eb0b5d0 60 | 61 | python.nativeLibrary 62 | // res4: util.Try[String] = Success(value = "python3.9") 63 | 64 | python.nativeLibraryPaths 65 | // res5: util.Try[Seq[String]] = Success( 66 | // value = ArraySeq( 67 | // "/usr/local/opt/python@3.9/Frameworks/Python.framework/Versions/3.9/lib/python3.9/config-3.9-darwin", 68 | // "/usr/local/opt/python@3.9/Frameworks/Python.framework/Versions/3.9/lib" 69 | // ) 70 | // ) 71 | 72 | python.scalapyProperties 73 | // res6: util.Try[Map[String, String]] = Success( 74 | // value = Map( 75 | // "jna.library.path" -> "/usr/local/opt/python@3.9/Frameworks/Python.framework/Versions/3.9/lib/python3.9/config-3.9-darwin:/usr/local/opt/python@3.9/Frameworks/Python.framework/Versions/3.9/lib", 76 | // "scalapy.python.library" -> "python3.9", 77 | // "scalapy.python.programname" -> "/usr/local/opt/python@3.9/bin/python3.9" 78 | // ) 79 | // ) 80 | 81 | python.ldflags 82 | // res7: util.Try[Seq[String]] = Success( 83 | // value = ArraySeq( 84 | // "-L/usr/local/opt/python@3.9/Frameworks/Python.framework/Versions/3.9/lib/python3.9/config-3.9-darwin", 85 | // "-L/usr/local/opt/python@3.9/Frameworks/Python.framework/Versions/3.9/lib", 86 | // "-lpython3.9", 87 | // "-ldl", 88 | // "-framework", 89 | // "CoreFoundation" 90 | // ) 91 | // ) 92 | ``` 93 | 94 | See `docs/details.md` to see the full list of these properties and what they mean. 95 | 96 | `scalapyProperties` contains the system properties used by `ScalaPy`. For example, to set up `ScalaPy` to use the Python located at `/usr/bin/python3` in [`Ammonite`](https://ammonite.io/) or [`Almond`](https://almond.sh/) run 97 | 98 | ```scala 99 | import $ivy.`ai.kien::python-native-libs:` 100 | import $ivy.`me.shadaj::scalapy-core:` 101 | 102 | import ai.kien.python.Python 103 | 104 | Python("/usr/bin/python3").scalapyProperties.fold( 105 | ex => println(s"Error while getting ScalaPy properties: $ex"), 106 | props => props.foreach { case(k, v) => System.setProperty(k, v) } 107 | ) 108 | 109 | import me.shadaj.scalapy.py 110 | 111 | println(py.module("sys").version) 112 | ``` 113 | -------------------------------------------------------------------------------- /src/main/scala/ai/kien/python/Python.scala: -------------------------------------------------------------------------------- 1 | package ai.kien.python 2 | 3 | import java.io.{File, FileNotFoundException} 4 | import java.nio.file.{FileSystem, FileSystems, Files} 5 | import scala.util.{Properties, Success, Try} 6 | 7 | /** A class for extracting the necessary configuration properties for embedding a specific Python 8 | * interpreter into an appication 9 | */ 10 | class Python private[python] ( 11 | interpreter: Option[String] = None, 12 | callProcess: Seq[String] => Try[String] = Defaults.callProcess, 13 | getEnv: String => Option[String] = Defaults.getEnv, 14 | fs: FileSystem = FileSystems.getDefault, 15 | isWindows: Option[Boolean] = None 16 | ) { 17 | 18 | /** Provides a list of possible locations for the `libpython` corresponding to this Python 19 | * interpreter 20 | */ 21 | lazy val nativeLibraryPaths: Try[Seq[String]] = 22 | callPython(if (isWin) Python.libPathCmdWin else Python.libPathCmd) 23 | .map(_.split(";")) 24 | .map(_.map(_.trim).distinct.filter(_.nonEmpty).toSeq) 25 | 26 | /** Name of the `libpython` corresponding to this Python interpreter, ''e.g.'' `python3.8`, 27 | * `python3.7m` 28 | */ 29 | lazy val nativeLibrary: Try[String] = ldversion.map("python" + _) 30 | 31 | /** Absolute path to the Python interpreter executable 32 | */ 33 | lazy val executable: Try[String] = callPython(Python.executableCmd) 34 | 35 | /** Provides the system properties necessary for setting up [[https://scalapy.dev/ ScalaPy]] with 36 | * this Python interpreter 37 | * 38 | * @example 39 | * 40 | * {{{ 41 | * import me.shadaj.scalapy.py 42 | * 43 | * Python("/usr/local/bin/python3").scalapyProperties.get.foreach { 44 | * case (k, v) => System.setProperty(k, v) 45 | * } 46 | * println(py.eval("'Hello from Python!'")) 47 | * }}} 48 | */ 49 | def scalapyProperties: Try[Map[String, String]] = for { 50 | nativeLibPaths <- nativeLibraryPaths 51 | library <- nativeLibrary 52 | executable <- executable 53 | } yield { 54 | val currentPathsStr = Properties.propOrEmpty("jna.library.path") 55 | val currentPaths = currentPathsStr.split(pathSeparator) 56 | 57 | val pathsToAdd = 58 | if (currentPaths.containsSlice(nativeLibPaths)) Nil else nativeLibPaths 59 | val pathsToAddStr = pathsToAdd.mkString(pathSeparator) 60 | 61 | val newPaths = (currentPathsStr, pathsToAddStr) match { 62 | case (c, p) if c.isEmpty => p 63 | case (c, p) if p.isEmpty => c 64 | case (c, p) => s"$p$pathSeparator$c" 65 | } 66 | 67 | Map( 68 | "jna.library.path" -> newPaths, 69 | "scalapy.python.library" -> library, 70 | "scalapy.python.programname" -> executable 71 | ) 72 | } 73 | 74 | /** Provides the recommended linker options for embedding this Python interpreter into another 75 | * application, mostly extracted from the outputs of 76 | * 77 | * `pythonX.Y-config --ldflags` for `python` 3.7 and 78 | * 79 | * `pythonX.Y-config --ldflags --embed` for `python` 3.8+ 80 | */ 81 | lazy val ldflags: Try[Seq[String]] = if (isWin) ldflagsWin else ldflagsNix 82 | 83 | lazy val ldflagsWin = for { 84 | nativeLibraryPaths <- nativeLibraryPaths 85 | nativeLibrary <- nativeLibrary 86 | } yield ("-l" + nativeLibrary) +: nativeLibraryPaths.map("-L" + _) 87 | 88 | lazy val ldflagsNix: Try[Seq[String]] = for { 89 | rawLdflags <- rawLdflags 90 | nativeLibraryPaths <- nativeLibraryPaths 91 | libPathFlags = nativeLibraryPaths.map("-L" + _) 92 | flags = rawLdflags 93 | .split("\\s+(?=-)") 94 | .filter(f => f.nonEmpty && !libPathFlags.contains(f)) 95 | .flatMap(f => if (f.startsWith("-L")) Array(f) else f.split("\\s+")) 96 | .toSeq 97 | } yield libPathFlags ++ flags 98 | 99 | private val path: String = getEnv("PATH").getOrElse("") 100 | 101 | private lazy val isWin = isWindows.getOrElse( 102 | System.getProperty("os.name").startsWith("Windows") 103 | ) 104 | 105 | private val pathSeparator = 106 | isWindows.map(if (_) ";" else ":").getOrElse(File.pathSeparator) 107 | 108 | private def existsInPath(exec: String): Boolean = { 109 | val pathExts = getEnv("PATHEXT").getOrElse("").split(pathSeparator) 110 | val l = for { 111 | elem <- path.split(pathSeparator).iterator 112 | elemPath = fs.getPath(elem) 113 | ext <- pathExts.iterator 114 | } yield Files.exists(elemPath.resolve(exec + ext)) 115 | 116 | l.contains(true) 117 | } 118 | 119 | private lazy val python: Try[String] = Try( 120 | if (existsInPath("python3")) 121 | "python3" 122 | else if (existsInPath("python")) 123 | "python" 124 | else 125 | throw new FileNotFoundException( 126 | "Neither python3 nor python was found in $PATH." 127 | ) 128 | ) 129 | 130 | private lazy val interp: Try[String] = 131 | interpreter.map(Success(_)).getOrElse(python) 132 | 133 | private def callPython(cmd: String): Try[String] = 134 | interp.flatMap(python => callProcess(Seq(python, "-c", cmd))) 135 | 136 | private def ldversion: Try[String] = callPython( 137 | if (isWin) Python.ldversionCmdWin else Python.ldversionCmd 138 | ) 139 | 140 | private lazy val binDir = 141 | callPython("import sys;print(sys.base_prefix)") 142 | .map(base => s"${base}${fs.getSeparator}bin") 143 | 144 | private lazy val pythonConfigExecutable = for { 145 | binDir <- binDir 146 | ldversion <- ldversion 147 | pythonConfigExecutable = s"${binDir}${fs.getSeparator}python${ldversion}-config" 148 | _ <- Try { 149 | if (!Files.exists(fs.getPath(pythonConfigExecutable))) 150 | throw new FileNotFoundException(s"$pythonConfigExecutable does not exist") 151 | else () 152 | } 153 | } yield pythonConfigExecutable 154 | 155 | private lazy val pythonConfig = pythonConfigExecutable.map(new PythonConfig(_, callProcess)) 156 | 157 | private lazy val rawLdflags = pythonConfig.flatMap(_.ldflags) 158 | } 159 | 160 | object Python { 161 | 162 | /** @param interpreter 163 | * optional path to a Python interpreter executable, which defaults to `Some("python3")` if not 164 | * provided 165 | * 166 | * @example 167 | * 168 | * {{{ 169 | * val python = Python() 170 | * python.scalapyProperties.get.foreach { 171 | * case (k, v) => System.setProperty(k, v) 172 | * } 173 | * 174 | * import me.shadaj.scalapy.py 175 | * println(py.eval("'Hello from Python!'")) 176 | * }}} 177 | * 178 | * @return 179 | * an instance of [[ai.kien.python.Python]] which provides the necessary configuration 180 | * properties for embedding a specific Python interpreter 181 | */ 182 | def apply(interpreter: Option[String] = None): Python = new Python(interpreter) 183 | 184 | /** @param interpreter 185 | * path to a Python interpreter executable 186 | * 187 | * @example 188 | * 189 | * {{{ 190 | * val python = Python("/usr/local/bin/python3") 191 | * python.scalapyProperties.get.foreach { 192 | * case (k, v) => System.setProperty(k, v) 193 | * } 194 | * 195 | * import me.shadaj.scalapy.py 196 | * println(py.eval("'Hello from Python!'")) 197 | * }}} 198 | * 199 | * @return 200 | * an instance of [[ai.kien.python.Python]] which provides the necessary configuration 201 | * properties for embedding a specific Python interpreter 202 | */ 203 | def apply(interpreter: String): Python = apply(Some(interpreter)) 204 | 205 | private def executableCmd = "import sys;print(sys.executable)" 206 | 207 | private def ldversionCmd = 208 | """import sys 209 | |import sysconfig 210 | |try: 211 | | abiflags = sys.abiflags 212 | |except AttributeError: 213 | | abiflags = sysconfig.get_config_var('abiflags') or '' 214 | |print(sysconfig.get_python_version() + abiflags) 215 | """.stripMargin 216 | 217 | private def ldversionCmdWin = 218 | """import sys 219 | |import sysconfig 220 | |try: 221 | | abiflags = sys.abiflags 222 | |except AttributeError: 223 | | abiflags = sysconfig.get_config_var('abiflags') or '' 224 | |print(''.join(map(str, sys.version_info[:2])) + abiflags) 225 | """.stripMargin 226 | 227 | private def libPathCmd = 228 | """import sys 229 | |import os.path 230 | |from sysconfig import get_config_var 231 | |libpl = get_config_var('LIBPL') 232 | |libpl = libpl + ';' if libpl is not None else '' 233 | |print(libpl + os.path.join(sys.base_prefix, 'lib')) 234 | """.stripMargin 235 | 236 | private def libPathCmdWin = 237 | """import sys 238 | |print(sys.base_prefix) 239 | """.stripMargin 240 | } 241 | --------------------------------------------------------------------------------