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

Python bindings: add Value type #17

Open
wants to merge 39 commits into
base: python-bindings
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
2c1d537
ci: Always run with sandbox, even on Darwin
infinisil Feb 17, 2023
ae07006
python: Initialise bindings from Pythonix
infinisil Feb 1, 2023
80e5b4d
python: Fixes for Nix changes
infinisil Feb 17, 2023
e3020bf
python: Integrate incremental and CI build
infinisil Feb 17, 2023
7751140
python: Fix for IFD
infinisil Feb 17, 2023
afa4916
python: Format using clang-format
infinisil Feb 17, 2023
26cb4f0
python: Rename pythonnix namespace to nix::python
infinisil Feb 17, 2023
36a74d1
python: Add exampleEnv to try out the bindings
infinisil Feb 17, 2023
e4df267
python: Install the bindings in hopefully the correct location
infinisil Feb 17, 2023
402a16d
WIP: Start writing documentation and example
infinisil Feb 17, 2023
47bd4a4
Remove .cache from .gitignore
infinisil Feb 24, 2023
3fab794
Always include config.h
infinisil Feb 24, 2023
2fce5fd
Release the GIL for Nix evaluation
infinisil Feb 24, 2023
e11273f
Don't release GIL lock because it would segfault
infinisil Feb 24, 2023
a10ae63
Allow getting exact throw error messages
infinisil Feb 24, 2023
e460074
Readd .cache to .gitignore
infinisil Feb 24, 2023
8a9d1fe
Add ThrownNixError subclass
infinisil Feb 27, 2023
6be7198
python: Add clang-tools to dev env
infinisil Mar 3, 2023
813cbc4
python: Properly release and reacquire GIL
infinisil Mar 3, 2023
fe03f3d
python: Detect stack overflow from recursive data structure
infinisil Mar 3, 2023
7e46ddb
Insert some TODO's
infinisil Mar 3, 2023
b69d933
python: Handle null's in expressions correctly
infinisil Mar 3, 2023
af8d911
python: Fix boolean to nix conversion
infinisil Mar 3, 2023
bb06d43
python: Add test for Null conversion
infinisil Mar 3, 2023
cd1442a
python: Use assertRaises for tests
infinisil Mar 3, 2023
1e76e28
python: We don't need to install the bindings into a subdirectly
infinisil Mar 9, 2023
724d1d8
Remove python from manual build again
infinisil Mar 9, 2023
56a0ab0
Very simple Nix source filtering
infinisil Mar 9, 2023
75696bd
Fix the buildPythonApplication example
infinisil Mar 9, 2023
b9dc4e4
Fix some unset variable problems with vars-and-functions.sh
infinisil Mar 9, 2023
090ee61
assertEquals -> assertEqual
infinisil Mar 9, 2023
22432b3
Alternate approach to calling init.sh
infinisil Mar 9, 2023
1df5ea8
Don't depend on all files for the python bindings
infinisil Mar 9, 2023
754e342
Make buildPythonApplication test work
infinisil Mar 9, 2023
29d3a70
Fix dev shell
infinisil Mar 10, 2023
e412e35
Add debugging capability to dev shell
infinisil Mar 10, 2023
c87a1b4
Convert from eval to callExprString API
infinisil Mar 10, 2023
7cc4ac2
python: Use global EvalState for now
roberth Mar 11, 2023
6e7eff4
python: Add nix.evalExprString() and value.toPythonStrict()
roberth Mar 11, 2023
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: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ jobs:
with:
fetch-depth: 0
- uses: cachix/install-nix-action@v19
with:
# The sandbox would be disabled by default on Darwin
extra_nix_config: "sandbox = true"
- run: echo CACHIX_NAME="$(echo $GITHUB_REPOSITORY-install-tests | tr "[A-Z]/" "[a-z]-")" >> $GITHUB_ENV
- uses: cachix/cachix-action@v12
if: needs.check_secrets.outputs.cachix == 'true'
Expand Down
19 changes: 18 additions & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,13 @@
name = "nix-${version}";
inherit version;

src = self;
src = builtins.path {
name = "source-nix";
path = ./.;
filter = path: type:
path != toString ./python
&& path != toString ./flake.nix;
};

VERSION_SUFFIX = versionSuffix;

Expand Down Expand Up @@ -410,6 +416,11 @@
postUnpack = "sourceRoot=$sourceRoot/perl";
});

passthru.python-bindings = final.callPackage ./python {
inherit self system;
python = final.python3;
};

meta.platforms = lib.platforms.unix;
};

Expand Down Expand Up @@ -465,6 +476,10 @@
# Perl bindings for various platforms.
perlBindings = forAllSystems (system: nixpkgsFor.${system}.native.nix.perl-bindings);

pythonBindings = nixpkgs.lib.genAttrs systems (system: self.packages.${system}.nix.python-bindings);
# TODO: recurseIntoAttrs or combine multiple tests into a single one
pythonBindingsTests = nixpkgs.lib.genAttrs systems (system: self.packages.${system}.nix.python-bindings.tests.example-buildPythonApplication);

# Binary tarball for various platforms, containing a Nix store
# with the closure of 'nix' package, and the second half of
# the installation script.
Expand Down Expand Up @@ -585,6 +600,7 @@
checks = forAllSystems (system: {
binaryTarball = self.hydraJobs.binaryTarball.${system};
perlBindings = self.hydraJobs.perlBindings.${system};
pythonBindings = self.hydraJobs.pythonBindings.${system};
installTests = self.hydraJobs.installTests.${system};
nixpkgsLibTests = self.hydraJobs.tests.nixpkgsLibTests.${system};
} // (lib.optionalAttrs (builtins.elem system linux64BitSystems)) {
Expand Down Expand Up @@ -665,6 +681,7 @@
(forAllCrossSystems (crossSystem: let pkgs = nixpkgsFor.${system}.cross.${crossSystem}; in makeShell pkgs pkgs.stdenv)) //
{
default = self.devShells.${system}.native-stdenvPackages;
python = self.packages.${system}.nix.python-bindings.shell;
}
);
};
Expand Down
3 changes: 3 additions & 0 deletions python/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
build
# For clang-tools
.cache
49 changes: 49 additions & 0 deletions python/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# This Makefile is only used for development of the Python bindings, it is not
# used in the Nix build.
# The reason this exists is to make it easier to develop the Python bindings in
# tandem with the main Nix.
# The default `make` (defaults to `make build`) calls the main Nix projects
# `make install` before calling the Python bindings' `meson compile`, therefore
# ensuring that the needed Nix dynamic libraries are up-to-date

builddir=build

.PHONY: build
build: nix-install setup-done
meson compile -C $(builddir)

.PHONY: test
test: nix-install setup-done
meson test -C $(builddir) -v

.PHONY: clean
clean:
rm -rf $(builddir)

# We include the main Nix projects Makefile.config file to know the $(libdir)
# variable, which is where Nix is installed in, which we can then use to setup
# the meson build
include ../Makefile.config

# We need the file to exist though
../Makefile.config:
@# Throw a good error message in case ./configure hasn't been run yet
@[[ -e ../config.status ]] || ( echo "The main Nix project needs to be configured first, see https://nixos.org/manual/nix/stable/contributing/hacking.html" && exit 1 )
@# If ./configure is done, we can create the file ourselves
$(MAKE) -C .. Makefile.config

.PHONY: setup
setup: nix-install
@# Make meson be able to find the locally-installed Nix
PKG_CONFIG_PATH=$(libdir)/pkgconfig:$$PKG_CONFIG_PATH meson setup $(builddir)

.PHONY: setup-done
setup-done:
@# A better error message in case the build directory doesn't exist yet
@[[ -e $(builddir) ]] || ( echo "Run 'make setup' once to configure the project build directory" && exit 1 )

