[Buildroot] [RFC v2 1/6] common service startup files

Alex Suykov alex.suykov at gmail.com
Sun Apr 5 22:01:50 UTC 2015


This patch introduces support infrastructure for per-project .run
files containing the information needed to generate either sysv
intiscripts or systemd services in a common format.

Packages are expected to set $(PKG)_INIT_RUN = list of .run files
to install in their .mk files, allowing pkg-generic to call
support/init/install with appropriate arguments.

Alternatively, packages may define $(PKG)_INSTALL_INIT, calling
support/init/install themselves or doing something more elaborate.
Packages may also define $(PKG)_INSTALL_INIT_SYSV or _SYSTEMD,
overriding non-specific _INSTALL_INIT.

The format of the .run files is simple set of "key: value" lines,
reflecting the structure of the data while also being somewhat
readable.

Explicit $(PKG)_INIT_RUN in every .mk file provides clues for unaware
users regarding the purpose of otherwise unremarkable .run files.
INIT_RUN string is grepable.

Signed-off-by: Alex Suykov <alex.suykov at gmail.com>
---
 package/pkg-generic.mk |  26 +++-
 package/pkg-utils.mk   |   4 +
 support/init/install   | 349 +++++++++++++++++++++++++++++++++++++++++++++++++
 system/Config.in       |   6 +
 4 files changed, 381 insertions(+), 4 deletions(-)
 create mode 100755 support/init/install

diff --git a/package/pkg-generic.mk b/package/pkg-generic.mk
index 0d95541..833f9b5 100644
--- a/package/pkg-generic.mk
+++ b/package/pkg-generic.mk
@@ -235,10 +235,7 @@ $(BUILD_DIR)/%/.stamp_target_installed:
 	@$(call MESSAGE,"Installing to target")
 	$(foreach hook,$($(PKG)_PRE_INSTALL_TARGET_HOOKS),$(call $(hook))$(sep))
 	+$($(PKG)_INSTALL_TARGET_CMDS)
-	$(if $(BR2_INIT_SYSTEMD),\
-		$($(PKG)_INSTALL_INIT_SYSTEMD))
-	$(if $(BR2_INIT_SYSV)$(BR2_INIT_BUSYBOX),\
-		$($(PKG)_INSTALL_INIT_SYSV))
+	+$($(PKG)_INSTALL_INIT_$(INIT))
 	$(foreach hook,$($(PKG)_POST_INSTALL_TARGET_HOOKS),$(call $(hook))$(sep))
 	$(Q)if test -n "$($(PKG)_CONFIG_SCRIPTS)" ; then \
 		$(RM) -f $(addprefix $(TARGET_DIR)/usr/bin/,$($(PKG)_CONFIG_SCRIPTS)) ; \
@@ -763,6 +760,27 @@ ifneq ($$(call suitable-extractor,$$($(2)_SOURCE)),$$(XZCAT))
 DL_TOOLS_DEPENDENCIES += $$(firstword $$(call suitable-extractor,$$($(2)_SOURCE)))
 endif
 
+ifeq ($(4),target)
+
+ifndef $(3)_INSTALL_INIT
+ ifdef $(3)_INIT_RUN
+  define $(3)_INSTALL_INIT
+	support/init/install $(BR2_INIT) $(TARGET_DIR) \
+		$$(addprefix $(pkgdir),$$($(3)_INIT_RUN))
+  endef
+ endif
+endif
+
+# Init-specific rules override generic INSTALL_INIT
+ifndef $(3)_INSTALL_INIT_$(INIT)
+ ifdef $(3)_INSTALL_INIT
+  $(3)_INSTALL_INIT_$(INIT) = $$($(3)_INSTALL_INIT)
+ endif
+endif
+
+endif # $(4) == target
+
+
 endif # $(2)_KCONFIG_VAR
 endef # inner-generic-package
 
diff --git a/package/pkg-utils.mk b/package/pkg-utils.mk
index 7eddc47..057a693 100644
--- a/package/pkg-utils.mk
+++ b/package/pkg-utils.mk
@@ -76,6 +76,10 @@ INFLATE.tar  = cat
 # suitable-extractor(filename): returns extractor based on suffix
 suitable-extractor = $(INFLATE$(suffix $(1)))
 
+# Uppercase init name suitable for use in variables
+# BR2_INIT="sysv" INIT=SYSV
+INIT = $(call UPPERCASE,$(call qstrip,$(BR2_INIT)))
+
 # MESSAGE Macro -- display a message in bold type
 MESSAGE = echo "$(TERM_BOLD)>>> $($(PKG)_NAME) $($(PKG)_VERSION) $(call qstrip,$(1))$(TERM_RESET)"
 TERM_BOLD := $(shell tput smso)
diff --git a/support/init/install b/support/init/install
new file mode 100755
index 0000000..f5f21de
--- /dev/null
+++ b/support/init/install
@@ -0,0 +1,349 @@
+#!/usr/bin/env python
+
+# Usage:
+#
+#   support/init/install init-system target-directory file1.run file2.run ...
+#
+# Each .run file describes a single daemon to be spawned on system startup.
+# This script turns .run into relevant files for init-system and writes
+# those to target-directory.
+#
+# Expected format of the .run files:
+#
+#   # comment
+#   description: Some sample daemon
+#   umask: 077
+#   foreground: /sbin/daemon -A -b 7000
+#
+# The first word is the key, the rest is value. See Run.__init__ below
+# for the list of possible keys.
+#
+# Most daemons should only need foreground: line, which is the command
+# to start it in foreground (non-forking) mode.
+
+# Typical test run:
+#
+#   .../support/init/install sysv . foo.run
+#
+# that should create ./etc/init.d/S50foo or something similar.
+
+# Bundling all init systems into a single script, vs having install-init-(system)
+# scripts for each system, may look strange, but it turns out large parts of the
+# code below are common for all init systems.
+
+import re
+import os
+import sys
+
+class Run:
+    class ParseError(Exception): pass
+    class KeywordError(Exception): pass
+    class ConfigError(Exception): pass
+
+    def __init__(self, path):
+        self.name = None
+        self.path = None
+
+        # possible keywords in the .run file
+        self.attr = {
+            # process properties
+            'pidfile': None,        # if set, assume the daemon writes it
+            'umask': None,
+            'user': None,           # effective uid to run the daemon as
+            'group': None,          # effecitve gid
+            # non-process info
+            'description': None,    # systemd, and comment in initscripts
+            'after': None,          # systemd only
+            'priority': None,       # sysv only
+            # commands to run
+            'prestart': [ ],        # commands to run before starting the daemon
+            'foreground': None,     # command to start the daemon in fg mode
+            'background': None,     # same, in background mode
+                                    # (the daemon is assumed to fork)
+
+            'start': None,          # sysv-specific, skips start-stop-daemon
+            'stop': None,           # sysv-specific, skips start-stop-daemon
+            'reload': None          # both sysv and systemd can use this
+        }
+
+        # constructing Run from a file means parsing that file
+        if path: self.parse(path)
+
+    # [] to access .attr dictionary
+    def __setitem__(self, key, val):
+        if key not in self.attr:
+            raise Run.KeywordError(key)
+        if type(self.attr[key]) is list:
+            self.attr[key].append(val)
+        else:
+            self.attr[key] = val
+
+    def __getitem__(self, key):
+        return self.attr[key]
+
+    # Input file has simple "key any-string-value" structure
+    # for all significant lines, possibly also comments and empty
+    # lines. This reads it into attr[].
+    def parse(self, path):
+        self.path = path
+        self.name = re.sub(r'\..*', '', os.path.basename(path))
+        with open(path) as fh:
+            for lnum, line in enumerate(fh, 1):
+                line = line.strip()
+
+                # skip comments and empty lines
+                if line[0] == '#' or not re.match(r'\S', line):
+                    continue
+
+                m = re.match(r'^([a-z]+):\s+(.*)', line)
+                if not m:
+                    raise Run.ParseError(path, lnum, 'bad line format')
+                try:
+                    self[m.group(1)] = m.group(2)
+                except Run.KeywordError as e:
+                    raise Run.ParseError(path, lnum, 'unknown keyword %s' % e.args[0])
+
+def die(message, *args):
+    sys.stderr.write((message+"\n") % tuple(args))
+    exit(-1)
+
+# Directory structure under $TARGET_DIR is init-specific
+# and may not be there by the time init/install gets called.
+# mode is 0644 for systemd services but 0755 for initscripts.
+def openmode(path, mode):
+    pathdir = os.path.dirname(path)
+    if not os.path.isdir(pathdir):
+        os.makedirs(pathdir)
+    fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC)
+    os.fchmod(fd, mode)
+    return os.fdopen(fd, 'w')
+
+# ------------------------------------------------------------------------------
+
+# Sysv initscripts are expected to spawn background daemons, tracking them
+# via pid files. In case background line was supplied, it will be used,
+# otherwise start-stop-daemon will use foreground command and fork/daemonize
+# the process itself.
+#
+# Pre-start commands naturally go to the start) block.
+
+class Initscript(Run):
+    ssd = '/sbin/start-stop-daemon'
+
+    def write(self, targetdir):
+        scriptname = '%s/etc/init.d/S%s%s' % (targetdir, self.priority(), self.name)
+        script = openmode(scriptname, 0o755)
+
+        def caseblock(patt, lines):
+            script.write("\t%s)\n" % patt)
+            for line in lines:
+                if not line is None:
+                    script.write("\t\t%s\n" % line)
+            script.write("\t\t;;\n")
+
+        script.write("#!/bin/sh\n\n")
+        if self['description']:
+            script.write("# %s\n\n" % self['description'])
+        script.write("case \"$1\" in\n")
+        caseblock('start', self.start())
+        caseblock('stop', self.stop())
+        # XXX: is it needed / does anyone use this?
+        if self['reload']:
+            caseblock('reload', [self['reload']])
+            caseblock('restart', ['$0 stop', 'sleep 1', '$0 start'])
+        else:
+            caseblock('reload|restart', ['$0 stop', 'sleep 1', '$0 start'])
+        script.write("esac\n")
+
+        script.close()
+
+    # Priority field (that 50 in S50foo) gets almost no use in buildroot
+    # since the stuff that needs strict order (mounts, swap and such)
+    # happens directly in inittab. For the services installed via .run files,
+    # the idea is to do sysinit first, then start daemons.
+    # For (rare) case that do not fit, allow specifying it explicitly.
+    def priority(self):
+        if self['priority']:
+            return self['priority']
+        else:
+            return '50'
+
+    def pidfile(self):
+        if self['pidfile']:
+            return self['pidfile']
+        else:
+            return'/var/run/%s' % self.name
+
+    def start(self):
+        if self['start']:
+            return [self['start']]
+
+        start = [ self.ssd, '-S' ]
+        if self['user']:
+            start.extend(['-c', self['user']])
+        if self['group']:
+            start.extend(['-g', self['group']])
+
+        start.extend(['-p', self.pidfile()])
+
+        if self['umask']:
+            start.extend(['-k', self['umask']])
+
+        # start-stop-daemon requires -- before command args
+        def dashdashargs(cmd):
+            return re.sub(r'\s+', ' -- ', cmd, 1)
+
+        # When running services in background mode, prefer background
+        # commands since they tend to be simplier (as in, an option
+        # required to prevent forking)
+        if self['background']:
+            start.append(dashdashargs(self['background']))
+        elif self['foreground']:
+            start.extend(['-m', '-b', dashdashargs(self['foreground'])])
+        else:
+            raise Run.ConfigError(self.path, "no process to start")
+
+        cmds = self['prestart'][:]
+        cmds.append(" ".join(start))
+
+        return cmds
+
+    def stop(self):
+        if self['stop']:
+            return [self['stop']]
+        else:
+            # do not bother checking executable name,
+            # it does not improve things much
+            return ["%s -K -p %s" % (self.ssd, self.pidfile())]
+
+# ------------------------------------------------------------------------------
+
+# Systemd .service files are essentially .run files with slightly different
+# key names and completely pointless sections.
+# Foreground command is preferred, but background can be used as well.
+
+class Service(Run):
+    sysdir = '/usr/lib/systemd/system'
+
+    def write(self, targetdir):
+        filename = '%s%s/%s.service' % (targetdir, self.sysdir, self.name)
+        service = openmode(filename, 0o644)
+
+        def writefield(tag, key = None):
+            if key is None:
+                key = tag.lower()
+            if(self[key]):
+                service.write("%s=%s\n" % (tag, self[key]))
+
+        service.write("[Unit]\n")
+        writefield('Description')
+        if self['after']:
+            service.write("After=%s\n" % self.after())
+
+        service.write("\n")
+        service.write("[Service]\n")
+        writefield('UMask')
+        writefield('User')
+        writefield('Group')
+
+        # not needed to control foreground services
+        if not self['foreground']:
+            writefield('PIDFile')
+
+        for cmd in self['prestart']:
+            service.write("ExecStartPre=%s\n" % cmd)
+
+        if self['foreground']:
+            # Type=simple is the default
+            service.write("ExecStart=%s\n" % self['foreground'])
+        elif self['background']:
+            service.write("Type=forking\n")
+            service.write("ExecStart=%s\n" % self['background'])
+        else:
+            raise Run.ConfigError(self.path, "no process to start")
+
+        writefield('ExecStop', 'stop')
+        writefield('ExecReload', 'reload')
+
+        # systemd does not object against including this with
+        # Type=forking and can apparently restart forking daemons
+        # in some cases.
+        service.write("Restart=always\n")
+
+        # systemd default is to setuid/setgid for prestart entries.
+        # That's the opposite of what initscripts do. For now, just
+        # force initscripts behavior and do not bother with possible
+        # su invocactions.
+        if self['prestart']:
+            service.write("PermissionsStartOnly=true\n")
+
+        # Buildroot does not use targets. Just boot into the default multi-user.
+        service.write("\n")
+        service.write("[Install]\n")
+        service.write("WantedBy=%s\n" % 'multi-user.target')
+
+        Service.enable(filename, 'multi-user.target')
+
+    # systemd needs dependency specification to order entries,
+    # and just throwing "syslog network audit" on every single
+    # entry does not look like a good solution (though it would
+    # likely work).
+    #
+    # To simplify things a bit, and to allow possible use for
+    # non-systemd inits, allow using short-hand notation like
+    # "syslog network" instead of "syslog.target network.target".
+    #
+    # Non-suffixed entries in After make no sense, so there is no
+    # problem in mixing shorthands and raw unit names.
+
+    shorthand = {
+            'network': 'network.target',
+            'syslog': 'syslog.target',
+            'auditd': 'auditd.service' }
+
+    def after(self):
+        after = [ ]
+        for unit in self['after'].split():
+            if unit in self.shorthand:
+                unit = self.shorthand[unit]
+            after.append(unit)
+        return " ".join(after)
+
+    # Mimic current BR systemd installation routines and create
+    # relevant symlinks to make the service "enabled".
+    #
+    # XXX: this should be in support/init/finalize, not here.
+
+    def enable(filename, wantedby):
+        base = os.path.basename(filename)
+        link = "%s/%s.wants/%s" % (os.path.dirname(filename), wantedby, base)
+        ldir = os.path.dirname(link)
+        if os.path.islink(link):
+            os.unlink(link)
+        if not os.path.isdir(ldir):
+            os.makedirs(ldir)
+        os.symlink("../" + base, link)
+
+
+# ------------------------------------------------------------------------------
+
+if len(sys.argv) < 2:
+    die("Usage: support/init/install init-system target-dir file.run file.run ...")
+
+try:
+    init = {
+    # init-system: parser-class
+        'sysv': Initscript,
+        'systemd': Service
+    }[sys.argv[1]]
+except KeyError:
+    die("Unknown init system %s", sys.argv[1])
+
+try:
+    for f in sys.argv[3:]:
+        if f.endswith('.run'):
+            init(f).write(sys.argv[2])
+except Run.ParseError as e:
+    die("%s:%s: %s", *e.args)
+except Run.ConfigError as e:
+    die("%s: %s", *e.args)
diff --git a/system/Config.in b/system/Config.in
index 935f7a1..b654e39 100644
--- a/system/Config.in
+++ b/system/Config.in
@@ -107,6 +107,12 @@ config BR2_INIT_NONE
 
 endchoice
 
+config BR2_INIT
+	string
+	default "sysv" if BR2_INIT_SYSV || BR2_INIT_BUSYBOX
+	default "systemd" if BR2_INIT_SYSTEMD
+	default "none"
+
 choice
 	prompt "/dev management" if !BR2_INIT_SYSTEMD
 	default BR2_ROOTFS_DEVICE_CREATION_DYNAMIC_DEVTMPFS
-- 
2.0.3




More information about the buildroot mailing list