fw update: common code

new daemons to implement the flow as described in
https://github.com/openbmc/docs/blob/master/designs/code-update.md

- common/
  common code folder
  - common update flow
  - base class for the device specific update daemons

The new daemons are all following the generic template of Code Updater
daemon as outlined in the design.

The idea is that they are separate daemons (per device, as outlined in
the design) but share all the code that's not device specific.

Tested: next patch in series

Change-Id: If2438b8506aceb8c5313ec13a0bf7cb68f3cc279
Signed-off-by: Alexander Hansen <alexander.hansen@9elements.com>
diff --git a/common/README.md b/common/README.md
new file mode 100644
index 0000000..9ac5054
--- /dev/null
+++ b/common/README.md
@@ -0,0 +1,23 @@
+# Common Firmware Library
+
+This library follows the code update design:
+https://github.com/openbmc/docs/blob/master/designs/code-update.md
+
+It enables implementing code updaters for different devices. Each one inherits
+from the classes found in the common folder.
+
+Device-specific class members can be added to implement the code update flow for
+different devices.
+
+## PLDM Package Parser
+
+The PackageParser in the pldm directory currently references a following
+revision from the openbmc/pldm repository -
+
+https://github.com/openbmc/pldm/blob/f48015b39f945c2f9534e674819bdfef7b6c7054/fw-update/package_parser.cpp#L294
+
+However, this code will be deprecated and replaced with the package parsing APIs
+provided by libpldm once they become available.
+
+The PackageParser has a maximum supported PLDM firmware package revision of
+1.0.0.
diff --git a/common/include/device.hpp b/common/include/device.hpp
new file mode 100644
index 0000000..56fc920
--- /dev/null
+++ b/common/include/device.hpp
@@ -0,0 +1,123 @@
+#pragma once
+
+#include "common/pldm/package_parser.hpp"
+#include "software.hpp"
+#include "software_config.hpp"
+
+#include <sdbusplus/async/context.hpp>
+#include <xyz/openbmc_project/Association/Definitions/aserver.hpp>
+#include <xyz/openbmc_project/Software/Activation/aserver.hpp>
+#include <xyz/openbmc_project/Software/Update/aserver.hpp>
+#include <xyz/openbmc_project/Software/Version/aserver.hpp>
+
+#include <string>
+
+using ActivationInterface =
+    sdbusplus::common::xyz::openbmc_project::software::Activation;
+
+namespace phosphor::software::manager
+{
+class SoftwareManager;
+};
+
+namespace phosphor::software::device
+{
+
+class Device
+{
+  public:
+    Device(sdbusplus::async::context& ctx,
+           const phosphor::software::config::SoftwareConfig& deviceConfig,
+           phosphor::software::manager::SoftwareManager* parent,
+           std::set<RequestedApplyTimes> allowedApplyTimes);
+
+    virtual ~Device() = default;
+    Device(const Device&) = delete;
+    Device& operator=(const Device&) = delete;
+    Device(Device&&) = delete;
+    Device& operator=(Device&&) = delete;
+
+    // @brief                      Applies the image to the device
+    // @param image                raw fw image without pldm header
+    // @param image_size           size of 'image'
+    // @returns                    true if update was applied successfully.
+    //                             Should also return true if update was applied
+    //                             successfully, but the host failed to power
+    //                             on.
+    virtual sdbusplus::async::task<bool> updateDevice(const uint8_t* image,
+                                                      size_t image_size) = 0;
+
+    // @brief               Set the ActivationProgress properties on dbus
+    // @param progress      progress value
+    // @returns             true on successful property update
+    bool setUpdateProgress(uint8_t progress) const;
+
+    // @brief                      This coroutine is spawned to perform the
+    // async update of the device.
+    // @param image                The memory fd with the pldm package
+    // @param applyTime            When the update should be applied
+    // @param swid                 The software id to use
+    // @returns                    true if update was successfull
+    sdbusplus::async::task<bool> startUpdateAsync(
+        sdbusplus::message::unix_fd image, RequestedApplyTimes applyTime,
+        std::unique_ptr<Software> softwareUpdateExternal);
+
+    // Value of 'Type' field for the configuration in EM exposes record
+    std::string getEMConfigType() const;
+
+  protected:
+    // The apply times for updates which are supported by the device
+    // Override this if your device deviates from the default set of apply
+    // times.
+    std::set<RequestedApplyTimes> allowedApplyTimes;
+
+    // software instance, identified by its swid
+    // The specific derived class also owns its dbus interfaces,
+    // which are destroyed when the instance is deleted.
+    std::unique_ptr<Software> softwareCurrent;
+
+    // In case of apply time == OnReset, this contains the software version
+    // which has been written to the device, or should be written to it,
+    // but is not active yet.
+    std::unique_ptr<Software> softwarePending;
+
+    // Resets the device, in whichever way is appropriate for the device.
+    // The reset must be capable to apply the firmware update which was done
+    // by 'deviceSpecificUpdateFunction', in case that function did not already
+    // apply it. This method is optional to implement for that reason.
+    virtual sdbusplus::async::task<bool> resetDevice();
+
+    // The common configuration that all devices share.
+    // We get this from EM configuration.
+    config::SoftwareConfig config;
+
+    manager::SoftwareManager* parent;
+
+    sdbusplus::async::context& ctx;
+
+  private:
+    bool updateInProgress = false;
+
+    // @param componentImage       component image as extracted from update pkg
+    // @param componentImageSize   size of 'componentImage'
+    // @param applyTime            when the update should be applied
+    // @param softwarePendingIn    the pending software instance
+    // @returns                    the return value of the device specific
+    // update function
+    sdbusplus::async::task<bool> continueUpdateWithMappedPackage(
+        const uint8_t* componentImage, size_t componentImageSize,
+        const std::string& componentVersion, RequestedApplyTimes applyTime,
+        const std::unique_ptr<Software>& softwarePendingIn);
+
+    // @brief     extracts the information we need from the pldm package
+    // @returns   true on success
+    sdbusplus::async::task<bool> getImageInfo(
+        std::unique_ptr<void, std::function<void(void*)>>& pldmPackage,
+        size_t pldmPackageSize, uint8_t** matchingComponentImage,
+        size_t* componentImageSize, std::string& componentVersion);
+
+    friend update::SoftwareUpdate;
+    friend Software;
+};
+
+}; // namespace phosphor::software::device
diff --git a/common/include/software.hpp b/common/include/software.hpp
new file mode 100644
index 0000000..09a0b0f
--- /dev/null
+++ b/common/include/software.hpp
@@ -0,0 +1,127 @@
+#pragma once
+
+#include "software_update.hpp"
+
+#include <sdbusplus/async/context.hpp>
+#include <xyz/openbmc_project/Association/Definitions/aserver.hpp>
+#include <xyz/openbmc_project/Software/Activation/aserver.hpp>
+#include <xyz/openbmc_project/Software/ActivationBlocksTransition/aserver.hpp>
+#include <xyz/openbmc_project/Software/ActivationProgress/aserver.hpp>
+#include <xyz/openbmc_project/Software/Version/aserver.hpp>
+
+#include <string>
+
+namespace phosphor::software::device
+{
+class Device;
+}
+
+namespace phosphor::software
+{
+
+// Need to declare this class to initialize the protected members of our base
+// class. This prevents "Conditional jump or move depends on uninitialised
+// value(s)" when properties are updated for the first time.
+class SoftwareActivationProgress :
+    private sdbusplus::aserver::xyz::openbmc_project::software::
+        ActivationProgress<Software>
+{
+  public:
+    SoftwareActivationProgress(sdbusplus::async::context& ctx,
+                               const char* objPath);
+
+    void setProgress(int progressArg);
+};
+
+using SoftwareActivationBlocksTransition = sdbusplus::aserver::xyz::
+    openbmc_project::software::ActivationBlocksTransition<Software>;
+
+using SoftwareVersion =
+    sdbusplus::aserver::xyz::openbmc_project::software::Version<Software>;
+using SoftwareActivation =
+    sdbusplus::aserver::xyz::openbmc_project::software::Activation<Software>;
+using SoftwareAssociationDefinitions =
+    sdbusplus::aserver::xyz::openbmc_project::association::Definitions<
+        Software>;
+
+// This represents a software version running on the device.
+class Software : private SoftwareActivation
+{
+  public:
+    Software(sdbusplus::async::context& ctx, device::Device& parent);
+
+    // Set the activation status of this software
+    // @param activation         The activation status
+    void setActivation(SoftwareActivation::Activations activation);
+
+    // Add / remove the 'ActivationBlocksTransition' dbus interface.
+    // This dbus interface is only needed during the update process.
+    // @param enabled       determines if the dbus interface should be there
+    void setActivationBlocksTransition(bool enabled);
+
+    // This should populate 'softwareUpdate'
+    // @param allowedApplyTimes        When updates to this Version can be
+    // applied
+    void enableUpdate(const std::set<RequestedApplyTimes>& allowedApplyTimes);
+
+    // This should populate 'softwareVersion'
+    // @param version      the version string
+    void setVersion(const std::string& versionStr);
+
+    // This should populate 'softwareAssociationDefinitions'
+    // @param isRunning             if the software version is currently running
+    // on the device. Otherwise the software is assumed to be activating (not
+    // yet running).
+    sdbusplus::async::task<> createInventoryAssociations(bool isRunning);
+
+    // object path of this software
+    sdbusplus::message::object_path objectPath;
+
+    // The device we are associated to, meaning this software is either running
+    // on the device, or could potentially run on that device (update).
+    device::Device& parentDevice;
+
+    // The software id
+    const std::string swid;
+
+    // This is only required during the activation of the new fw
+    // and is deleted again afterwards.
+    // This member is public since the device specific update function
+    // needs to update the progress.
+    std::unique_ptr<SoftwareActivationProgress> softwareActivationProgress =
+        nullptr;
+
+  protected:
+    // @returns        a random software id (swid) for that device
+    static std::string getRandomSoftwareId(device::Device& parent);
+
+  private:
+    Software(sdbusplus::async::context& ctx, device::Device& parent,
+             const std::string& swid);
+
+    // Dbus interface to prevent power state transition during update.
+    std::unique_ptr<SoftwareActivationBlocksTransition>
+        activationBlocksTransition = nullptr;
+
+    // The software update dbus interface is not always present.
+    // It is constructed if the software version is able to be updated.
+    // For the new software version, this interface is constructed after the
+    // update has taken effect
+    std::unique_ptr<update::SoftwareUpdate> updateIntf = nullptr;
+
+    // We do not know the software version until we parse the PLDM package.
+    // Since the Activation interface needs to be available
+    // before then, this is nullptr until we get to know the version.
+    std::unique_ptr<SoftwareVersion> version = nullptr;
+
+    // This represents our association to the inventory item in case
+    // this software is currently on the device.
+    std::unique_ptr<SoftwareAssociationDefinitions> associationDefinitions =
+        nullptr;
+
+    sdbusplus::async::context& ctx;
+
+    friend update::SoftwareUpdate;
+};
+
+}; // namespace phosphor::software
diff --git a/common/include/software_config.hpp b/common/include/software_config.hpp
new file mode 100644
index 0000000..f6d41ee
--- /dev/null
+++ b/common/include/software_config.hpp
@@ -0,0 +1,58 @@
+#pragma once
+
+#include "sdbusplus/async/context.hpp"
+
+#include <cstdint>
+#include <string>
+
+namespace phosphor::software::device
+{
+class Device;
+}
+
+using namespace phosphor::software::device;
+
+namespace phosphor::software::config
+{
+
+/* This class represents the device software configuration we get from
+ * entity-manager via D-Bus. Each Code Updater can create its own configuration
+ * class that inherits from this to store additional properties from their
+ * device configuration, like bus/address/...
+ */
+class SoftwareConfig
+{
+  public:
+    SoftwareConfig(const std::string& objPath, uint32_t vendorIANA,
+                   const std::string& compatible, const std::string& configType,
+                   const std::string& name);
+
+    // The dbus object path this configuration was fetched from
+    const std::string objectPath;
+
+    // https://github.com/openbmc/entity-manager/blob/master/schemas/firmware.json
+
+    // 'Name' field from the EM config
+    const std::string configName;
+
+    // 'Type' field from the EM config
+    const std::string configType;
+
+    // @returns        the object path of the inventory item which
+    //                 can be associated with this device.
+    sdbusplus::async::task<std::string> getInventoryItemObjectPath(
+        sdbusplus::async::context& ctx);
+
+  private:
+    // 'VendorIANA' field from the EM config
+    const uint32_t vendorIANA; // e.g. "0x0000A015", 4 bytes as per PLDM spec
+
+    // 'CompatibleHardware' field from the EM config
+    const std::string
+        compatibleHardware; // e.g.
+                            // "com.meta.Hardware.Yosemite4.MedusaBoard.CPLD.LCMX02_2000HC"
+
+    friend Device;
+};
+
+}; // namespace phosphor::software::config
diff --git a/common/include/software_manager.hpp b/common/include/software_manager.hpp
new file mode 100644
index 0000000..7cc2e6a
--- /dev/null
+++ b/common/include/software_manager.hpp
@@ -0,0 +1,70 @@
+#pragma once
+
+#include "device.hpp"
+
+#include <boost/asio/steady_timer.hpp>
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/asio/connection.hpp>
+#include <sdbusplus/asio/object_server.hpp>
+#include <sdbusplus/async/context.hpp>
+#include <sdbusplus/timer.hpp>
+
+#include <string>
+
+using namespace phosphor::software::config;
+using namespace phosphor::software::device;
+
+namespace phosphor::software::manager
+{
+
+// This is the base class for the code updater
+// Every code updater can inherit from this
+class SoftwareManager
+{
+  public:
+    SoftwareManager(sdbusplus::async::context& ctx,
+                    const std::string& serviceNameSuffix);
+
+    // Fetches initial configuration from dbus and initializes devices.
+    // This should be called once by a code updater at startup.
+    // @param configurationInterfaces    the dbus interfaces from which to fetch
+    // configuration
+    sdbusplus::async::task<> initDevices(
+        const std::vector<std::string>& configurationInterfaces);
+
+    // Map of EM config object path to device.
+    std::map<sdbusplus::message::object_path, std::unique_ptr<Device>> devices;
+
+  protected:
+    // This function receives a dbus name and object path for a single device,
+    // which was configured.
+    // The component code updater overrides this function and may create a
+    // device instance internally, or reject the configuration as invalid.
+    // @param service       The dbus name where our configuration is
+    // @param config        The common configuration properties which are shared
+    // by all devices.
+    //                      Also includes the object path to fetch other
+    //                      configuration properties.
+    // @returns true        if the configuration was accepted
+    virtual sdbusplus::async::task<bool> initDevice(const std::string& service,
+                                                    const std::string& path,
+                                                    SoftwareConfig& config) = 0;
+
+    sdbusplus::async::context& ctx;
+
+  private:
+    // request the bus name on dbus after all configuration has been parsed
+    // and the devices have been initialized
+    // @returns        the name on dbus
+    std::string setupBusName();
+
+    // this is appended to the common prefix to construct the dbus name
+    std::string serviceNameSuffix;
+
+    sdbusplus::server::manager_t manager;
+
+    friend Software;
+    friend Device;
+};
+
+}; // namespace phosphor::software::manager
diff --git a/common/include/software_update.hpp b/common/include/software_update.hpp
new file mode 100644
index 0000000..406c282
--- /dev/null
+++ b/common/include/software_update.hpp
@@ -0,0 +1,37 @@
+#pragma once
+
+#include <sdbusplus/async/context.hpp>
+#include <xyz/openbmc_project/Software/Update/aserver.hpp>
+
+namespace phosphor::software
+{
+class Software;
+};
+
+using RequestedApplyTimes = sdbusplus::common::xyz::openbmc_project::software::
+    ApplyTime::RequestedApplyTimes;
+
+namespace phosphor::software::update
+{
+
+class SoftwareUpdate :
+    public sdbusplus::aserver::xyz::openbmc_project::software::Update<
+        SoftwareUpdate>
+{
+  public:
+    SoftwareUpdate(sdbusplus::async::context& ctx, const char* path,
+                   Software& software,
+                   const std::set<RequestedApplyTimes>& allowedApplyTimes);
+
+    auto method_call(start_update_t su, auto image, auto applyTime)
+        -> sdbusplus::async::task<start_update_t::return_type>;
+
+    auto get_property(allowed_apply_times_t aat) const;
+
+  private:
+    Software& software;
+
+    const std::set<RequestedApplyTimes> allowedApplyTimes;
+};
+
+}; // namespace phosphor::software::update
diff --git a/common/meson.build b/common/meson.build
index 766fbe0..5642b44 100644
--- a/common/meson.build
+++ b/common/meson.build
@@ -1,2 +1,17 @@
 
 subdir('pldm')