.PHONY: nix-install
nix-install:
@# The python bindings don't technically need an _entire_ Nix installation,
@# but it seems non-trivial to pick out only exactly the files it actually needs
$(MAKE) -C .. install
11 changes: 11 additions & 0 deletions python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Python Bindings

This directory contains experimental Python bindings to a small subset of Nix's functionality. These bindings are very fast since they link to the necessary dynamic libraries directly, without having to call the Nix CLI for every operation.

Originally these bindings were created by [@Mic92](https://github.com/Mic92) and called [Pythonix](https://github.com/Mic92/pythonix). However, after Nix changed the internal library interface multiple times, therefore requiring Pythonix to be updated, it wasn't worth the third-party time investment anymore and the Pythonix repository was archived. At that point it only worked for Nix versions below 2.4. By having these bindings upstream, they can be updated right along the C++ code that defines the library interface.

## Documentation

See [index.md](./doc/index.md), which is also rendered in the HTML manual.
Copy link
Member

Choose a reason for hiding this comment

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

I may be missing something but I don't see where it's rendered in the manual.


To hack on these bindings, see [hacking.md](./doc/hacking.md), also rendered in the HTML manual.
Copy link
Member

Choose a reason for hiding this comment

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

This seems empty though. Let's not reference things that don't exist, or create those things first.

98 changes: 98 additions & 0 deletions python/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
{
self,
system,
lib,
python,
boost,
gdb,
clang-tools,
pkg-config,
ninja,
meson,
nix,
mkShell,
enableDebugging,
recurseIntoAttrs,
isShell ? false,
}:
let
_python = python;
in
let
python = _python.override { self = enableDebugging _python; };
# Extracts tests/init.sh
testScripts = nix.overrideAttrs (old: {
name = "nix-test-scripts-${old.version}";
outputs = [ "out" ];
separateDebugInfo = false;
buildPhase = ''
make tests/{init.sh,common/vars-and-functions.sh}
'';
script = ''
pushd ${placeholder "out"}/libexec >/dev/null
source init.sh
popd >/dev/null
'';
passAsFile = [ "script" ];
installPhase = ''
rm -rf "$out"
mkdir -p "$out"/{libexec/common,share/bash}
cp tests/init.sh "$out"/libexec
cp tests/common/vars-and-functions.sh "$out"/libexec/common

cp "$scriptPath" "$out"/share/bash/nix-test.sh
'';
dontFixup = true;
});
in
python.pkgs.buildPythonPackage {
name = "nix";
format = "other";

src = builtins.path {
path = ./.;
filter = path: type:
path == toString ./meson.build
|| path == toString ./tests.py
|| path == toString ./test.sh
|| lib.hasPrefix (toString ./src) path;
};


strictDeps = true;

nativeBuildInputs = [
ninja
pkg-config
(meson.override { python3 = python; })
] ++ lib.optional (!isShell) nix;

buildInputs = nix.propagatedBuildInputs ++ [
boost
] ++ lib.optional (!isShell) nix;

mesonBuildType = "release";

doInstallCheck = true;
TEST_SCRIPTS = testScripts;
installCheckPhase = "meson test -v";

passthru = {
exampleEnv = python.withPackages (p: [ nix.python-bindings ]);
tests = {
example-buildPythonApplication = import ./examples/buildPythonApplication {
inherit nix system testScripts;
};
};
shell = mkShell {
packages = [
clang-tools
gdb
];
TEST_SCRIPTS = testScripts;
inputsFrom = [
(nix.python-bindings.override { isShell = true; })
];
};
};
}
3 changes: 3 additions & 0 deletions python/doc/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Experimental Python Bindings API

This is the API!
3 changes: 3 additions & 0 deletions python/doc/hacking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Python Bindings Hacking

This is how to hack on the bindings
32 changes: 32 additions & 0 deletions python/doc/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Experimental Python Bindings

Nix comes with minimal experimental Python bindings that link directly to the necessary dynamic libraries, making them very fast.

## Trying it out

The easiest way to try out the bindings is using the provided example environment:

```
$ nix run github:NixOS/nix#nix.python-bindings.exampleEnv
Python 3.10.8 (main, Oct 11 2022, 11:35:05) [GCC 11.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import nix
>>> nix.eval('"Hello ${name}!"', vars=dict(name="Python"))
'Hello Python!'
```

For the available functions and their interfaces, see the API section.

## Build integration

In the future these Python bindings will be available from Nixpkgs as `python3Packages.nix`.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
In the future these Python bindings will be available from Nixpkgs as `python3Packages.nix`.

Let's not talk about the future here.


Until then the Python bindings are only available from the Nix derivation via the `python-bindings` [passthru attribute](https://nixos.org/manual/nixpkgs/stable/#var-stdenv-passthru). Without any modifications, this derivation is built for the default Python 3 version from the Nixpkgs version used to build Nix. This Python version might not match the Python version of the project you're trying to use them in. Therefore it is recommended to override the bindings with the correct Python version using

```
nix.python-bindings.override {
python = myPythonVersion;
}
```

For complete examples, see https://github.com/NixOS/nix/tree/master/python/examples
37 changes: 37 additions & 0 deletions python/examples/buildPythonApplication/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
system ? builtins.currentSystem,
pkgs ? import (fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/545c7a31e5dedea4a6d372712a18e00ce097d462.tar.gz";
sha256 = "1dbsi2ccq8x0hyl8n0hisigj8q19amvj9irzfbgy4b3szb6x2y6l";
}) {
config = {};
overlays = [];
inherit system;
},
nix ? (import ../../..).default,
testScripts,
}:
let
python = pkgs.python3;
nixBindings = nix.python-bindings.override { inherit python; };
in python.pkgs.buildPythonApplication {
pname = "hello-nix";
version = "0.1";
src = builtins.path {
path = ./.;
filter = path: type:
pkgs.lib.hasPrefix (toString ./hello) path
|| path == toString ./setup.py;
};
propagatedBuildInputs = [ nixBindings ];
doInstallCheck = true;
nativeCheckInputs = [
nix
];
installCheckPhase = ''
(
source ${testScripts}/share/bash/nix-test.sh
$out/bin/hello-nix
)
'';
}
4 changes: 4 additions & 0 deletions python/examples/buildPythonApplication/hello/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import nix

def greet():
print("Evaluating 1 + 1 in Nix gives: " + str(nix.eval("1 + 1")))
12 changes: 12 additions & 0 deletions python/examples/buildPythonApplication/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from setuptools import setup, find_packages

setup(
name="hello-nix",
version="0.1",
packages=find_packages(),
entry_points={
'console_scripts': [
'hello-nix = hello:greet',
],
},
)
26 changes: 26 additions & 0 deletions python/include/nix/python/value.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#pragma once

#define PY_SSIZE_T_CLEAN
#include <Python.h>

#include "eval.hh"

namespace nix::python {

typedef struct {
PyObject_HEAD
// TODO: (performance) flatten this pointer, while making sure
// initialization is correct,
// or allocate these python objects with boehmgc
nix::RootValue *value;

inline nix::RootValue & operator *() {
return *value;
}
} ValuePyObject;

extern PyTypeObject ValuePyType;

ValuePyObject *allocValuePyObject(nix::EvalState * state);

}
18 changes: 18 additions & 0 deletions python/meson.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
project('python-nix', 'cpp',
version : '0.1.8',
license : 'LGPL-2.0',
)

python_mod = import('python')
py_installation = python_mod.find_installation()

nix_expr_dep = dependency('nix-expr', required: true)
nix_main_dep = dependency('nix-main', required: true)

subdir('src')

fs = import('fs')

env = environment()
env.prepend('PYTHONPATH', fs.parent(nix_bindings.full_path()))
test('python test', find_program('bash'), args : files('test.sh'), env : env)
Loading