]> git.e2factory.org Git - e2factory.git/commitdiff
Introduce result classes
authorTobias Ulmer <tu@emlix.com>
Wed, 25 May 2016 14:34:34 +0000 (16:34 +0200)
committerTobias Ulmer <tu@emlix.com>
Wed, 16 Nov 2016 14:41:18 +0000 (15:41 +0100)
basic_result class implements all common methods, but can't be used on
its own.

result_class represents the standard result type.

Special results can inherit from one of the two classes, overwriting
methods and such.

Composition is of course also possible, and may even be the better
option for plugins.

Signed-off-by: Tobias Ulmer <tu@emlix.com>
local/Makefile
local/result.lua [new file with mode: 0644]

index 629fdb81a6b7ab1734ce06f3a06c758f91738fec..22bf28e166d790c0ae4f55f1df3f8c55db7a3e7b 100644 (file)
@@ -42,7 +42,7 @@ LOCALLUATOOLS = e2-build e2-dlist e2-dsort e2-fetch-sources \
 
 LOCALLUALIBS= digest.lua e2build.lua e2tool.lua environment.lua \
              policy.lua scm.lua licence.lua chroot.lua project.lua \
-             source.lua sl.lua
+             source.lua sl.lua result.lua
 LOCALTOOLS = $(LOCALLUATOOLS)
 
 .PHONY: all install uninstall local install-local doc install-doc