+
+software_common_lib = static_library('software_common_lib',
+  'src/software_manager.cpp',
+  'src/device.cpp',
+  'src/software_config.cpp',
+  'src/software.cpp',
+  'src/software_update.cpp',
+  include_directories: ['.','include/', common_include],
+  dependencies: [
+    pdi_dep,
+    phosphor_logging_dep,
+    sdbusplus_dep,
+    libpldm_dep,
+  ],
+)
diff --git a/common/pldm/pldm_package_util.cpp b/common/pldm/pldm_package_util.cpp
index c3e92e5..2d348a0 100644
--- a/common/pldm/pldm_package_util.cpp
+++ b/common/pldm/pldm_package_util.cpp
@@ -17,6 +17,9 @@
 
 #include <cassert>
 #include <cstring>
+#include <functional>
+
+PHOSPHOR_LOG2_USING;
 
 using namespace pldm::fw_update;
 
@@ -33,19 +36,19 @@
         pkgData.push_back(buf[i]);
     }
 
-    lg2::debug("parsing package header");
+    debug("parsing package header");
 
     std::unique_ptr<PackageParser> packageParser =
         pldm::fw_update::parsePackageHeader(pkgData);
 
     if (packageParser == nullptr)
     {
-        lg2::error("could not parse package header");
+        error("could not parse package header");
         return packageParser;
     }
 
