Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 2 additions & 1 deletion doc/languages-frameworks/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ and the aliases
#### `buildPythonPackage` function

The `buildPythonPackage` function is implemented in
`pkgs/development/python-modules/generic/default.nix`
`pkgs/development/interpreters/python/build-python-package.nix`

and can be used as:

Expand Down Expand Up @@ -536,6 +536,7 @@ All parameters from `mkDerivation` function are still supported.
* `installFlags`: A list of strings. Arguments to be passed to `pip install`. To pass options to `python setup.py install`, use `--install-option`. E.g., `installFlags=["--install-option='--cpp_implementation'"].
* `format`: Format of the source. Options are `setup` for when the source has a `setup.py` and `setuptools` is used to build a wheel, and `wheel` in case the source is already a binary wheel. The default value is `setup`.
* `catchConflicts` If `true`, abort package build if a package name appears more than once in dependency tree. Default is `true`.
* `checkInputs` Dependencies needed for running the `checkPhase`. These are added to `buildInputs` when `doCheck = true`.
Copy link
Contributor

Choose a reason for hiding this comment

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

I like this conceptually, but is there really anything wrong with just deeming these to be buildInputs?

Copy link
Member Author

Choose a reason for hiding this comment

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

Not really. As you can read, they are put together anyway. It might save some building when you disable tests. Furthermore, I'm still playing with the idea of separating the building and installing/testing in two separate derivations.


#### `buildPythonApplication` function

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,57 +3,37 @@
(http://pypi.python.org/pypi/setuptools/), which represents a large
number of Python packages nowadays. */

{ python, setuptools, unzip, wrapPython, lib, bootstrapped-pip
, ensureNewerSourcesHook }:
{ lib
, python
, mkPythonDerivation
, bootstrapped-pip
}:

{ name

# by default prefix `name` e.g. "python3.3-${name}"
, namePrefix ? python.libPrefix + "-"

, buildInputs ? []
{ buildInputs ? []

# propagate build dependencies so in case we have A -> B -> C,
# C can import package A propagated by B
, propagatedBuildInputs ? []
# C can import package A propagated by B
#, propagatedBuildInputs ? []

# passed to "python setup.py build_ext"
# https://github.com/pypa/pip/issues/881
, setupPyBuildFlags ? []

# DEPRECATED: use propagatedBuildInputs
, pythonPath ? []

# used to disable derivation, useful for specific python versions
, disabled ? false

, meta ? {}

# Execute before shell hook
, preShellHook ? ""

# Execute after shell hook
, postShellHook ? ""

# Additional arguments to pass to the makeWrapper function, which wraps
# generated binaries.
, makeWrapperArgs ? []

# Additional flags to pass to "pip install".
, installFlags ? []

# Raise an error if two packages are installed with the same name
, catchConflicts ? true

, format ? "setup"

, ... } @ attrs:


# Keep extra attributes from `attrs`, e.g., `patchPhase', etc.
if disabled
then throw "${name} not supported for interpreter ${python.executable}"
else


let
# use setuptools shim (so that setuptools is imported before distutils)
Expand All @@ -73,14 +53,10 @@ let
installCheckPhase = attrs.checkPhase or ":";

# Wheels don't have any checks to run
doInstallCheck = attrs.doCheck or false;
doCheck = attrs.doCheck or false;
}
else if format == "setup" then
{
# propagate python/setuptools to active setup-hook in nix-shell
propagatedBuildInputs =
propagatedBuildInputs ++ [ python setuptools ];

# we copy nix_run_setup.py over so it's executed relative to the root of the source
# many project make that assumption
buildPhase = attrs.buildPhase or ''
Expand All @@ -100,21 +76,17 @@ let
# are typically distributed with tests.
# With Python it's a common idiom to run the tests
# after the software has been installed.

# For backwards compatibility, let's use an alias
doInstallCheck = attrs.doCheck or true;
doCheck = attrs.doCheck or true;
}
else
throw "Unsupported format ${format}";
in
python.stdenv.mkDerivation (builtins.removeAttrs attrs ["disabled" "doCheck"] // {
name = namePrefix + name;

buildInputs = [ wrapPython bootstrapped-pip ] ++ buildInputs ++ pythonPath
++ [ (ensureNewerSourcesHook { year = "1980"; }) ]
++ (lib.optional (lib.hasSuffix "zip" attrs.src.name or "") unzip);
in mkPythonDerivation ( attrs // {

# To build and install a wheel we need pip
buildInputs = buildInputs ++ [ bootstrapped-pip ];

pythonPath = pythonPath;
#inherit propagatedBuildInputs;

configurePhase = attrs.configurePhase or ''
runHook preConfigure
Expand All @@ -126,9 +98,6 @@ python.stdenv.mkDerivation (builtins.removeAttrs attrs ["disabled" "doCheck"] //
runHook postConfigure
'';

# Python packages don't have a checkPhase, only an installCheckPhase
doCheck = false;

installPhase = attrs.installPhase or ''
runHook preInstall

Expand All @@ -142,14 +111,6 @@ python.stdenv.mkDerivation (builtins.removeAttrs attrs ["disabled" "doCheck"] //
runHook postInstall
'';

postFixup = attrs.postFixup or ''
wrapPythonPrograms
'' + lib.optionalString catchConflicts ''
# check if we have two packages with the same name in closure and fail
# this shouldn't happen, something went wrong with dependencies specs
${python.interpreter} ${./catch_conflicts.py}
'';

shellHook = attrs.shellHook or ''
${preShellHook}
if test -e setup.py; then
Expand All @@ -162,13 +123,4 @@ python.stdenv.mkDerivation (builtins.removeAttrs attrs ["disabled" "doCheck"] //
${postShellHook}
'';

meta = with lib.maintainers; {
# default to python's platforms
platforms = python.meta.platforms;
} // meta // {
# add extra maintainer(s) to every package
maintainers = (meta.maintainers or []) ++ [ chaoflow domenkozar ];
# a marker for release utilities to discover python packages
isBuildPythonPackage = python.meta.platforms;
};
} // formatspecific)
93 changes: 93 additions & 0 deletions pkgs/development/interpreters/python/mk-python-derivation.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/* Generic builder for Python packages that come without a setup.py. */

{ lib
, python
, wrapPython
, setuptools
, unzip
, ensureNewerSourcesHook
}:

{ name

# by default prefix `name` e.g. "python3.3-${name}"
, namePrefix ? python.libPrefix + "-"

# Dependencies for building the package
, buildInputs ? []

# Dependencies needed for running the checkPhase.
# These are added to buildInputs when doCheck = true.
, checkInputs ? []

# propagate build dependencies so in case we have A -> B -> C,
# C can import package A propagated by B
, propagatedBuildInputs ? []

# DEPRECATED: use propagatedBuildInputs
, pythonPath ? []

# used to disable derivation, useful for specific python versions
, disabled ? false

# Raise an error if two packages are installed with the same name
, catchConflicts ? true

# Additional arguments to pass to the makeWrapper function, which wraps
# generated binaries.
, makeWrapperArgs ? []

, meta ? {}

, passthru ? {}

, ... } @ attrs:


# Keep extra attributes from `attrs`, e.g., `patchPhase', etc.
if disabled
then throw "${name} not supported for interpreter ${python.executable}"
else

python.stdenv.mkDerivation (builtins.removeAttrs attrs ["disabled"] // {

name = namePrefix + name;

inherit pythonPath;

buildInputs = [ wrapPython ] ++ buildInputs ++ pythonPath
++ [ (ensureNewerSourcesHook { year = "1980"; }) ]
++ (lib.optional (lib.hasSuffix "zip" attrs.src.name or "") unzip)
++ lib.optionals attrs.doCheck checkInputs;

# propagate python/setuptools to active setup-hook in nix-shell
propagatedBuildInputs = propagatedBuildInputs ++ [ python setuptools ];

# Python packages don't have a checkPhase, only an installCheckPhase
doCheck = false;
doInstallCheck = attrs.doCheck or false;

postFixup = attrs.postFixup or ''
wrapPythonPrograms
'' + lib.optionalString catchConflicts ''
# check if we have two packages with the same name in closure and fail
# this shouldn't happen, something went wrong with dependencies specs
${python.interpreter} ${./catch_conflicts.py}
'';

passthru = {
inherit python; # The python interpreter
} // passthru;

meta = with lib.maintainers; {
# default to python's platforms
platforms = python.meta.platforms;
} // meta // {
# add extra maintainer(s) to every package
maintainers = (meta.maintainers or []) ++ [ chaoflow domenkozar ];
# a marker for release utilities to discover python packages
isBuildPythonPackage = python.meta.platforms;
};
})


51 changes: 51 additions & 0 deletions pkgs/development/interpreters/python/wrap-python.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{ lib
, python
, makeSetupHook
, makeWrapper }:

with lib;

makeSetupHook {
deps = makeWrapper;
Copy link
Contributor

Choose a reason for hiding this comment

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

2-space Indentation

substitutions.libPrefix = python.libPrefix;
substitutions.executable = python.interpreter;
substitutions.python = python;
substitutions.magicalSedExpression = let
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe a high-level explanation of what this is doing? I've never understood this tbh

Copy link
Member Author

Choose a reason for hiding this comment

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

+1 to that. I have a rough idea of what it does, but my regex-fu isn't too great.

# Looks weird? Of course, it's between single quoted shell strings.
# NOTE: Order DOES matter here, so single character quotes need to be
# at the last position.
quoteVariants = [ "'\"'''\"'" "\"\"\"" "\"" "'\"'\"'" ]; # hey Vim: ''

mkStringSkipper = labelNum: quote: let
label = "q${toString labelNum}";
isSingle = elem quote [ "\"" "'\"'\"'" ];
endQuote = if isSingle then "[^\\\\]${quote}" else quote;
in ''
/^[a-z]?${quote}/ {
/${quote}${quote}|${quote}.*${endQuote}/{n;br}
:${label}; n; /^${quote}/{n;br}; /${endQuote}/{n;br}; b${label}
}
'';

# This preamble does two things:
# * Sets argv[0] to the original application's name; otherwise it would be .foo-wrapped.
# Python doesn't support `exec -a`.
# * Adds all required libraries to sys.path via `site.addsitedir`. It also handles *.pth files.
preamble = ''
import sys
import site
import functools
sys.argv[0] = '"'$(basename "$f")'"'
Copy link
Contributor

Choose a reason for hiding this comment

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

Why basename here? Wouldn't it be better to use the full path?

Copy link
Member Author

Choose a reason for hiding this comment

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

Imagine a script that is patched like this and calls itself in a subprocess. We now want to use this script in a buildEnv so it can reach other modules as well. If you hardcode the path, it won't be able to reach those modules whereas if it would just call the basename, it can.

However, according to the Python docs it can actually be the full pathname on certain systems.

The list of command line arguments passed to a Python script. argv[0] is the script name (it is operating system dependent whether this is a full pathname or not). If the command was executed using the -c command line option to the interpreter, argv[0] is set to the string '-c'. If no script name was passed to the Python interpreter, argv[0] is the empty string.

https://docs.python.org/3.5/library/sys.html

functools.reduce(lambda k, p: site.addsitedir(p, k), ['"$([ -n "$program_PYTHONPATH" ] && (echo "'$program_PYTHONPATH'" | sed "s|:|','|g") || true)"'], site._init_pathinfo())
'';

in ''
1 {
:r
/\\$|,$/{N;br}
/__future__|^ |^ *(#.*)?$/{n;br}
${concatImapStrings mkStringSkipper quoteVariants}
/^[^# ]/i ${replaceStrings ["\n"] [";"] preamble}
}
'';
} ./wrap.sh
4 changes: 2 additions & 2 deletions pkgs/development/python-modules/bootstrapped-pip/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ let
sha256 = "ea8033fc9905804e652f75474d33410a07404c1a78dd3c949a66863bd1050ebd";
};
setuptools_source = fetchurl {
url = "https://pypi.python.org/packages/3.5/s/setuptools/setuptools-19.4-py2.py3-none-any.whl";
sha256 = "0801e6d862ca4ce24d918420d62f07ee2fe736dc016e3afa99d2103e7a02e9a6";
url = "https://files.pythonhosted.org/packages/3b/c7/e9724e6f81c96248fba5876054418c11d327b3093d075790903cd66fad44/setuptools-26.1.1-py2.py3-none-any.whl";
sha256 = "226c9ce65e76c6069e805982b036f36dc4b227b37dd87fc219aef721ec8604ae";
};
argparse_source = fetchurl {
url = "https://pypi.python.org/packages/2.7/a/argparse/argparse-1.4.0-py2.py3-none-any.whl";
Expand Down
16 changes: 6 additions & 10 deletions pkgs/development/python-modules/dbus/default.nix
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{ stdenv, fetchurl, python, pkgconfig, dbus, dbus_glib, dbus_tools, isPyPy
{ lib, fetchurl, mkPythonDerivation, python, pkgconfig, dbus, dbus_glib, dbus_tools, isPyPy
, ncurses, pygobject3 }:

if isPyPy then throw "dbus-python not supported for interpreter ${python.executable}" else stdenv.mkDerivation rec {
if isPyPy then throw "dbus-python not supported for interpreter ${python.executable}" else mkPythonDerivation rec {
name = "dbus-python-1.2.4";

src = fetchurl {
Expand All @@ -11,21 +11,17 @@ if isPyPy then throw "dbus-python not supported for interpreter ${python.executa

postPatch = "patchShebangs .";

buildInputs = [ python pkgconfig dbus dbus_glib ]
++ stdenv.lib.optionals doCheck [ dbus_tools pygobject3 ]
buildInputs = [ pkgconfig dbus dbus_glib ]
++ lib.optionals doCheck [ dbus_tools pygobject3 ]
# My guess why it's sometimes trying to -lncurses.
# It seems not to retain the dependency anyway.
++ stdenv.lib.optional (! python ? modules) ncurses;
++ lib.optional (! python ? modules) ncurses;

doCheck = true;

# Set empty pythonPath, so that the package is recognized as a python package
# for python.buildEnv
pythonPath = [];

meta = {
description = "Python DBus bindings";
license = stdenv.lib.licenses.mit;
license = lib.licenses.mit;
platforms = dbus.meta.platforms;
};
}
8 changes: 4 additions & 4 deletions pkgs/development/python-modules/pycairo/default.nix
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{ stdenv, fetchurl, fetchpatch, python, pkgconfig, cairo, xlibsWrapper, isPyPy, isPy35 }:
{ lib, fetchurl, fetchpatch, python, mkPythonDerivation, pkgconfig, cairo, xlibsWrapper, isPyPy, isPy35 }:

if (isPyPy) then throw "pycairo not supported for interpreter ${python.executable}" else stdenv.mkDerivation rec {
if (isPyPy) then throw "pycairo not supported for interpreter ${python.executable}" else mkPythonDerivation rec {
version = "1.10.0";
name = "${python.libPrefix}-pycairo-${version}";
src = if python.is_py3k or false
Expand Down Expand Up @@ -32,13 +32,13 @@ if (isPyPy) then throw "pycairo not supported for interpreter ${python.executabl
cd $(${python.executable} waf unpack)
pwd
patch -p1 < ${patch_waf}
${stdenv.lib.optionalString isPy35 "patch -p1 < ${patch_waf-py3_5}"}
${lib.optionalString isPy35 "patch -p1 < ${patch_waf-py3_5}"}
)

${python.executable} waf configure --prefix=$out
'';
buildPhase = "${python.executable} waf";
installPhase = "${python.executable} waf install";

meta.platforms = stdenv.lib.platforms.linux ++ stdenv.lib.platforms.darwin;
meta.platforms = lib.platforms.linux ++ lib.platforms.darwin;
}
Loading