summaryrefslogtreecommitdiffstats
path: root/common/blspec.c
diff options
context:
space:
mode:
authorSascha Hauer <s.hauer@pengutronix.de>2013-09-27 16:35:46 +0200
committerSascha Hauer <s.hauer@pengutronix.de>2013-10-14 14:04:20 +0200
commitef5dac9c2bf7ac7ceb3027d75dadb5c52e633572 (patch)
treefeb68df17a7b53f794a0caf76b7836fdc26ded93 /common/blspec.c
parent1369bfd44f2393893036ee50f81e0104f7fb6416 (diff)
downloadbarebox-ef5dac9c2bf7ac7ceb3027d75dadb5c52e633572.tar.gz
Implement bootloader spec support for barebox
The Bootloader Specification describes a way how kernels can be installed on devices and how they can be started by the bootloader. The bootloader spec is currently supported by (x86) gummiboot and by systemd which provides a kernel-install script. With the bootloader spec it's possible for the Operating system to install a new kernel without knowing about the bootloader and for the bootloader it's possible to discover and start Operating Systems on a media without being configured. For more details about the spec see: http://www.freedesktop.org/wiki/Specifications/BootLoaderSpec/ This patch adds barebox support for the spec. It enhances the 'boot' command so that not only boot script names can be given, but also devices containing bootloader spec entries. With this it's possible to call the 'boot' command like: 'boot sd emmc net'. It would then first look for bootloader spec entries on the (removable) sd card, then, is nothing is found, on the internal emmc and if still unsuccessful would call the 'net' bootscript. The bootloader Spec currently doesn't specify which entry should be default if multiple entries are found on a single device. Therefore barebox currently has two extensions of the spec. The $BOOT diretory can contain a file named 'default'. If present, the content of the file is treated as a filename under $BOOT/loader/entries/ which is used as default. Similarly if a file named 'once' is present, the entry is started once and the file is removed afterwards. This is useful for testing if a newly installed kernel works before making it the default. As on ARM and other Architectures a devicetree has to be specified for the kernel, the 'devicetree' property is used to specify a devicetree. Like 'kernel' and 'initrd' this also contains a pth relative to $BOOT. Signed-off-by: Sascha Hauer <s.hauer@pengutronix.de>
Diffstat (limited to 'common/blspec.c')
-rw-r--r--common/blspec.c516
1 files changed, 516 insertions, 0 deletions
diff --git a/common/blspec.c b/common/blspec.c
new file mode 100644
index 0000000..f306ada
--- /dev/null
+++ b/common/blspec.c
@@ -0,0 +1,516 @@
+/*
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ */
+#include <environment.h>
+#include <globalvar.h>
+#include <readkey.h>
+#include <common.h>
+#include <driver.h>
+#include <blspec.h>
+#include <malloc.h>
+#include <block.h>
+#include <fcntl.h>
+#include <libbb.h>
+#include <init.h>
+#include <boot.h>
+#include <fs.h>
+#include <of.h>
+#include <linux/stat.h>
+#include <linux/err.h>
+
+/*
+ * blspec_entry_var_set - set a variable to a value
+ */
+int blspec_entry_var_set(struct blspec_entry *entry, const char *name,
+ const char *val)
+{
+ return of_set_property(entry->node, name, val,
+ val ? strlen(val) + 1 : 0, 1);
+}
+
+/*
+ * blspec_entry_var_get - get the value of a variable
+ */
+const char *blspec_entry_var_get(struct blspec_entry *entry, const char *name)
+{
+ const char *str;
+ int ret;
+
+ ret = of_property_read_string(entry->node, name, &str);
+
+ return ret ? NULL : str;
+}
+
+/*
+ * blspec_entry_open - open an entry given a path
+ */
+static struct blspec_entry *blspec_entry_open(struct blspec *blspec,
+ const char *abspath)
+{
+ struct blspec_entry *entry;
+ char *end, *line, *next;
+ char *buf;
+
+ pr_debug("%s: %s\n", __func__, abspath);
+
+ buf = read_file(abspath, NULL);
+ if (!buf)
+ return ERR_PTR(-errno);
+
+ entry = blspec_entry_alloc(blspec);
+
+ next = buf;
+
+ while (*next) {
+ char *name, *val;
+
+ line = next;
+
+ next = strchr(line, '\n');
+ if (next) {
+ *next = 0;
+ next++;
+ }
+
+ name = line;
+ end = name;
+
+ while (*end && (*end != ' ' && *end != '\t'))
+ end++;
+
+ if (!*end) {
+ blspec_entry_var_set(entry, name, NULL);
+ continue;
+ }
+
+ *end = 0;
+
+ end++;
+
+ while (*end == ' ' || *end == '\t')
+ end++;
+
+ if (!*end) {
+ blspec_entry_var_set(entry, name, NULL);
+ continue;
+ }
+
+ val = end;
+
+ blspec_entry_var_set(entry, name, val);
+ }
+
+ free(buf);
+
+ return entry;
+}
+
+/*
+ * blspec_have_entry - check if we already have an entry with
+ * a certain path
+ */
+static int blspec_have_entry(struct blspec *blspec, const char *path)
+{
+ struct blspec_entry *e;
+
+ list_for_each_entry(e, &blspec->entries, list) {
+ if (e->configpath && !strcmp(e->configpath, path))
+ return 1;
+ }
+
+ return 0;
+}
+
+/*
+ * blspec_scan_directory - scan over a directory
+ *
+ * Given a root path collects all blspec entries found under /blspec/entries/.
+ *
+ * returns 0 if at least one entry could be successfully loaded, negative
+ * error value otherwise.
+ */
+static int blspec_scan_directory(struct blspec *blspec, const char *root,
+ struct cdev *cdev)
+{
+ struct blspec_entry *entry;
+ DIR *dir;
+ struct dirent *d;
+ char *abspath;
+ int ret, found = 0;
+ const char *dirname = "loader/entries";
+ char *entry_default = NULL, *entry_once = NULL;
+
+ pr_debug("%s: %s %s\n", __func__, root, dirname);
+
+ entry_default = read_file_line("%s/default", root);
+ entry_once = read_file_line("%s/once", root);
+
+ abspath = asprintf("%s/%s", root, dirname);
+
+ dir = opendir(abspath);
+ if (!dir) {
+ pr_debug("%s: %s: %s\n", __func__, abspath, strerror(errno));
+ ret = -errno;
+ goto err_out;
+ }
+
+ while ((d = readdir(dir))) {
+ char *configname;
+ struct stat s;
+ char *dot;
+ char *devname = NULL, *hwdevname = NULL;
+
+ if (*d->d_name == '.')
+ continue;
+
+ configname = asprintf("%s/%s", abspath, d->d_name);
+
+ dot = strrchr(configname, '.');
+ if (!dot) {
+ free(configname);
+ continue;
+ }
+
+ if (strcmp(dot, ".conf")) {
+ free(configname);
+ continue;
+ }
+
+ ret = stat(configname, &s);
+ if (ret) {
+ free(configname);
+ continue;
+ }
+
+ if (!S_ISREG(s.st_mode)) {
+ free(configname);
+ continue;
+ }
+
+ if (blspec_have_entry(blspec, configname)) {
+ free(configname);
+ continue;
+ }
+
+ entry = blspec_entry_open(blspec, configname);
+ if (IS_ERR(entry)) {
+ free(configname);
+ continue;
+ }
+
+ found = 1;
+
+ entry->rootpath = xstrdup(root);
+ entry->configpath = configname;
+ entry->cdev = cdev;
+
+ if (entry_default && !strcmp(d->d_name, entry_default))
+ entry->boot_default = true;
+ if (entry_once && !strcmp(d->d_name, entry_once))
+ entry->boot_once = true;
+
+ devname = xstrdup(dev_name(entry->cdev->dev));
+ if (entry->cdev->dev->parent)
+ hwdevname = xstrdup(dev_name(entry->cdev->dev->parent));
+
+ entry->me.display = asprintf("%-20s %-20s %s", devname, hwdevname,
+ blspec_entry_var_get(entry, "title"));
+ free(devname);
+ free(hwdevname);
+
+ entry->me.type = MENU_ENTRY_NORMAL;
+ }
+
+ ret = found ? 0 : -ENOENT;
+
+ closedir(dir);
+err_out:
+ free(abspath);
+ free(entry_default);
+ free(entry_once);
+
+ return ret;
+}
+
+/*
+ * blspec_scan_cdev - scan over a cdev
+ *
+ * Given a cdev this function mounts the filesystem and collects all blspec
+ * entries found under /blspec/entries/.
+ *
+ * returns 0 if at least one entry could be successfully loaded, negative
+ * error value otherwise.
+ */
+static int blspec_scan_cdev(struct blspec *blspec, struct cdev *cdev)
+{
+ int ret;
+ void *buf = xzalloc(512);
+ enum filetype type;
+ const char *rootpath;
+
+ pr_debug("%s: %s\n", __func__, cdev->name);
+
+ ret = cdev_read(cdev, buf, 512, 0, 0);
+ if (ret < 0) {
+ free(buf);
+ return ret;
+ }
+
+ type = file_detect_partition_table(buf, 512);
+ free(buf);
+
+ if (type == filetype_mbr || type == filetype_gpt)
+ return -EINVAL;
+
+ rootpath = cdev_mount_default(cdev);
+ if (IS_ERR(rootpath))
+ return PTR_ERR(rootpath);
+
+ ret = blspec_scan_directory(blspec, rootpath, cdev);
+
+ return ret;
+}
+
+/*
+ * blspec_scan_devices - scan all devices for child cdevs
+ *
+ * Iterate over all devices and collect child their cdevs.
+ */
+void blspec_scan_devices(struct blspec *blspec)
+{
+ struct device_d *dev;
+ struct block_device *bdev;
+
+ for_each_device(dev)
+ device_detect(dev);
+
+ for_each_block_device(bdev) {
+ struct cdev *cdev = &bdev->cdev;
+
+ list_for_each_entry(cdev, &bdev->dev->cdevs, devices_list)
+ blspec_scan_cdev(blspec, cdev);
+ }
+}
+
+/*
+ * blspec_scan_device - scan a device for child cdevs
+ *
+ * Given a device this functions scans over all child cdevs looking
+ * for blspec entries.
+ */
+int blspec_scan_device(struct blspec *blspec, struct device_d *dev)
+{
+ struct device_d *child;
+ struct cdev *cdev;
+ int ret;
+
+ pr_debug("%s: %s\n", __func__, dev_name(dev));
+
+ list_for_each_entry(cdev, &dev->cdevs, devices_list) {
+ /*
+ * If the OS is installed on a disk with MBR disk label, and a
+ * partition with the MBR type id of 0xEA already exists it
+ * should be used as $BOOT
+ */
+ if (cdev->dos_partition_type == 0xea) {
+ blspec_scan_cdev(blspec, cdev);
+ return 0;
+ }
+
+ /*
+ * If the OS is installed on a disk with GPT disk label, and a
+ * partition with the GPT type GUID of
+ * bc13c2ff-59e6-4262-a352-b275fd6f7172 already exists, it
+ * should be used as $BOOT.
+ *
+ * Not yet implemented
+ */
+ }
+
+ /* Try child devices */
+ device_for_each_child(dev, child) {
+ ret = blspec_scan_device(blspec, child);
+ if (!ret)
+ return 0;
+ }
+
+ /*
+ * As a last resort try all cdevs (Not only the ones explicitly stated
+ * by the bootblspec spec).
+ */
+ list_for_each_entry(cdev, &dev->cdevs, devices_list) {
+ ret = blspec_scan_cdev(blspec, cdev);
+ if (!ret)
+ return 0;
+ }
+
+ return -ENODEV;
+}
+
+/*
+ * blspec_scan_hwdevice - scan a hardware device for child cdevs
+ *
+ * Given a name of a hardware device this functions scans over all child
+ * cdevs looking for blspec entries.
+ */
+int blspec_scan_hwdevice(struct blspec *blspec, const char *devname)
+{
+ struct device_d *dev;
+
+ pr_debug("%s: %s\n", __func__, devname);
+
+ dev = get_device_by_name(devname);
+ if (!dev)
+ return -ENODEV;
+
+ device_detect(dev);
+
+ blspec_scan_device(blspec, dev);
+
+ return 0;
+}
+
+/*
+ * blspec_boot - boot an entry
+ *
+ * This boots an entry. On success this function does not return.
+ * In case of an error the error code is returned. This function may
+ * return 0 in case of a succesful dry run.
+ */
+int blspec_boot(struct blspec_entry *entry, int verbose, int dryrun)
+{
+ int ret;
+ const char *abspath, *devicetree, *options, *initrd, *linuximage;
+ struct bootm_data data = {
+ .initrd_address = UIMAGE_INVALID_ADDRESS,
+ .os_address = UIMAGE_SOME_ADDRESS,
+ .verbose = verbose,
+ .dryrun = dryrun,
+ };
+
+ globalvar_set_match("linux.bootargs.dyn.", "");
+ globalvar_set_match("bootm.", "");
+
+ devicetree = blspec_entry_var_get(entry, "devicetree");
+ initrd = blspec_entry_var_get(entry, "initrd");
+ options = blspec_entry_var_get(entry, "options");
+ linuximage = blspec_entry_var_get(entry, "linux");
+
+ if (entry->rootpath)
+ abspath = entry->rootpath;
+ else
+ abspath = "";
+
+ data.os_file = asprintf("%s/%s", abspath, linuximage);
+
+ if (devicetree) {
+ if (!strcmp(devicetree, "none")) {
+ struct device_node *node = of_get_root_node();
+ if (node)
+ of_delete_node(node);
+ } else {
+ data.oftree_file = asprintf("%s/%s", abspath,
+ devicetree);
+ }
+ }
+
+ if (initrd)
+ data.initrd_file = asprintf("%s/%s", abspath, initrd);
+
+ globalvar_add_simple("linux.bootargs.blspec", options);
+
+ pr_info("booting %s from %s\n", blspec_entry_var_get(entry, "title"),
+ dev_name(entry->cdev->dev));
+
+ if (entry->boot_once) {
+ char *s = asprintf("%s/once", abspath);
+
+ ret = unlink(s);
+ if (ret)
+ pr_err("unable to unlink 'once': %s\n", strerror(-ret));
+ else
+ pr_info("removed 'once'\n");
+
+ free(s);
+ }
+
+ ret = bootm_boot(&data);
+ if (ret)
+ pr_err("Booting failed\n");
+
+ free((char *)data.oftree_file);
+ free((char *)data.initrd_file);
+ free((char *)data.os_file);
+
+ return ret;
+}
+
+/*
+ * blspec_entry_default - find the entry to load.
+ *
+ * return in the order of precendence:
+ * - The entry specified in the 'once' file
+ * - The entry specified in the 'default' file
+ * - The first entry
+ */
+struct blspec_entry *blspec_entry_default(struct blspec *l)
+{
+ struct blspec_entry *entry_once = NULL;
+ struct blspec_entry *entry_default = NULL;
+ struct blspec_entry *entry_first = NULL;
+ struct blspec_entry *e;
+
+ list_for_each_entry(e, &l->entries, list) {
+ if (!entry_first)
+ entry_first = e;
+ if (e->boot_once)
+ entry_once = e;
+ if (e->boot_default)
+ entry_default = e;
+ }
+
+ if (entry_once)
+ return entry_once;
+ if (entry_default)
+ return entry_default;
+ return entry_first;
+}
+
+/*
+ * blspec_boot_hwdevice - scan hardware device for blspec entries and
+ * start the best one.
+ */
+int blspec_boot_hwdevice(const char *devname, int verbose, int dryrun)
+{
+ struct blspec *blspec;
+ struct blspec_entry *e;
+ int ret;
+
+ blspec = blspec_alloc();
+
+ ret = blspec_scan_hwdevice(blspec, devname);
+ if (ret)
+ return ret;
+
+ e = blspec_entry_default(blspec);
+ if (!e) {
+ printf("Nothing found on %s\n", devname);
+ ret = -ENOENT;
+ goto out;
+ }
+
+ ret = blspec_boot(e, verbose, dryrun);
+out:
+ blspec_free(blspec);
+
+ return ret;
+}