Skip to content

Latest commit

 

History

History
302 lines (239 loc) · 10.6 KB

HELLO.md

File metadata and controls

302 lines (239 loc) · 10.6 KB

GNU Hello demo

Complete source of the demo

Pylightnix is a library for manipulating immutable data objects. It provides core API for checking, creating and querying such objects using files and folders as a data sotrage. One kind of applications which could benefit from this API is package managers.

The task

We illustrate the basic concepts by designing a toy package manager able to compile and run the GNU hello program.

GNU Hello is a demo application which prints ‘Hello world!’ on its standard output. It’s purpose is to demonstrate the usage of Automake tools. We will see how Pylighnix could help us to solve this quest. We assume that the host system provides an access to the GNU Automake toolchain, that is, paths to Autoconf, Automake, gcc, etc should present in system PATH variable.

We go through the following plan of actions:

  1. Use built-in rules to download and unpack the GNU Hello sources.
  2. Define a custom rule for compiling the GNU Hello from sources.
  3. Run the application by querying the Pylightnix artifact storage.

Implementation

Pylightnix offers functions which form a kind of domain-specific language, embedded in Python language. Our program is a Python script, which could be executed by a common python3 interpreter. In this demo we will need certain standard Python functions. Later we will import Pylightnix functions as needed.

from os.path import join
from os import system, chdir, getcwd
from shutil import copytree
from tempfile import TemporaryDirectory
from typing import Any
from subprocess import Popen, PIPE

First things first, Pylightnix uses filesystem for tracking immutable data objects which could depend on each other. Objects reside partly in the filesystem, partly in memory as a Python objects. We initialize the filesystem part by calling fsinit on StorageSettings and then create the global Registry. The Registry and the part of filesystem described by StorageSettings are the only mutable objects that we will operate on.

from os import environ
from pylightnix import Registry, StorageSettings, mkSS, fsinit

S:StorageSettings=mkSS('/tmp/pylightnix_hello_demo')
fsinit(S,remove_existing=True)
R=Registry(S)

hello_version = '2.10'

Fetchurl and Unpack stages

Pylightnix manages data processing operations called Derivations. The toolbox provides a generic constructor mkdrv and a set of pre-defined Stages which wraps it with problem-specific parameters.

In this tutorial we will need fetchurl2 and unpack stages. The first one knows how to download URLs from the Internet, the second one knows how to unpack archives. We import both functions, along with other Pylightnix APIs.

from pylightnix import (Registry, DRef, RRef, fetchurl2, unpack, mklens, selfref)

Our first goal is to make derivations ready for realization by registering them in the Registry R. We call fetchurl2 with the appropriate parameters and get an unique DRef reference back. Every DRef value proofs to us that the Registry is aware of our new derivation.

tarball:DRef = fetchurl2(
    name='hello-src',
    url=f'http://ftp.gnu.org/gnu/hello/hello-{hello_version}.tar.gz',
    sha256='31e066137a962676e89f69d1b65382de95a7ef7d914b8cb956f41ea72e0f516b',
    out=[selfref, f'hello-{hello_version}.tar.gz'], r=R)

In order to link derivations into a chain we should put the derivation reference of a prerequisite derivation somewhere into the config of a child derivaiton.

hello_src:DRef = unpack(
    name='unpack-hello',
    refpath=mklens(tarball,r=R).out.refpath,
    aunpack_args=['-q'],
    src=[selfref, f'hello-{hello_version}'],r=R)

The selfref path is our promise to Pylightnix that the said path would appear after the derivation is realized. Pylightnix checks such promises and raises in case they are not fulfilled.

Now we are done with registrations and going to obtain our objects. We call instantiate to compute the dependency closure of the target object and pass it to realize1 which runs the show. As a result, we obtain a reference of another kind RRef

from pylightnix import instantiate, realize1
hello_rref:RRef = realize1(instantiate(hello_src, r=R))
print(hello_rref)
rref:29eaa2c8e74cbc939dfdd8e43f3987eb-43323fae07b9e30f65ed0a1b6213b6f0-unpack-hello
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed

  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
 25  708k   25  180k    0     0   208k      0  0:00:03 --:--:--  0:00:03  208k
100  708k  100  708k    0     0   465k      0  0:00:01  0:00:01 --:--:--  465k
hello-2.10.tar.gz: extracted to `hello-2.10'

RRefs could be converted to system paths by calling an mklens the swiss-army-knife data accessor of Pylightnix:

from pylightnix import current_storage

with current_storage(S):
  print(mklens(hello_rref).val)
  print(mklens(hello_rref).syspath)
  print(mklens(hello_rref).src.syspath)
rref:29eaa2c8e74cbc939dfdd8e43f3987eb-43323fae07b9e30f65ed0a1b6213b6f0-unpack-hello
/tmp/pylightnix_hello_demo/store-v0/43323fae07b9e30f65ed0a1b6213b6f0-unpack-hello/29eaa2c8e74cbc939dfdd8e43f3987eb
/tmp/pylightnix_hello_demo/store-v0/43323fae07b9e30f65ed0a1b6213b6f0-unpack-hello/29eaa2c8e74cbc939dfdd8e43f3987eb/hello-2.10

Pylightnix offers a number of other shell-like helper functions for accessing realization, like lsref:

from pylightnix import lsref, catref

print(lsref(hello_rref, S))
['__buildstop__.txt', '__buildstart__.txt', 'hello-2.10', 'context.json']

A custom compile stage

In this section we define a custom stage to build the newly obtained sources of GNU Hello application.

Defining Pylighnix stages requires us to provide Pylightnix with the following components:

  • The JSON-like configuration object.
  • The matcher Python function, dealing with non-determenistic builds.
  • The realizer Python function which specifies the actual build process.

The matcher business is beyond the scope of this tutorial. We will use a trivial match_only matcher which instructs Pylightnix to expect no more than one realization of a stage in its storage.

We produce a Config object by reading local variables of a helper function hello_config. We could have just call mkconfig on a dict.

from pylightnix import Config, mkconfig, mklens, selfref

def hello_config()->Config:
  name = 'hello-bin'
  src = mklens(hello_src,r=R).src.refpath
  out_hello = [selfref, 'usr', 'bin', 'hello']
  out_log = [selfref, 'build.log']
  return mkconfig(locals())

To specify Realizer we write another Python function which accepts the Build context. We use mklens to query the parameters of the derivation being built just as we used it for querying parameters of completed realizations. The selfref paths is initialized to paths inside the build temporary folder where we must put the build artifacts. Here we produce the GNU hello binary and a build log as a side-product.

from pylightnix import (Path, Build, build_cattrs, build_outpath, build_path,
                        dirrw )

def hello_realize(b:Build)->None:
  with TemporaryDirectory() as tmp:
    copytree(mklens(b).src.syspath,join(tmp,'src'))
    dirrw(Path(join(tmp,'src')))
    cwd = getcwd()
    try:
      chdir(join(tmp,'src'))
      system(f'( ./configure --prefix=/usr && '
             f'  make &&'
             f'  make install DESTDIR={mklens(b).syspath}'
             f')>{mklens(b).out_log.syspath} 2>&1')
    finally:
      chdir(cwd)

Finally, we introduce a new stage to Pylightnix by instantiating a generic mkdrv stage:

from pylightnix import mkdrv, build_wrapper, match_only

hello:DRef = mkdrv(hello_config(),match_only(),build_wrapper(hello_realize),R)

print(hello)
dref:e48878b9f7760fe0972eb6863775045f-hello-bin

As before, we get a DRef promise pass it through instantiate and realize1 pipeline:

rref:RRef=realize1(instantiate(hello,r=R))
print(rref)
rref:b02efb753dbd11e37d4e1c5d77eceb52-e48878b9f7760fe0972eb6863775045f-hello-bin

Accessing the results

Lets print the last few lines of the build log:

for line in open(mklens(rref,r=R).out_log.syspath).readlines()[-10:]:
  print(line.strip())
fi
/nix/store/x0jla3hpxrwz76hy9yckg1iyc9hns81k-coreutils-8.31/bin/mkdir -p '/tmp/pylightnix_hello_demo/tmp/210906-00:33:57:724316+0300_2b29fe60_35aw7mg4/usr/share/info'
/nix/store/x0jla3hpxrwz76hy9yckg1iyc9hns81k-coreutils-8.31/bin/install -c -m 644 ./doc/hello.info '/tmp/pylightnix_hello_demo/tmp/210906-00:33:57:724316+0300_2b29fe60_35aw7mg4/usr/share/info'
install-info --info-dir='/tmp/pylightnix_hello_demo/tmp/210906-00:33:57:724316+0300_2b29fe60_35aw7mg4/usr/share/info' '/tmp/pylightnix_hello_demo/tmp/210906-00:33:57:724316+0300_2b29fe60_35aw7mg4/usr/share/info/hello.info'
/nix/store/x0jla3hpxrwz76hy9yckg1iyc9hns81k-coreutils-8.31/bin/mkdir -p '/tmp/pylightnix_hello_demo/tmp/210906-00:33:57:724316+0300_2b29fe60_35aw7mg4/usr/share/man/man1'
/nix/store/x0jla3hpxrwz76hy9yckg1iyc9hns81k-coreutils-8.31/bin/install -c -m 644 hello.1 '/tmp/pylightnix_hello_demo/tmp/210906-00:33:57:724316+0300_2b29fe60_35aw7mg4/usr/share/man/man1'
make[4]: Leaving directory '/run/user/1000/tmp3yo1o2pv/src'
make[3]: Leaving directory '/run/user/1000/tmp3yo1o2pv/src'
make[2]: Leaving directory '/run/user/1000/tmp3yo1o2pv/src'
make[1]: выход из каталога «/run/user/1000/tmp3yo1o2pv/src»

Finally, we convert RRef to the system path and run the GNU Hello binary.

print(Popen([mklens(rref,r=R).out_hello.syspath],
            stdout=PIPE, shell=True).stdout.read()) # type:ignore
b'Hello, world!\n'