-    lg2::debug("parsing package, pkg header size: {N}", "N",
-               packageParser->pkgHeaderSize);
+    debug("parsing package, pkg header size: {N}", "N",
+          packageParser->pkgHeaderSize);
 
     std::vector<uint8_t> pkgHeaderOnly;
     pkgHeaderOnly.insert(
@@ -57,24 +60,23 @@
     return packageParser;
 }
 
-int readImagePackage(FILE* file, uint8_t* package_data,
-                     const size_t package_size)
+int readImagePackage(FILE* file, uint8_t* packageData, const size_t packageSize)
 {
-    if (file == NULL || package_data == NULL)
+    if (file == NULL || packageData == NULL)
     {
         return 1;
     }
 
-    if (package_size == 0)
+    if (packageSize == 0)
     {
-        lg2::error("Package size is 0");
+        error("Package size is 0");
         return 1;
     }
 
-    lg2::debug("reading {NBYTES} bytes from file", "NBYTES", package_size);
+    debug("reading {NBYTES} bytes from file", "NBYTES", packageSize);
 
     // Read the package into memory
-    if (fread(package_data, 1, package_size, file) != package_size)
+    if (fread(packageData, 1, packageSize, file) != packageSize)
     {
         perror("Failed to read package data");
         fclose(file);
@@ -84,32 +86,42 @@
     return 0;
 }
 
-void* mmapImagePackage(sdbusplus::message::unix_fd image, size_t* size_out)
+std::unique_ptr<void, std::function<void(void*)>> mmapImagePackage(
+    sdbusplus::message::unix_fd image, size_t* sizeOut)
 {
-    lg2::debug("open fd {FD}", "FD", int(image));
+    debug("open fd {FD}", "FD", int(image));
 
     off_t size = lseek(image.fd, 0, SEEK_END);
 
     if (size < 0)
     {
-        lg2::error("failed to determine file size");
+        error("failed to determine file size");
         perror("error:");
-        return NULL;
+        return nullptr;
     }
 
-    *size_out = size;
+    *sizeOut = size;
 
-    lg2::debug("file size: {SIZE}", "SIZE", (uint64_t)size);
+    debug("file size: {SIZE}", "SIZE", (uint64_t)size);
 
     void* data = mmap(nullptr, size, PROT_READ, MAP_SHARED, image.fd, 0);
 
     if (data == MAP_FAILED)
     {
-        lg2::error("could not mmap the image");
-        return NULL;
+        error("could not mmap the image");
+        return nullptr;
     }
 
-    return data;
+    using mmapUniquePtr = std::unique_ptr<void, std::function<void(void*)>>;
+
+    mmapUniquePtr dataUnique(data, [size](void* arg) {
+        if (munmap(arg, size) != 0)
+        {
+            error("Failed to un map the PLDM package");
+        }
+    });
+
+    return dataUnique;
 }
 
 bool fwDeviceIDRecordMatchesCompatible(const FirmwareDeviceIDRecord& record,
@@ -130,8 +142,7 @@
 
     if (!std::holds_alternative<VendorDefinedDescriptorInfo>(v))
     {
-        lg2::debug(
-            "descriptor does not have the vendor defined descriptor info");
+        debug("descriptor does not have the vendor defined descriptor info");
         return false;
     }
 
@@ -154,7 +165,7 @@
 
     if (!desc.contains(0x1))
     {
-        lg2::error("did not find iana enterprise id");
+        error("did not find iana enterprise id");
         return false;
     }
 
@@ -162,7 +173,7 @@
 
     if (!std::holds_alternative<DescriptorData>(viana))
     {
-        lg2::error("did not find iana enterprise id");
+        error("did not find iana enterprise id");
         return false;
     }
 
@@ -170,7 +181,7 @@
 
     if (dd.size() != 4)
     {
-        lg2::error("descriptor data wrong size ( != 4) for vendor iana");
+        error("descriptor data wrong size ( != 4) for vendor iana");
         return false;
     }
 
@@ -204,7 +215,8 @@
 int extractMatchingComponentImage(
     const std::shared_ptr<PackageParser>& packageParser,
     const std::string& compatible, uint32_t vendorIANA,
-    uint32_t* component_offset_out, size_t* component_size_out)
+    uint32_t* componentOffsetOut, size_t* componentSizeOut,
+    std::string& componentVersionOut)
 {
     const FirmwareDeviceIDRecords& fwDeviceIdRecords =
         packageParser->getFwDeviceIDRecords();
@@ -215,7 +227,7 @@
 
     if (deviceDescriptorIndex < 0)
     {
-        lg2::error(
+        error(
             "did not find a matching device descriptor for {IANA}, {COMPATIBLE}",
             "IANA", lg2::hex, vendorIANA, "COMPATIBLE", compatible);
         return EXIT_FAILURE;
@@ -230,7 +242,7 @@
 
     if (ac.empty())
     {
-        lg2::error("did not find an applicable component image for the device");
+        error("did not find an applicable component image for the device");
         return EXIT_FAILURE;
     }
 
@@ -241,7 +253,7 @@
 
     if (component >= cs.size())
     {
-        lg2::error("applicable component out of bounds");
+        error("applicable component out of bounds");
         return EXIT_FAILURE;
     }
 
@@ -250,8 +262,9 @@
     CompLocationOffset off = std::get<5>(c);
     CompSize size = std::get<6>(c);
 
-    *component_offset_out = off;
-    *component_size_out = size;
+    *componentOffsetOut = off;
+    *componentSizeOut = size;
+    componentVersionOut = std::get<7>(c);
 
     return EXIT_SUCCESS;
 }
diff --git a/common/pldm/pldm_package_util.hpp b/common/pldm/pldm_package_util.hpp
index e510be1..4b544c2 100644
--- a/common/pldm/pldm_package_util.hpp
+++ b/common/pldm/pldm_package_util.hpp
@@ -4,6 +4,7 @@
 #include "sdbusplus/message/native_types.hpp"
 
 #include <cstdint>
+#include <functional>
 #include <memory>
 
 using namespace pldm::fw_update;
@@ -19,24 +20,27 @@
 
 // reads into a buffer, from file
 // @param file            the file to read from
-// @param package_data    the pre-allocated buffer for the package data
-// @param package_size    how many bytes to read from the file
-int readImagePackage(FILE* file, uint8_t* package_data, size_t package_size);
+// @param packageData     the pre-allocated buffer for the package data
+// @param packageSize     how many bytes to read from the file
+int readImagePackage(FILE* file, uint8_t* packageData, size_t packageSize);
 
 // @param image        file descriptor to the package
-// @param size_out     function will write the size of the package here
-// @returns            a pointer to the mmapped pldm package
-void* mmapImagePackage(sdbusplus::message::unix_fd image, size_t* size_out);
+// @param sizeOut      function will write the size of the package here
+// @returns            a unique pointer to the mmapped pldm package
+std::unique_ptr<void, std::function<void(void*)>> mmapImagePackage(
+    sdbusplus::message::unix_fd image, size_t* sizeOut);
 
 // @param packageParser          PackageParser instance
 // @param compatible             'compatible' string of device
 // @param vendorIANA             vendor iana of device
-// @param component_offset_out   function returns offset of component image
-// @param component_size_out     function returns size of component image
+// @param componentOffsetOut     function returns offset of component image
+// @param componentSizeOut       function returns size of component image
+// @param componentVersionOut    function returns version of component image
 // @returns                      0 on success
 int extractMatchingComponentImage(
     const std::shared_ptr<PackageParser>& packageParser,
     const std::string& compatible, uint32_t vendorIANA,
-    uint32_t* component_offset_out, size_t* size_out);
+    uint32_t* componentOffsetOut, size_t* componentSizeOut,
+    std::string& componentVersionOut);
 
 } // namespace pldm_package_util
diff --git a/common/src/device.cpp b/common/src/device.cpp
new file mode 100644
index 0000000..471697c
--- /dev/null
+++ b/common/src/device.cpp
@@ -0,0 +1,213 @@
+#include "device.hpp"
+
+#include "common/pldm/pldm_package_util.hpp"
+#include "software.hpp"
+#include "software_manager.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/asio/object_server.hpp>
+#include <sdbusplus/async/context.hpp>
+#include <sdbusplus/bus.hpp>
+#include <xyz/openbmc_project/Association/Definitions/server.hpp>
+#include <xyz/openbmc_project/State/Host/client.hpp>
+
+#include <utility>
+
+PHOSPHOR_LOG2_USING;
+
+using namespace phosphor::software::device;
+
+const auto applyTimeImmediate = sdbusplus::common::xyz::openbmc_project::
+    software::ApplyTime::RequestedApplyTimes::Immediate;
+
+const auto ActivationInvalid = ActivationInterface::Activations::Invalid;
+const auto ActivationFailed = ActivationInterface::Activations::Failed;
+
+Device::Device(sdbusplus::async::context& ctx, const SoftwareConfig& config,
+               manager::SoftwareManager* parent,
+               std::set<RequestedApplyTimes> allowedApplyTimes =
+                   {RequestedApplyTimes::Immediate,
+                    RequestedApplyTimes::OnReset}) :
+    allowedApplyTimes(std::move(allowedApplyTimes)), config(config),
+    parent(parent), ctx(ctx)
+{}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> Device::getImageInfo(
+    std::unique_ptr<void, std::function<void(void*)>>& pldmPackage,
+    size_t pldmPackageSize, uint8_t** matchingComponentImage,
+    size_t* componentImageSize, std::string& componentVersion)
+
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    std::shared_ptr<PackageParser> packageParser =
+        pldm_package_util::parsePLDMPackage(
+            static_cast<uint8_t*>(pldmPackage.get()), pldmPackageSize);
+
+    if (packageParser == nullptr)
+    {
+        error("could not parse PLDM package");
+        co_return false;
+    }
+
+    uint32_t componentOffset = 0;
+    const int status = pldm_package_util::extractMatchingComponentImage(
+        packageParser, config.compatibleHardware, config.vendorIANA,
+        &componentOffset, componentImageSize, componentVersion);
+
+    if (status != 0)
+    {
+        error("could not extract matching component image");
+        co_return false;
+    }
+
+    *matchingComponentImage =
+        static_cast<uint8_t*>(pldmPackage.get()) + componentOffset;
+
+    co_return true;
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> Device::startUpdateAsync(
+    sdbusplus::message::unix_fd image, RequestedApplyTimes applyTime,
+    std::unique_ptr<Software> softwarePendingIn)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    debug("starting the async update with memfd {FD}", "FD", image.fd);
+
+    size_t pldm_pkg_size = 0;
+    auto pldm_pkg = pldm_package_util::mmapImagePackage(image, &pldm_pkg_size);
+
+    if (pldm_pkg == nullptr)
+    {
+        softwarePendingIn->setActivation(ActivationInvalid);
+        co_return false;
+    }
+
+    uint8_t* componentImage;
+    size_t componentImageSize = 0;
+    std::string componentVersion;
+
+    if (!co_await getImageInfo(pldm_pkg, pldm_pkg_size, &componentImage,
+                               &componentImageSize, componentVersion))
+    {
+        error("could not extract matching component image");
+        softwarePendingIn->setActivation(ActivationInvalid);
+        co_return false;
+    }
+
+    const bool success = co_await continueUpdateWithMappedPackage(
+        componentImage, componentImageSize, componentVersion, applyTime,
+        softwarePendingIn);
+
+    if (success)
+    {
+        if (applyTime == RequestedApplyTimes::Immediate)
+        {
+            softwareCurrent = std::move(softwarePendingIn);
+
+            // In case an immediate update is triggered after an update for
+            // onReset.
+            softwarePending = nullptr;
+
+            debug("Successfully updated to software version {SWID}", "SWID",
+                  softwareCurrent->swid);
+        }
+        else if (applyTime == RequestedApplyTimes::OnReset)
+        {
+            softwarePending = std::move(softwarePendingIn);
+        }
+    }
+    else
+    {
+        softwarePendingIn->setActivation(ActivationFailed);
+        error("Failed to update the software for {SWID}", "SWID",
+              softwareCurrent->swid);
+    }
+
+    co_return success;
+}
+
+std::string Device::getEMConfigType() const
+{
+    return config.configType;
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> Device::resetDevice()
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    debug("Default implementation for device reset");
+
+    co_return true;
+}
+
+bool Device::setUpdateProgress(uint8_t progress) const
+{
+    if (!softwarePending || !softwarePending->softwareActivationProgress)
+    {
+        return false;
+    }
+
+    softwarePending->softwareActivationProgress->setProgress(progress);
+
+    return true;
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> Device::continueUpdateWithMappedPackage(
+    const uint8_t* matchingComponentImage, size_t componentImageSize,
+    const std::string& componentVersion, RequestedApplyTimes applyTime,
+    const std::unique_ptr<Software>& softwarePendingIn)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    softwarePendingIn->setActivation(ActivationInterface::Activations::Ready);
+
+    softwarePendingIn->setVersion(componentVersion);
+
+    std::string objPath = softwarePendingIn->objectPath;
+
+    softwarePendingIn->softwareActivationProgress =
+        std::make_unique<SoftwareActivationProgress>(ctx, objPath.c_str());
+
+    softwarePendingIn->setActivationBlocksTransition(true);
+
+    softwarePendingIn->setActivation(
+        ActivationInterface::Activations::Activating);
+
+    bool success =
+        co_await updateDevice(matchingComponentImage, componentImageSize);
+
+    if (success)
+    {
+        softwarePendingIn->setActivation(
+            ActivationInterface::Activations::Active);
+    }
+
+    softwarePendingIn->setActivationBlocksTransition(false);
+
+    softwarePendingIn->softwareActivationProgress = nullptr;
+
+    if (!success)
+    {
+        // do not apply the update, it has failed.
+        // We can delete the new software version.
+
+        co_return false;
+    }
+
+    if (applyTime == applyTimeImmediate)
+    {
+        co_await resetDevice();
+
+        co_await softwarePendingIn->createInventoryAssociations(true);
+
+        softwarePendingIn->enableUpdate(allowedApplyTimes);
+    }
+    else
+    {
+        co_await softwarePendingIn->createInventoryAssociations(false);
+    }
+
+    co_return true;
+}
diff --git a/common/src/software.cpp b/common/src/software.cpp
new file mode 100644
index 0000000..03e9199
--- /dev/null
+++ b/common/src/software.cpp
@@ -0,0 +1,174 @@
+#include "software.hpp"
+
+#include "device.hpp"
+#include "software_update.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/async/context.hpp>
+#include <xyz/openbmc_project/Association/Definitions/server.hpp>
+#include <xyz/openbmc_project/Software/Activation/aserver.hpp>
+#include <xyz/openbmc_project/Software/Update/aserver.hpp>
+#include <xyz/openbmc_project/State/Host/client.hpp>
+
+PHOSPHOR_LOG2_USING;
+
+using namespace phosphor::software;
+using namespace phosphor::software::device;
+using namespace phosphor::software::config;
+using namespace phosphor::software::update;
+
+const static std::string baseObjPathSoftware = "/xyz/openbmc_project/software/";
+
+SoftwareActivationProgress::SoftwareActivationProgress(
+    sdbusplus::async::context& ctx, const char* objPath) :
+    ActivationProgress(ctx, objPath)
+{
+    // This prevents "Conditional jump or move depends on uninitialised
+    // value(s)"
+    // when properties are updated for the first time
+    progress_ = 0;
+}
+
+void SoftwareActivationProgress::setProgress(int progressArg)
+{
+    progress(progressArg);
+}
+
+Software::Software(sdbusplus::async::context& ctx, Device& parent) :
+    Software(ctx, parent, getRandomSoftwareId(parent))
+{}
+
+Software::Software(sdbusplus::async::context& ctx, Device& parent,
+                   const std::string& swid) :
+    SoftwareActivation(ctx, (baseObjPathSoftware + swid).c_str()),
+    objectPath(baseObjPathSoftware + swid), parentDevice(parent), swid(swid),
+    ctx(ctx)
+{
+    // initialize the members of our base class to prevent
+    // "Conditional jump or move depends on uninitialised value(s)"
+    activation_ = Activations::NotReady;
+    requested_activation_ = RequestedActivations::None;
+
+    std::string objPath = baseObjPathSoftware + swid;
+
+    debug("{SWID}: created dbus interfaces on path {OBJPATH}", "SWID", swid,
+          "OBJPATH", objPath);
+};
+
+static long int getRandomId()
+{
+    struct timespec ts;
+    clock_gettime(CLOCK_REALTIME, &ts);
+    unsigned int seed = ts.tv_nsec ^ getpid();
+    srandom(seed);
+    return random() % 10000;
+}
+
+std::string Software::getRandomSoftwareId(Device& parent)
+{
+    return std::format("{}_{}", parent.config.configName, getRandomId());
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<> Software::createInventoryAssociations(bool isRunning)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    debug("{SWID}: setting association definitions", "SWID", swid);
+
+    std::string endpoint = "";
+
+    try
+    {
+        endpoint = co_await parentDevice.config.getInventoryItemObjectPath(ctx);
+    }
+    catch (std::exception& e)
+    {
+        error(e.what());
+    }
+
+    if (!associationDefinitions)
+    {
+        std::string path = objectPath;
+        associationDefinitions =
+            std::make_unique<SoftwareAssociationDefinitions>(ctx, path.c_str());
+    }
+
+    std::vector<std::tuple<std::string, std::string, std::string>> assocs;
+
+    if (endpoint.empty())
+    {
+        associationDefinitions->associations(assocs);
+        co_return;
+    }
+
+    if (isRunning)
+    {
+        debug("{SWID}: creating 'running' association to {OBJPATH}", "SWID",
+              swid, "OBJPATH", endpoint);
+        std::tuple<std::string, std::string, std::string> assocRunning = {
+            "running", "ran_on", endpoint};
+        assocs.push_back(assocRunning);
+    }
+    else
+    {
+        debug("{SWID}: creating 'activating' association to {OBJPATH}", "SWID",
+              swid, "OBJPATH", endpoint);
+        std::tuple<std::string, std::string, std::string> assocActivating = {
+            "activating", "activated_on", endpoint};
+        assocs.push_back(assocActivating);
+    }
+
+    associationDefinitions->associations(assocs);
+
+    co_return;
+}
+
+void Software::setVersion(const std::string& versionStr)
+{
+    debug("{SWID}: set version {VERSION}", "SWID", swid, "VERSION", versionStr);
+
+    if (version)
+    {
+        error("{SWID}: version was already set", "SWID", swid);
+        return;
+    }
+
+    version = std::make_unique<SoftwareVersion>(ctx, objectPath.str.c_str());
+    version->version(versionStr);
+}
+
+void Software::setActivationBlocksTransition(bool enabled)
+{
+    if (!enabled)
+    {
+        activationBlocksTransition = nullptr;
+        return;
+    }
+
+    std::string path = objectPath;
+    activationBlocksTransition =
+        std::make_unique<SoftwareActivationBlocksTransition>(ctx, path.c_str());
+}
+
+void Software::setActivation(SoftwareActivation::Activations act)
+{
+    activation(act);
+}
+
+void Software::enableUpdate(
+    const std::set<RequestedApplyTimes>& allowedApplyTimes)
+{
+    if (updateIntf != nullptr)
+    {
+        error("[Software] update of {OBJPATH} has already been enabled",
+              "OBJPATH", objectPath);
+        return;
+    }
+
+    debug(
+        "[Software] enabling update of {OBJPATH} (adding the update interface)",
+        "OBJPATH", objectPath);
+
+    updateIntf = std::make_unique<SoftwareUpdate>(ctx, objectPath.str.c_str(),
+                                                  *this, allowedApplyTimes);
+}
diff --git a/common/src/software_config.cpp b/common/src/software_config.cpp
new file mode 100644
index 0000000..8fb3162
--- /dev/null
+++ b/common/src/software_config.cpp
@@ -0,0 +1,70 @@
+#include "common/include/software_config.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <xyz/openbmc_project/ObjectMapper/client.hpp>
+
+#include <regex>
+#include <stdexcept>
+
+PHOSPHOR_LOG2_USING;
+
+using namespace phosphor::software::config;
+
+SoftwareConfig::SoftwareConfig(const std::string& objPath, uint32_t vendorIANA,
+                               const std::string& compatible,
+                               const std::string& configType,
+                               const std::string& name) :
+    objectPath(objPath), configName(name), configType(configType),
+    vendorIANA(vendorIANA), compatibleHardware(compatible)
+{
+    std::regex reCompatible("([a-zA-Z0-9])+(\\.([a-zA-Z0-9])+)+");
+    std::cmatch m;
+
+    if (name.empty())
+    {
+        throw std::invalid_argument(
+            "invalid EM config 'Name' string: '" + name + "'");
+    }
+
+    // check compatible string with regex
+    if (!std::regex_match(compatible.c_str(), m, reCompatible))
+    {
+        throw std::invalid_argument(
+            "invalid compatible string: '" + compatible + "'");
+    }
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<std::string> SoftwareConfig::getInventoryItemObjectPath(
+    sdbusplus::async::context& ctx)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    std::vector<std::string> allInterfaces = {
+        "xyz.openbmc_project.Inventory.Item.Board",
+        "xyz.openbmc_project.Inventory.Item.Chassis",
+    };
+
+    auto client = sdbusplus::client::xyz::openbmc_project::ObjectMapper<>(ctx)
+                      .service("xyz.openbmc_project.ObjectMapper")
+                      .path("/xyz/openbmc_project/object_mapper");
+    auto res = co_await client.get_sub_tree(
+        "/xyz/openbmc_project/inventory/system", 0, allInterfaces);
+
+    for (auto& [path, v] : res)
+    {
+        debug("inventory item at path {PATH}", "PATH", path);
+
+        // check if their path is a parent of our path
+        if (objectPath.starts_with(path))
+        {
+            debug("found associated inventory item for {NAME}: {PATH}", "NAME",
+                  configName, "PATH", path);
+            co_return path;
+        }
+    }
+
+    error("could not find associated inventory item for {NAME}", "NAME",
+          configName);
+
+    co_return "";
+}
diff --git a/common/src/software_manager.cpp b/common/src/software_manager.cpp
new file mode 100644
index 0000000..9c80e62
--- /dev/null
+++ b/common/src/software_manager.cpp
@@ -0,0 +1,136 @@
+#include "software_manager.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/asio/object_server.hpp>
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/async/context.hpp>
+#include <sdbusplus/bus.hpp>
+#include <xyz/openbmc_project/Association/Definitions/server.hpp>
+#include <xyz/openbmc_project/ObjectMapper/client.hpp>
+#include <xyz/openbmc_project/State/Host/client.hpp>
+
+#include <cstdint>
+
+PHOSPHOR_LOG2_USING;
+
+using namespace phosphor::software::manager;
+
+SoftwareManager::SoftwareManager(sdbusplus::async::context& ctx,
+                                 const std::string& serviceNameSuffix) :
+    ctx(ctx), serviceNameSuffix(serviceNameSuffix), manager(ctx, "/")
+{
+    debug("initialized SoftwareManager");
+}
+
+std::string SoftwareManager::setupBusName()
+{
+    const std::string serviceNameFull =
+        "xyz.openbmc_project.Software." + serviceNameSuffix;
+
+    debug("requesting dbus name {BUSNAME}", "BUSNAME", serviceNameFull);
+
+    ctx.get_bus().request_name(serviceNameFull.c_str());
+
+    return serviceNameFull;
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<> SoftwareManager::initDevices(
+    const std::vector<std::string>& configurationInterfaces)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    auto client = sdbusplus::client::xyz::openbmc_project::ObjectMapper<>(ctx)
+                      .service("xyz.openbmc_project.ObjectMapper")
+                      .path("/xyz/openbmc_project/object_mapper");
+
+    auto res =
+        co_await client.get_sub_tree("/xyz/openbmc_project/inventory", 0, {});
+
+    for (auto& iface : configurationInterfaces)
+    {
+        debug("[config] looking for dbus interface {INTF}", "INTF", iface);
+    }
+
+    for (auto& [path, v] : res)
+    {
+        for (auto& [service, interfaceNames] : v)
+        {
+            std::string interfaceFound;
+
+            for (std::string& interfaceName : interfaceNames)
+            {
+                for (auto& iface : configurationInterfaces)
+                {
+                    if (interfaceName == iface)
+                    {
+                        interfaceFound = interfaceName;
+                    }
+                }
+            }
+
+            if (interfaceFound.empty())
+            {
+                continue;
+            }
+
+            debug(
+                "[config] found configuration interface at {SERVICE}, {OBJPATH}",
+                "SERVICE", service, "OBJPATH", path);
+
+            auto client =
+                sdbusplus::async::proxy().service(service).path(path).interface(
+                    "org.freedesktop.DBus.Properties");
+
+            uint64_t vendorIANA = 0;
+            std::string compatible{};
+            std::string emConfigType{};
+            std::string emConfigName{};
+
+            try
+            {
+                {
+                    auto propVendorIANA =
+                        co_await client.call<std::variant<uint64_t>>(
+                            ctx, "Get", interfaceFound, "VendorIANA");
+
+                    vendorIANA = std::get<uint64_t>(propVendorIANA);
+                }
+                {
+                    auto propCompatible =
+                        co_await client.call<std::variant<std::string>>(
+                            ctx, "Get", interfaceFound, "Compatible");
+
+                    compatible = std::get<std::string>(propCompatible);
+                }
+                {
+                    auto propEMConfigType =
+                        co_await client.call<std::variant<std::string>>(
+                            ctx, "Get", interfaceFound, "Type");
+
+                    emConfigType = std::get<std::string>(propEMConfigType);
+                }
+                {
+                    auto propEMConfigName =
+                        co_await client.call<std::variant<std::string>>(
+                            ctx, "Get", interfaceFound, "Name");
+
+                    emConfigName = std::get<std::string>(propEMConfigName);
+                }
+            }
+            catch (std::exception& e)
+            {
+                error(e.what());
+                continue;
+            }
+
+            SoftwareConfig config(path, vendorIANA, compatible, emConfigType,
+                                  emConfigName);
+
+            co_await initDevice(service, path, config);
+        }
+    }
+
+    debug("[config] done with initial configuration");
+
+    setupBusName();
+}
diff --git a/common/src/software_update.cpp b/common/src/software_update.cpp
new file mode 100644
index 0000000..26a4602
--- /dev/null
+++ b/common/src/software_update.cpp
@@ -0,0 +1,103 @@
+#include "software_update.hpp"
+
+#include "device.hpp"
+#include "software.hpp"
+
+#include <phosphor-logging/elog-errors.hpp>
+#include <phosphor-logging/elog.hpp>
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/async/context.hpp>
+#include <xyz/openbmc_project/Software/Update/aserver.hpp>
+
+PHOSPHOR_LOG2_USING;
+
+using Unavailable = sdbusplus::xyz::openbmc_project::Common::Error::Unavailable;
+
+using namespace phosphor::logging;
+using namespace phosphor::software::update;
+using namespace phosphor::software::device;
+using namespace phosphor::software;
+
+namespace SoftwareLogging = phosphor::logging::xyz::openbmc_project::software;
+namespace SoftwareErrors =
+    sdbusplus::error::xyz::openbmc_project::software::image;
+
+SoftwareUpdate::SoftwareUpdate(
+    sdbusplus::async::context& ctx, const char* path, Software& software,
+    const std::set<RequestedApplyTimes>& allowedApplyTimes) :
+    sdbusplus::aserver::xyz::openbmc_project::software::Update<SoftwareUpdate>(
+        ctx, path),
+    software(software), allowedApplyTimes(allowedApplyTimes)
+{}
+
+auto SoftwareUpdate::method_call(start_update_t /*unused*/, auto image,
+                                 auto applyTime)
+    -> sdbusplus::async::task<start_update_t::return_type>
+{
+    debug("Requesting Image update with {FD}", "FD", image.fd);
+
+    Device& device = software.parentDevice;
+
+    if (device.updateInProgress)
+    {
+        error("An update is already in progress, cannot update.");
+        report<Unavailable>();
+        co_return sdbusplus::message::object_path();
+    }
+
+    device.updateInProgress = true;
+
+    // check if the apply time is allowed by our device
+    if (!allowedApplyTimes.contains(applyTime))
+    {
+        error(
+            "the selected apply time {APPLYTIME} is not allowed by the device",
+            "APPLYTIME", applyTime);
+        device.updateInProgress = false;
+        report<Unavailable>();
+        co_return sdbusplus::message::object_path();
+    }
+
+    debug("started asynchronous update with fd {FD}", "FD", image.fd);
+
+    int imageDup = dup(image.fd);
+
+    if (imageDup < 0)
+    {
+        error("ERROR calling dup on fd: {ERR}", "ERR", strerror(errno));
+        device.updateInProgress = false;
+        co_return software.objectPath;
+    }
+
+    debug("starting async update with FD: {FD}\n", "FD", imageDup);
+
+    std::unique_ptr<Software> softwareInstance =
+        std::make_unique<Software>(ctx, device);
+
+    softwareInstance->setActivation(ActivationInterface::Activations::NotReady);
+
+    std::string newObjPath = softwareInstance->objectPath;
+
+    // NOLINTBEGIN(readability-static-accessed-through-instance)
+    ctx.spawn(
+        [](Device& device, int imageDup, RequestedApplyTimes applyTime,
+           std::unique_ptr<Software> swupdate) -> sdbusplus::async::task<> {
+            co_await device.startUpdateAsync(imageDup, applyTime,
+                                             std::move(swupdate));
+            device.updateInProgress = false;
+            close(imageDup);
+            co_return;
+        }(device, imageDup, applyTime, std::move(softwareInstance)));
+    // NOLINTEND
+
+    // We need the object path for the new software here.
+    // It must be the same as constructed during the update process.
+    // This is so that bmcweb and redfish clients can keep track of the update
+    // process.
+    co_return newObjPath;
+}
+
+auto SoftwareUpdate::get_property(allowed_apply_times_t /*unused*/) const
+{
+    return allowedApplyTimes;
+}
diff --git a/meson.build b/meson.build
index 64a272f..2864764 100644
--- a/meson.build
+++ b/meson.build
@@ -66,14 +66,17 @@
 
 subdir('bmc')
 
-if build_tests.allowed()
-    libpldm_dep = dependency('libpldm')
-endif
-
 common_include = include_directories('.')
 
+common_build = build_tests.allowed()
+
+if common_build
+  libpldm_dep = dependency('libpldm')
+
+  subdir('common')
+endif
+
 if build_tests.allowed()
-    subdir('common')
-    subdir('test')
+  subdir('test')
 endif