diff --git a/local/result.lua b/local/result.lua
new file mode 100644 (file)
index 0000000..91db44e
--- /dev/null
@@ -0,0 +1,732 @@
+--- Result class. Implements the base result class and config loader.
+-- @module local.result
+
+-- Copyright (C) 2007-2016 emlix GmbH, see file AUTHORS
+--
+-- This file is part of e2factory, the emlix embedded build system.
+-- For more information see http://www.e2factory.org
+--
+-- e2factory is a registered trademark of emlix GmbH.
+--
+-- e2factory 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 3 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.
+
+local result = {}
+
+local chroot = require("chroot")
+local class = require("class")
+local e2lib = require("e2lib")
+local e2tool = require("e2tool")
+local environment = require("environment")
+local err = require("err")
+local hash = require("hash")
+local project = require("project")
+local sl = require("sl")
+local source = require("source")
+local strict = require("strict")
+
+--- Dictionary holding all result objects indexed by their name.
+result.results = {}
+
+--- Array holding all results objects in alphabetical order.
+result.results_sorted = {}
+
+--- Vector of result type detector functions.
+local type_detection_fns = {}
+
+--- Dictionary holding class objects indexed by type
+local result_types = {}
+
+--------------------------------------------------------------------------------
+--- Result base class.
+--------------------------------------------------------------------------------
+result.basic_result = class("basic_result")
+
+--- Result base constructor. Assert error on invalid input.
+-- @param rawres Result config dict containing at least "name" and "type"
+-- attributes.
+-- @return Nil on success, throws exception on failure.
+function result.basic_result:initialize(rawres)
+    assert(type(rawres) == "table")
+    assert(type(rawres.name) == "string" and rawres.name ~= "")
+    assert(type(rawres.type) == "string" and rawres.type ~= "")
+
+    self._name = rawres.name
+    self._type = rawres.type
+
+    --
+    -- e2build currently needs this stuff in every result.
+    --
+    self._build_config = false
+    self._build_mode = false
+    self._chroot_list = sl.sl:new(false, true)
+end
+
+--- Constructor that's called by load_result_configs() after all results
+-- have been created. Put consistency checks between results in here.
+-- return True on success, false on error.
+-- return Error object on failure.
+function result.basic_result:post_initialize()
+    return true -- default is no action
+end
+
+--- Get name.
+-- @return Name of result (Ex: group.result).
+function result.basic_result:get_name()
+    assertIsStringN(self._name)
+    return self._name
+end
+
+--- Get type.
+-- @return Type (string) of result config.
+function result.basic_result:get_type()
+    assertIsStringN(self._type)
+    return self._type
+end
+
+--- Get name as directory path.
+-- @return Path of result (ex: group/result).
+function result.basic_result:get_name_as_path()
+    assertIsStringN(self._name)
+    local p = e2tool.src_res_name_to_path(self._name)
+    assertIsStringN(p)
+    return p
+end
+
+--- Project-wide build id for this result and all of its depdencies.
+function result.basic_result:buildid()
+    error(err.new("called buildid() of result base class, type %s name %s",
+        self._type, self._name))
+end
+
+--- Return list of depdencencies
+function result.basic_result:dlist()
+    error(err.new("called dlist() of result base class, type %s name %s",
+        self._type, self._name))
+end
+
+--- Return the complete and merged environment for this result.
+-- Does NOT include the builtin environment from buildconfig.
+-- @return Environment object
+function result.basic_result:merged_env()
+    error(err.new("called merged_env() of result base class, type %s name %s",
+        self._type, self._name))
+end
+
+--- Set build_config table for result. XXX: better solution would be nice
+function result.basic_result:set_buildconfig(bc)
+    assert(type(bc) == "table")
+    assert(self.build_config == nil) -- Detect "old" code setting this field
+    self._build_config = bc
+end
+
+--- Get build_config table if it was set
+function result.basic_result:buildconfig()
+    assert(type(self._build_config) == "table")
+    assert(self.build_config == nil)
+
+    -- XXX: Returned table can be changed, and the current code relies on it
+    -- XXX: It would be nice if this could be fixed, but for now this has to do
+    return self._build_config
+end
+
+--- Set build_mode table for result:
+-- @param build_mode Build mode table
+function result.basic_result:set_build_mode(build_mode)
+    assert(type(build_mode) == "table")
+    assert(self.build_mode == nil)
+
+    self._build_mode = build_mode
+end
+
+function result.basic_result:get_build_mode()
+    assert(type(self._build_mode) == "table")
+    assert(self.build_mode == nil)
+
+    -- XXX: comments for buildconfig() apply
+    return self._build_mode
+end
+
+--- Return the complete and merged environment for this result.
+-- @return Environment object
+function result.basic_result:merged_env()
+    error(err.new("called merged_env() of result base class, type %s name %s",
+        self._type, self._name))
+end
+
+--- Return the list of chroot groups for this result.
+-- @return string list of required chroot groups.
+function result.basic_result:my_chroot_list()
+    return self._chroot_list
+end
+
+--- Get/set the settings class. Settings hold per-result information
+-- for the build process. Each result that's passed to a build process needs
+-- a valid settings_class
+-- @param bs Optional settings_class
+function result.basic_result:build_settings(bs)
+    if bs then
+        assertIsTable(bs)
+        self._build_settings = bs
+    else
+        assertIsTable(self._build_settings)
+    end
+
+    return self._build_settings
+end
+
+--- Textual free-form representation of the result.
+-- Returns a table in the form
+-- { { "sources", "a", "b", "c" }, { "depends", "d", "e" }, ... }.
+-- Informative only, output/order may change at any time!
+-- @param flagt Optional table with keys enabling additional attributes. At the
+-- moment 'chroot' and 'env' are known.
+-- @return A vector filled with attribute tables.
+function result.basic_result:attribute_table(flagt)
+    assert(flagt == nil or type(flagt) == "table")
+    error(err.new("called attribute_table() of result base class, type %s name %s",
+        self._type, self._name))
+end
+
+--- Dot representation
+function result.basic_result:dot(flagt)
+    assert(flagt == nil or type(flagt) == "table")
+    error(err.new("called todot() of result base class, type %s name %s",
+        self._type, self._name))
+end
+
+--------------------------------------------------------------------------------
+--- Result class (standard).
+--------------------------------------------------------------------------------
+result.result_class = class("result_class", result.basic_result)
+
+function result.result_class:initialize(rawres)
+    assert(type(rawres) == "table")
+    assert(rawres.type == "result")
+
+    result.basic_result.initialize(self, rawres)
+
+    self.XXXdepends = sl.sl:new(false, true)
+    self._buildid = false
+    self._sources_list = sl.sl:new(false, true)
+    self._env = environment.new()
+
+    local e = err.new("in result %s:", self._name)
+    local rc, re, info
+
+    rc, re = e2lib.vrfy_dict_exp_keys(rawres, "e2result config", {
+        "chroot",
+        "depends",
+        "env",
+        "name",
+        "sources",
+        "type",
+    })
+    if not rc then
+        error(e:cat(re))
+    end
+
+    if rawres.sources == nil then
+        e2lib.warnf("WDEFAULT", "in result %s:", self._name)
+        e2lib.warnf("WDEFAULT", " sources attribute not configured." ..
+            "Defaulting to empty list")
+        rawres.sources = {}
+    elseif type(rawres.sources) == "string" then
+        e2lib.warnf("WDEPRECATED", "in result %s:", self._name)
+        e2lib.warnf("WDEPRECATED", " sources attribute is string. "..
+            "Converting to list")
+        rawres.sources = { rawres.sources }
+    end
+
+    rc, re = e2lib.vrfy_listofstrings(rawres.sources, "sources", true, false)
+    if not rc then
+        e:append("source attribute:")
+        e:cat(re)
+    else
+        for _,sourcename in ipairs(rawres.sources) do
+            if not source.sources[sourcename] then
+                e:append("source does not exist: %s", sourcename)
+            end
+
+            self._sources_list:insert(sourcename)
+        end
+    end
+
+
+    if rawres.depends == nil then
+        e2lib.warnf("WDEFAULT", "in result %s: ", self._name)
+        e2lib.warnf("WDEFAULT", " depends attribute not configured. " ..
+        "Defaulting to empty list")
+        rawres.depends = {}
+    elseif type(rawres.depends) == "string" then
+        e2lib.warnf("WDEPRECATED", "in result %s:", self._name)
+        e2lib.warnf("WDEPRECATED", " depends attribute is string. "..
+        "Converting to list")
+        rawres.depends = { rawres.depends }
+    end
+    rc, re = e2lib.vrfy_listofstrings(rawres.depends, "depends", true, false)
+    if not rc then
+        e:append("dependency attribute:")
+        e:cat(re)
+    else
+        for _,depname in ipairs(rawres.depends) do
+            -- Delay depends checking until all results are loaded.
+            self.XXXdepends:insert(depname)
+        end
+    end
+
+    if rawres.chroot == nil then
+        e2lib.warnf("WDEFAULT", "in result %s:", self._name)
+        e2lib.warnf("WDEFAULT", " chroot groups not configured. " ..
+            "Defaulting to empty list")
+        rawres.chroot = {}
+    elseif type(rawres.chroot) == "string" then
+        e2lib.warnf("WDEPRECATED", "in result %s:", self._name)
+        e2lib.warnf("WDEPRECATED", " chroot attribute is string. "..
+            "Converting to list")
+        rawres.chroot = { rawres.chroot }
+    end
+    rc, re = e2lib.vrfy_listofstrings(rawres.chroot, "chroot", true, false)
+    if not rc then
+        e:append("chroot attribute:")
+        e:cat(re)
+    else
+        -- apply default chroot groups
+        for _,g in ipairs(chroot.groups_default) do
+            table.insert(rawres.chroot, g)
+        end
+        -- The list may have duplicates now. Unify.
+        rc, re = e2lib.vrfy_listofstrings(rawres.chroot, "chroot", false, true)
+        if not rc then
+            e:append("chroot attribute:")
+            e:cat(re)
+        end
+        for _,g in ipairs(rawres.chroot) do
+            if not chroot.groups_byname[g] then
+                e:append("chroot group does not exist: %s", g)
+            end
+
+            self:my_chroot_list():insert(g)
+        end
+    end
+
+
+    info = e2tool.info()
+
+    if rawres.env and type(rawres.env) ~= "table" then
+        e:append("result has invalid `env' attribute")
+    else
+        if not rawres.env then
+            e2lib.warnf("WDEFAULT", "result has no `env' attribute. "..
+                "Defaulting to empty dictionary")
+            rawres.env = {}
+        end
+
+        for k,v in pairs(rawres.env) do
+            if type(k) ~= "string" then
+                e:append("in `env' dictionary: "..
+                "key is not a string: %s", tostring(k))
+            elseif type(v) ~= "string" then
+                e:append("in `env' dictionary: "..
+                "value is not a string: %s", tostring(v))
+            else
+                self._env:set(k, v)
+            end
+        end
+    end
+
+    local build_script =
+        e2tool.resultbuildscript(self:get_name_as_path(), info.root)
+    if not e2lib.isfile(build_script) then
+        e:append("build-script does not exist: %s", build_script)
+    end
+
+    if e:getcount() > 1 then
+        error(e)
+    end
+
+end
+
+function result.result_class:post_initialize()
+    local e
+
+    for depname in self.XXXdepends:iter_sorted() do
+        if not result.results[depname] then
+            e = e or err.new("in result %s:", self:get_name())
+            e:append("dependency does not exist: %s", depname)
+        end
+    end
+
+    if e then
+        return false, e
+    end
+
+    return true
+end
+
+function result.result_class:dlist()
+    return self.XXXdepends:totable_sorted()
+end
+
+function result.result_class:my_sources_list()
+    return self._sources_list
+end
+
+function result.result_class:merged_env()
+    local e = environment.new()
+
+    -- Global env
+    e:merge(projenv.get_global_env(), false)
+
+    -- Sources env
+    for sourcename in self._sources_list:iter_sorted() do
+        local src = source.sources[sourcename]
+        e:merge(src:get_env(), true)
+    end
+
+    -- Global result specific env
+    e:merge(projenv.get_result_env(self._name), true)
+
+    -- Result specific env
+    e:merge(self._env, true)
+
+    return e
+end
+
+--- Get the project-wide buildid for a result, calculating it if required.
+-- @return BuildID or false on error.
+-- @return Error object on failure.
+function result.result_class:buildid()
+    local e, rc, re, info, hc, id, build_mode
+    build_mode = self:get_build_mode()
+
+    if self._buildid then
+        return build_mode.buildid(self._buildid)
+    end
+
+    e = err.new("error calculating BuildID for result: %s", self:get_name())
+    info = e2tool.info()
+    hc = hash.hash_start()
+
+    -- basic_result
+    hash.hash_append(hc, self:get_name())
+    hash.hash_append(hc, self:get_type())
+
+    -- sources
+    for sourcename in self:my_sources_list():iter_sorted() do
+        local src, sourceset
+
+        src = source.sources[sourcename]
+        sourceset = build_mode.source_set()
+        assert(type(sourceset) == "string" and sourceset ~= "")
+        id, re = src:sourceid(sourceset)
+        if not id then
+            return false, re
+        end
+
+        hash.hash_append(hc, id)
+    end
+
+    -- chroot
+    for groupname in self:my_chroot_list():iter_sorted() do
+        id, re = chroot.groups_byname[groupname]:chrootgroupid(info)
+        if not id then
+            return false, e:cat(re)
+        end
+        hash.hash_append(hc, id)
+    end
+
+    -- environment
+    hash.hash_append(hc, self:merged_env():id())
+
+    -- buildscript
+    local file = {
+        server = info.root_server_name,
+        location = e2tool.resultbuildscript(self:get_name_as_path()),
+    }
+
+    id, re = e2tool.fileid(info, file)
+    if not id then
+        return false, re
+    end
+    hash.hash_append(hc, id)
+
+
+    -- depends
+    for depname in self.XXXdepends:iter_sorted() do
+        id, re = result.results[depname]:buildid()
+        if not id then
+            return false, re
+        end
+        hash.hash_append(hc, id)
+    end
+
+    -- project
+    id, re = project.projid(info)
+    if not id then
+        return false, e:cat(re)
+    end
+    hash.hash_append(hc, id)
+
+    self._buildid = hash.hash_finish(hc)
+
+    return build_mode.buildid(self._buildid)
+end
+
+function result.result_class:attribute_table(flagt)
+    assert(flagt == nil or type(flagt) == "table")
+
+    local t = {}
+    flagt = flagt or {}
+
+    table.insert(t, { "sources", self:my_sources_list():unpack()})
+    table.insert(t, { "depends", self.XXXdepends:unpack()})
+    if flagt.chroot then
+        table.insert(t, { "chroot", self:my_chroot_list():unpack()})
+    end
+    if flagt.env then
+        local tenv = { "env" }
+        for k, v in self:merged_env():iter() do
+            table.insert(tenv, string.format("%s=%s", k, v))
+        end
+        table.insert(t, tenv)
+    end
+
+    return t
+end
+
+function result.result_class:todot(flagt)
+    assert(flagt == nil or type(flagt) == "table")
+    flagt = flagt or {}
+    error("todot missing implementation")
+end
+
+--------------------------------------------------------------------------------
+-- Result loading and plugin hookup
+--------------------------------------------------------------------------------
+
+--- Gather result paths.
+-- @param info Info table.
+-- @param basedir Nil or directory from where to start scanning for more
+--                results. Only for recursion.
+-- @param results Nil or table of result paths. Only for recursion.
+-- @return Table with result paths, or false on error.
+-- @return Error object on failure.
+local function gather_result_paths(info, basedir, results)
+    local rc, re
+    local currdir, resdir, resconfig, s
+
+    results = results or {}
+    currdir = e2tool.resultdir(basedir, info.root)
+    for entry, re in e2lib.directory(currdir) do
+        if not entry then
+            return false, re
+        end
+
+        if basedir then
+            entry = e2lib.join(basedir, entry)
+        end
+
+        resdir = e2tool.resultdir(entry, info.root)
+        resconfig = e2tool.resultconfig(entry, info.root)
+        s = e2lib.stat(resdir, false)
+        if s.type == "directory" then
+            if e2lib.exists(resconfig) then
+                table.insert(results, entry)
+            else
+                -- try subfolder
+                rc, re = gather_result_paths(info, entry, results)
+                if not rc then
+                    return false, re
+                end
+            end
+        end
+    end
+
+    return results
+end
+
+local function load_rawres(cfg)
+    local e, rc, re
+    local rawres, loadcnt, g, path, res, info
+
+    e = err.new("error loading result configuration")
+
+    rc, re = e2tool.verify_src_res_pathname_valid_chars(cfg)
+    if not rc then
+        e:append("invalid result file name: %s", cfg)
+        e:cat(re)
+        return false, e
+    end
+
+    info = e2tool.info()
+    rawres = nil
+    loadcnt = 0
+    g = {
+        e2result = function(data) rawres = data loadcnt = loadcnt + 1 end,
+        env = info.env,
+        string = e2lib.safe_string_table(),
+    }
+
+    path = e2tool.resultconfig(cfg, info.root)
+    rc, re = e2lib.dofile2(path, g)
+    if not rc then
+        return false, e:cat(re)
+    end
+
+    if type(rawres) ~= "table" then
+        return false, e:append("result %q is missing an e2result table", cfg)
+    end
+
+    if loadcnt > 1 then
+        return false, e:append("duplicate result config in %q", cfg)
+    end
+
+    if not rawres.name then
+        rawres.name = e2tool.src_res_path_to_name(cfg)
+    end
+
+    if rawres.name ~= e2tool.src_res_path_to_name(cfg) then
+        return false, e:append(
+            "result name %q must match result directory name %q",
+            rawres.name, e2tool.src_res_path_to_name(cfg))
+    end
+
+    rc, re = e2tool.verify_src_res_name_valid_chars(rawres.name)
+    if not rc then
+        e:append("invalid result name: %s", rawres.name)
+        e:cat(re)
+        return false, e
+    end
+
+    if result.results[rawres.name] then
+        return false, e:append("duplicate result: %s", rawres.name)
+    end
+
+    if not rawres.type then
+        for _,type_detection in ipairs(type_detection_fns) do
+            -- Do not shortcut type detection on success.
+            -- Some functions may need to see the raw result even if it
+            -- does not match their type.
+            type_detection(rawres)
+        end
+
+        -- If the type can't be guessed, assume it's a standard result
+        if not rawres.type then
+            rawres.type = "result"
+        end
+    end
+
+    if not result_types[rawres.type] then
+        return false,
+            e:append("don't know how to handle %q result type", rawres.type)
+    end
+
+    return rawres
+end
+
+local function load_one_config(cfg)
+    assert(type(cfg) == "string" and cfg ~= "")
+    local rc, re, e, rawres, res
+
+    rawres, re = load_rawres(cfg)
+    if not rawres then
+        return false, re
+    end
+
+    assert(type(rawres.type) == "string")
+    assert(type(rawres.name) == "string")
+
+    res = result_types[rawres.type]
+    rc, re = e2lib.trycall(res.new, res, rawres)
+    if not rc then
+        e = err.new("error in result %q", rawres.name)
+        return false, e:cat(re)
+    end
+
+    res = re
+    assert(type(res) == "table")
+    result.results[res:get_name()] = res
+
+    return true
+end
+
+--- Search, load and verify all result configs. On success, all results are
+--available as objects in result.results[].
+-- @param info Info table
+-- @return True on success, false on error.
+-- @return Error object on failure.
+function result.load_result_configs(info)
+    local rc, re, e, configs, res
+
+    configs, re = gather_result_paths(info)
+    if not configs then
+        return false, re
+    end
+
+    for _,cfg in ipairs(configs) do
+        rc, re = load_one_config(cfg)
+        if not rc then
+            return false, re
+        end
+    end
+
+    for resultname,_ in pairs(result.results) do
+        table.insert(result.results_sorted, resultname)
+    end
+
+    table.sort(result.results_sorted)
+
+    for _,resultname in ipairs(result.results_sorted) do
+        res = result.results[resultname]
+        rc, re = res:post_initialize()
+        if not rc then
+            return false, re
+        end
+    end
+
+    return true
+end
+
+--- Registers a function that detects the type of a raw result config table.
+-- The registered function is passed a "rawres" table, and must set the "type"
+-- field within that table if it recognizes its type.
+-- @param func Function in the form function(rawres)...end
+function result.register_type_detection(func)
+    assert(type(func) == "function")
+    for _,fn in ipairs(type_detection_fns) do
+        assert(fn ~= func, err.new("result type detection already registered"))
+    end
+    table.insert(type_detection_fns, func)
+
+    return true
+end
+
+--- Register a result class. A type can only be registered once.
+-- @param typ Source type name.
+-- @param result_class Class derived from basic_result.
+-- @return True on success, false on error.
+-- @return Error object on failure.
+function result.register_result_class(typ, result_class)
+    assert(type(typ) == "string" and typ ~= "")
+    assert(type(result_class) == "table")
+    assert(result_types[typ] == nil,
+        err.new("result %q already registered", typ))
+
+    result_types[typ] = result_class
+
+    return true
+end
+
+result.register_result_class("result", result.result_class)
+
+return strict.lock(result)
+
+-- vim:sw=4:sts=4:et: