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] Port switch-to-configuration script to Python #48378

Closed
wants to merge 5 commits into from

Conversation

bgamari
Copy link
Contributor

@bgamari bgamari commented Oct 14, 2018

Motivation for this change

Note that this has yet to be tested. Rather I am just posting this as a placeholder.

This is a port of the switch-to-configuration script from Perl to Python. The motivation for this change is to ease cross-compilation, which is documented to be quite difficult in the case of Perl (see #36675).

One tricky issue that this raises is the lack of sensible dbus bindings for Python. Each of the options has its share of issues:

  • dbus-python is largely unmaintained and binds the libdbus implementation, use of which is somewhat actively discouraged
  • pydbus uses gobject-introspection, which sadly appears to be very unfriendly to cross-compilation
  • QtDBus uses Qt, which is rather heavy-weight for this purpose.

Ideally I would like a small binding for sdbus but, that not being available, I have simply used dbus-python here.

Things done
  • Tested using sandboxing (nix.useSandbox on NixOS, or option sandbox in nix.conf on non-NixOS)
  • Built on platform(s)
    • NixOS
    • macOS
    • other Linux distributions
  • Tested via one or more NixOS test(s) if existing and applicable for the change (look inside nixos/tests)
  • Tested compilation of all pkgs that depend on this change using nix-shell -p nox --run "nox-review wip"
  • Tested execution of all binary files (usually in ./result/bin/)
  • Determined the impact on package closure size (by running nix path-info -S before and after)
  • Fits CONTRIBUTING.md.

@bgamari
Copy link
Contributor Author

bgamari commented Oct 14, 2018

CCing @shlevy @Ericson2314

Copy link
Member

@FRidh FRidh left a comment

Choose a reason for hiding this comment

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

Move the main code flow into main() and add

if __name__ == "__main__": 
    main()

import statements should all be at the top. Please also add docstrings.

restartListFile = "/run/systemd/restart-list"
reloadListFile = "/run/systemd/reload-list"

def die_with_usage():
Copy link
Member

Choose a reason for hiding this comment

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

Would you mind to type annotations, then changing this file becomes easier for type checking with mypy.
I can also add them if you want.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was very tempted to add type annotations when I started but wasn't quite sure whether we would want to retain Python 2 support so (mostly) refrained. I would be happy to add them.

Copy link
Member

Choose a reason for hiding this comment

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

No, we definitely don't want more legacy python2 code.

reloadListFile = "/run/systemd/reload-list"

def die_with_usage():
print("""
Copy link
Member

Choose a reason for hiding this comment

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

consider a function error which calls print with file=stderr
https://stackoverflow.com/questions/5574702/how-to-print-to-stderr-in-python

else:
return otherwise

if len(sys.argv) < 1:
Copy link
Member

Choose a reason for hiding this comment

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

use argparse for building the parser. Call it from main().

out = "@out@"

# To be robust against interruption, record what units need to be started etc.
startListFile = "/run/systemd/start-list"
Copy link
Member

Choose a reason for hiding this comment

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

constants should be in uppercase


# Install or update the bootloader.
if action == "switch" or action == "boot":
subprocess.checkCall(["@installBootLoader@", out])
Copy link
Member

Choose a reason for hiding this comment

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

check_call


# Just in case the new configuration hangs the system, do a sync now.
if os.environ.get('NIXOS_NO_SYNC') != "1":
subprocess.checkCall(["@coreutils@/bin/sync", "-f", "/nix/store"])
Copy link
Member

Choose a reason for hiding this comment

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

check_call

# absolute path as well.
def fingerprint_unit(filename):
fprint = os.path.abspath(filename)
override = '%s.d/overrides.conf' % filename
Copy link
Member

Choose a reason for hiding this comment

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

use .format

Copy link
Member

Choose a reason for hiding this comment

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

or python3.6 format strings

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Are we ready to accept a >= Python 3.6 as a requirement for NixOS?

Copy link
Member

Choose a reason for hiding this comment

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

Yes. python36 is the default python3, and soon it will be python37.

if not os.path.exists(prev_unit_file) \
and not os.path.exists(new_unit_file) \
and m is not None
baseUnit = "%s@%s" % (m.group(1), m.group(1))
Copy link
Member

Choose a reason for hiding this comment

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

underscores

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch!

transitions = UnitTransitions()
active_prev = get_active_units()

for unit, state in active_prev.items():
Copy link
Member

Choose a reason for hiding this comment

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

I think what's below this for loop should be in a separate function

prev_exists = os.path.exists(prev_unit_file)
new_exists = os.path.exists(new_unit_file)
if prev_exists and state['state'] in ['active', 'activating']:
if (not new_exists) or os.path.abspath(new_unit_file) == "/dev/null":
Copy link
Member

Choose a reason for hiding this comment

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

too much nested. Split up in functions.

output = subprocess.check_output(args)
return output.strip()

def unique(*args):
Copy link
Member

Choose a reason for hiding this comment

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

unused

if len(sys.argv) < 1:
die_with_usage()

action = sys.argv[1]
Copy link
Member

Choose a reason for hiding this comment

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

Can you add those lines in a one main function instead of mixing functions and code?

@FRidh
Copy link
Member

FRidh commented Oct 14, 2018

@Mic92 I think we can add a tiny builder in trivial-builders for building a Python script, that is also being checked with mypy and pylint. We may want to have the option for disabling those checks.

@Mic92
Copy link
Member

Mic92 commented Oct 14, 2018

There is also pystemd: https://github.com/facebookincubator/pystemd that wraps around libsystemd,
but I don't know what the story of cross-compiling cython is at the moment. It should be lightweight at least.
cc @flokli #47517

@Mic92
Copy link
Member

Mic92 commented Oct 14, 2018

@Mic92
Copy link
Member

Mic92 commented Oct 14, 2018

I also started rewriting setup-etc.pl at some point, but I think I lost the source code for that.


if reload_if_changed:
transitions.reload_unit(unit)
elif !restart_if_changed or refuse_manual_stop:
Copy link
Member

Choose a reason for hiding this comment

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

This is not valid python syntax.

elif !restart_if_changed or refuse_manual_stop:
transitions.skip_unit(unit)
else:
if !stop_if_changed:
Copy link
Member

Choose a reason for hiding this comment

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

also this one should be not.

@arianvp
Copy link
Member

arianvp commented Oct 14, 2018

Pystemd would indeed be very nice for this usecase. @flokli and I have been experimenting with it and it seemed to work really well. I'll have a look at Cython's cross-compilation story. Because I'm not sure either.

@jtojnar
Copy link
Contributor

jtojnar commented Oct 14, 2018

What about using Jeepney for DBus? It is a pure Python library.

@bgamari
Copy link
Contributor Author

bgamari commented Oct 14, 2018

What about using Jeepney for DBus? It is a pure Python library.

I considered it but decided against it for now for concerns of correctness and verbosity. I'd be happy to go this direction if others think it best, however.

@bgamari
Copy link
Contributor Author

bgamari commented Oct 14, 2018

The current head typechecks with mypy.

print_err(msg)
sys.exit(1)

def die(why: str):
Copy link
Member

@Mic92 Mic92 Oct 14, 2018

Choose a reason for hiding this comment

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

Functions should also always have a return type also if it its None. Otherwise the type check is skipped if a function does not have any typed arguments.


# This is a NixOS installation if it has /etc/NIXOS or a proper
# /etc/os-release.
def is_nixos():
Copy link
Member

@Mic92 Mic92 Oct 14, 2018

Choose a reason for hiding this comment

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

def is_nixos() -> bool:

The build process relies on the glib-marshal utility.
Strangely, they were previously marked as nativeBuildInputs.
@bgamari
Copy link
Contributor Author

bgamari commented Oct 14, 2018

@Mic92, this now includes a port of setup-etc as well (although it might be wise to split this into a new PR)

@FRidh
Copy link
Member

FRidh commented Nov 1, 2018

FWIW, see #48611 regarding nfs-utils.

@bgamari
Copy link
Contributor Author

bgamari commented Nov 1, 2018

Alright, I will put this aside then. Thanks to everyone who reviewed!

@edolstra
Copy link
Member

edolstra commented Nov 1, 2018

The only Python script in NixOS is systemd-boot-builder.py, which kind of slipped in under the radar. I once started to rewrite it in Perl, but didn't finish it.

@edolstra
Copy link
Member

edolstra commented Nov 1, 2018

So that one script effectively increases closure sizes by 55 MiB.

@grahamc
Copy link
Member

grahamc commented Nov 1, 2018

@FRidh I just bisected to that very commit. Tough :/

@andrew-d
Copy link
Contributor

andrew-d commented Nov 1, 2018

@edolstra - OOC, is the primary concern closure size? Any thoughts on a more acceptable number (e.g. is a 5MB increase okay)?

@bgamari
Copy link
Contributor Author

bgamari commented Nov 1, 2018

@edolstra,j while I'm not attached to Python in particular, I really don't see the sense in using Perl over Python. If anything it seems to me that the residual Perl should be moved to Python, not the other way around. My reasons are three-fold:

  • Python has a sensible static analysis/typechecking story, which Perl (AFAIK) does lacks. I have been pretty impressed by how much mypy can catch.
  • Perl is, frankly, hell to cross-compile and very little can be done to fix this. The language and its ecosystem were simply developed with very little consideration for cross-compilation.
  • In 2018 it is much easier to find people to work in Python than Perl

A compiled language (Rust or C++) also would be fine here. Really, just anything but Perl.

@edolstra
Copy link
Member

edolstra commented Nov 1, 2018

@andrew-d No, the main concern is not doing gratuitous rewrites. The motivation (cross-compilation) is a weak one because NixOS does not support cross-compilation.

Also, I find Python a horrible language to maintain (worse than Perl, even). Rewriting NixOps in Python was the worst decision I ever made. Never again.

@edolstra
Copy link
Member

edolstra commented Nov 1, 2018

rust/go binaries would be even larger in size then scripts

A Rust binary would not be bigger than the 50+ MiB that Python adds to the closure size. All the current Perl scripts (switch-to-configuration.pl, setup-etc.pl, install-grub.pl, nixos-generate-config.pl, update-users-groups.pl, command-not-found.pl) could be replaced by a single Rust binary that shouldn't be bigger than a megabyte or two.

@bgamari
Copy link
Contributor Author

bgamari commented Nov 2, 2018

No, the main concern is not doing gratuitous rewrites. The motivation (cross-compilation) is a weak one because NixOS does not support cross-compilation.

I'm not sure what you mean by this; Both @Ericson2314 and I have put quite a bit of work into fixing cross-compilation in both Nixpkgs and NixOS. However, at this point I cross-compile NixOS routinely and use it to support a client project.

@Ericson2314
Copy link
Member

I give others credit for NixOS in particular :). But yeah as I understand it the only obstical remaining is Perl.

I don't love either but Python > Perl is also by far the more common opinion as others have said, and that should be taken into account.

I don't think Rust buys us much either. I'm a huge believer in static types, but if everything but the dbus part is shelling out, it's still fundamentally stringly typed. Hardening non-weakest links is a rather pointless exercise.

Still, if Rust is the best way for this to move forward, so be it. I haven't looked that much in the nixpkgs some of things, but in principle it should work very well.

@edolstra
Copy link
Member

edolstra commented Nov 5, 2018

Does that actually work at the moment? The shebang paths in switch-to-configuration and other scripts in 64-bit closures won't work on 32-bit systems either.

To be honest I don't think it's a use case that needs to be a hard requirement.

@Mic92
Copy link
Member

Mic92 commented Nov 28, 2018

Rust would be soon usable w.r.t to cross compiling: #50866

@zimbatm
Copy link
Member

zimbatm commented Nov 28, 2018

We can also rewrite those scripts in Bash, bash has a small closure size ;)

@Mic92
Copy link
Member

Mic92 commented Nov 29, 2018

They were in bash before, but where rewritten in perl because they were too slow according to the git history.

@bgamari
Copy link
Contributor Author

bgamari commented Dec 7, 2018

Concerning

It would be nice to rewrite stdenv builders from bash to perl

I would just like to repeat my previous point:

Perl is, frankly, hell to cross-compile and very little can be done to fix this. The language and its ecosystem were simply developed with very little consideration for cross-compilation.

I would hope that this disqualifies the language from any use in something as central as the stdenv builders.

@danbst
Copy link
Contributor

danbst commented Jan 2, 2019

@edolstra

Also, I find Python a horrible language to maintain (worse than Perl, even). Rewriting NixOps in Python was the worst decision I ever made. Never again.

This is unique experience. Can you write-up a separate post on "why"? Or is it more personal, so nothing to share?

@rnhmjoj
Copy link
Contributor

rnhmjoj commented Jan 7, 2021

This is unique experience. Can you write-up a separate post on "why"? Or is it more personal, so nothing to share?

If I had to guess, breaking changes in the standard library between major/minor releases.
Perl is really stable: you can take 25yo script and it will still run unchanged.

@kvtb
Copy link
Contributor

kvtb commented Jan 10, 2021

Besides increasing minimal system closure size, switching to python would increase number of packets required to cross-compile a system closure: Python + some libs + deps would need to be cross-compilable.

I nice idea I think is to rewrite switch-to-configuration in C++ and build as i686 static executable.
Although it might put the readability even beyond perl, it would:

  1. decrease minimal system closure size
  2. allow to nix-rebuild boot; reboot (and to nix-infect) x64_86 closures on top of i686 installations effectively upgrading them to 64-bit. Currently switch-to-configuration in x64_86 closure requires 64-bit perl which does not run on i686, so nix-rebuild boot; reboot works only in one direction (x64_86 -> i686 using 32-bit perl which runs on both). switch-to-configuration in C++ would enable both directions.

@kvtb
Copy link
Contributor

kvtb commented Jan 10, 2021

@edolstra

Also, I find Python a horrible language to maintain (worse than Perl, even). Rewriting NixOps in Python was the worst decision I ever made. Never again.

This is unique experience. Can you write-up a separate post on "why"? Or is it more personal, so nothing to share?

That raises questions how the decision on test-driver perl->python rewrite had been passed and why @edolstra did not vetoed it (I agree that as embed language python is terrible + having tab-language within Nix string literals with non-trivial indent stripping and escape sequences is awful)

@Mic92
Copy link
Member

Mic92 commented Jan 10, 2021

@edolstra

Also, I find Python a horrible language to maintain (worse than Perl, even). Rewriting NixOps in Python was the worst decision I ever made. Never again.

This is unique experience. Can you write-up a separate post on "why"? Or is it more personal, so nothing to share?

That raises questions how the decision on test-driver perl->python rewrite had been passed and why @edolstra did not vetoed it (I agree that as embed language python is terrible + having tab-language within Nix string literals with non-trivial indent stripping and escape sequences is awful)

Because not many contributors know perl anymore.

@flokli
Copy link
Contributor

flokli commented Jan 10, 2021

Also note perl is probably worse when it comes to cross-compilation than python.

@kvtb
Copy link
Contributor

kvtb commented Jan 10, 2021

Also note perl is probably worse when it comes to cross-compilation than python.

Yes, we depend on https://arsv.github.io/perl-cross/ which is not very reliable. The project is not very popular and they might stop supporting it at any moment. That's +1 for C++

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet