updater: add functions to modify dynamic partition metadata

Test: sideload full OTA on cuttlefish
Test: sideload incremental OTA on cuttlefish (that grows
      system, shrinks vendor, and move vendor to group foo)
Test: verify that /cache/recovery/cc46ebfd04058569d0c6c1431c6af6c1328458e4
      exists (sha1sum of "system")

Bug: 111801737

Change-Id: Ibdf6565bc1b60f3665c01739b4c95a85f0261ae5
diff --git a/updater/Android.bp b/updater/Android.bp
index c95ec5e..b80cdb3 100644
--- a/updater/Android.bp
+++ b/updater/Android.bp
@@ -26,12 +26,14 @@
         "libedify",
         "libotautil",
         "libext4_utils",
+        "libdm",
         "libfec",
         "libfec_rs",
         "libverity_tree",
         "libfs_mgr",
         "libgtest_prod",
         "liblog",
+        "liblp",
         "libselinux",
         "libsparse",
         "libsquashfs_utils",
@@ -66,6 +68,7 @@
     srcs: [
         "blockimg.cpp",
         "commands.cpp",
+        "dynamic_partitions.cpp",
         "install.cpp",
     ],
 
diff --git a/updater/Android.mk b/updater/Android.mk
index 78e32ba..c7a6ba9 100644
--- a/updater/Android.mk
+++ b/updater/Android.mk
@@ -29,12 +29,14 @@
     libedify \
     libotautil \
     libext4_utils \
+    libdm \
     libfec \
     libfec_rs \
     libverity_tree \
     libfs_mgr \
     libgtest_prod \
     liblog \
+    liblp \
     libselinux \
     libsparse \
     libsquashfs_utils \
diff --git a/updater/blockimg.cpp b/updater/blockimg.cpp
index e35d483..6e5d5bb 100644
--- a/updater/blockimg.cpp
+++ b/updater/blockimg.cpp
@@ -70,6 +70,7 @@
 static constexpr size_t BLOCKSIZE = 4096;
 static constexpr mode_t STASH_DIRECTORY_MODE = 0700;
 static constexpr mode_t STASH_FILE_MODE = 0600;
+static constexpr mode_t MARKER_DIRECTORY_MODE = 0700;
 
 static CauseCode failure_type = kNoCause;
 static bool is_retry = false;
@@ -167,15 +168,22 @@
   return true;
 }
 
-static bool SetPartitionUpdatedMarker(const std::string& marker) {
+bool SetUpdatedMarker(const std::string& marker) {
+  auto dirname = android::base::Dirname(marker);
+  auto res = mkdir(dirname.c_str(), MARKER_DIRECTORY_MODE);
+  if (res == -1 && errno != EEXIST) {
+    PLOG(ERROR) << "Failed to create directory for marker: " << dirname;
+    return false;
+  }
+
   if (!android::base::WriteStringToFile("", marker)) {
     PLOG(ERROR) << "Failed to write to marker file " << marker;
     return false;
   }
-  if (!FsyncDir(android::base::Dirname(marker))) {
+  if (!FsyncDir(dirname)) {
     return false;
   }
-  LOG(INFO) << "Wrote partition updated marker to " << marker;
+  LOG(INFO) << "Wrote updated marker to " << marker;
   return true;
 }
 
@@ -1573,6 +1581,43 @@
 
 using CommandMap = std::unordered_map<Command::Type, CommandFunction>;
 
+static bool Sha1DevicePath(const std::string& path, uint8_t digest[SHA_DIGEST_LENGTH]) {
+  auto device_name = android::base::Basename(path);
+  auto dm_target_name_path = "/sys/block/" + device_name + "/dm/name";
+
+  struct stat sb;
+  if (stat(dm_target_name_path.c_str(), &sb) == 0) {
+    // This is a device mapper target. Use partition name as part of the hash instead. Do not
+    // include extents as part of the hash, because the size of a partition may be shrunk after
+    // the patches are applied.
+    std::string dm_target_name;
+    if (!android::base::ReadFileToString(dm_target_name_path, &dm_target_name)) {
+      PLOG(ERROR) << "Cannot read " << dm_target_name_path;
+      return false;
+    }
+    SHA1(reinterpret_cast<const uint8_t*>(dm_target_name.data()), dm_target_name.size(), digest);
+    return true;
+  }
+
+  if (errno != ENOENT) {
+    // This is a device mapper target, but its name cannot be retrieved.
+    PLOG(ERROR) << "Cannot get dm target name for " << path;
+    return false;
+  }
+
+  // This doesn't appear to be a device mapper target, but if its name starts with dm-, something
+  // else might have gone wrong.
+  if (android::base::StartsWith(device_name, "dm-")) {
+    LOG(WARNING) << "Device " << path << " starts with dm- but is not mapped by device-mapper.";
+  }
+
+  // Stash directory should be different for each partition to avoid conflicts when updating
+  // multiple partitions at the same time, so we use the hash of the block device name as the base
+  // directory.
+  SHA1(reinterpret_cast<const uint8_t*>(path.data()), path.size(), digest);
+  return true;
+}
+
 static Value* PerformBlockImageUpdate(const char* name, State* state,
                                       const std::vector<std::unique_ptr<Expr>>& argv,
                                       const CommandMap& command_map, bool dryrun) {
@@ -1657,12 +1702,10 @@
     return StringValue("");
   }
 
-  // Stash directory should be different for each partition to avoid conflicts when updating
-  // multiple partitions at the same time, so we use the hash of the block device name as the base
-  // directory.
   uint8_t digest[SHA_DIGEST_LENGTH];
-  SHA1(reinterpret_cast<const uint8_t*>(blockdev_filename->data.data()),
-       blockdev_filename->data.size(), digest);
+  if (!Sha1DevicePath(blockdev_filename->data, digest)) {
+    return StringValue("");
+  }
   params.stashbase = print_sha1(digest);
 
   // Possibly do return early on retry, by checking the marker. If the update on this partition has
@@ -1884,7 +1927,7 @@
       // Create a marker on /cache partition, which allows skipping the update on this partition on
       // retry. The marker will be removed once booting into normal boot, or before starting next
       // fresh install.
-      if (!SetPartitionUpdatedMarker(updated_marker)) {
+      if (!SetUpdatedMarker(updated_marker)) {
         LOG(WARNING) << "Failed to set updated marker; continuing";
       }
     }
diff --git a/updater/dynamic_partitions.cpp b/updater/dynamic_partitions.cpp
new file mode 100644
index 0000000..b50dd75
--- /dev/null
+++ b/updater/dynamic_partitions.cpp
@@ -0,0 +1,435 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "updater/dynamic_partitions.h"
+
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <algorithm>
+#include <chrono>
+#include <iterator>
+#include <memory>
+#include <optional>
+#include <string>
+#include <type_traits>
+#include <vector>
+
+#include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/parseint.h>
+#include <android-base/strings.h>
+#include <fs_mgr.h>
+#include <fs_mgr_dm_linear.h>
+#include <libdm/dm.h>
+#include <liblp/builder.h>
+
+#include "edify/expr.h"
+#include "otautil/error_code.h"
+#include "otautil/paths.h"
+#include "private/utils.h"
+
+using android::base::ParseUint;
+using android::dm::DeviceMapper;
+using android::dm::DmDeviceState;
+using android::fs_mgr::CreateLogicalPartition;
+using android::fs_mgr::DestroyLogicalPartition;
+using android::fs_mgr::LpMetadata;
+using android::fs_mgr::MetadataBuilder;
+using android::fs_mgr::Partition;
+using android::fs_mgr::PartitionOpener;
+
+static constexpr std::chrono::milliseconds kMapTimeout{ 1000 };
+static constexpr char kMetadataUpdatedMarker[] = "/dynamic_partition_metadata.UPDATED";
+
+static std::string GetSuperDevice() {
+  return "/dev/block/by-name/" + fs_mgr_get_super_partition_name();
+}
+
+static std::vector<std::string> ReadStringArgs(const char* name, State* state,
+                                               const std::vector<std::unique_ptr<Expr>>& argv,
+                                               const std::vector<std::string>& arg_names) {
+  if (argv.size() != arg_names.size()) {
+    ErrorAbort(state, kArgsParsingFailure, "%s expects %zu arguments, got %zu", name,
+               arg_names.size(), argv.size());
+    return {};
+  }
+
+  std::vector<std::unique_ptr<Value>> args;
+  if (!ReadValueArgs(state, argv, &args)) {
+    return {};
+  }
+
+  CHECK_EQ(args.size(), arg_names.size());
+
+  for (size_t i = 0; i < arg_names.size(); ++i) {
+    if (args[i]->type != Value::Type::STRING) {
+      ErrorAbort(state, kArgsParsingFailure, "%s argument to %s must be string",
+                 arg_names[i].c_str(), name);
+      return {};
+    }
+  }
+
+  std::vector<std::string> ret;
+  std::transform(args.begin(), args.end(), std::back_inserter(ret),
+                 [](const auto& arg) { return arg->data; });
+  return ret;
+}
+
+static bool UnmapPartitionOnDeviceMapper(const std::string& partition_name) {
+  auto state = DeviceMapper::Instance().GetState(partition_name);
+  if (state == DmDeviceState::INVALID) {
+    return true;
+  }
+  if (state == DmDeviceState::ACTIVE) {
+    return DestroyLogicalPartition(partition_name, kMapTimeout);
+  }
+  LOG(ERROR) << "Unknown device mapper state: "
+             << static_cast<std::underlying_type_t<DmDeviceState>>(state);
+  return false;
+}
+
+static bool MapPartitionOnDeviceMapper(const std::string& partition_name, std::string* path) {
+  auto state = DeviceMapper::Instance().GetState(partition_name);
+  if (state == DmDeviceState::INVALID) {
+    return CreateLogicalPartition(GetSuperDevice(), 0 /* metadata slot */, partition_name,
+                                  true /* force writable */, kMapTimeout, path);
+  }
+
+  if (state == DmDeviceState::ACTIVE) {
+    return DeviceMapper::Instance().GetDmDevicePathByName(partition_name, path);
+  }
+  LOG(ERROR) << "Unknown device mapper state: "
+             << static_cast<std::underlying_type_t<DmDeviceState>>(state);
+  return false;
+}
+
+Value* UnmapPartitionFn(const char* name, State* state,
+                        const std::vector<std::unique_ptr<Expr>>& argv) {
+  auto args = ReadStringArgs(name, state, argv, { "name" });
+  if (args.empty()) return StringValue("");
+
+  return UnmapPartitionOnDeviceMapper(args[0]) ? StringValue("t") : StringValue("");
+}
+
+Value* MapPartitionFn(const char* name, State* state,
+                      const std::vector<std::unique_ptr<Expr>>& argv) {
+  auto args = ReadStringArgs(name, state, argv, { "name" });
+  if (args.empty()) return StringValue("");
+
+  std::string path;
+  bool result = MapPartitionOnDeviceMapper(args[0], &path);
+  return result ? StringValue(path) : StringValue("");
+}
+
+namespace {  // Ops
+
+struct OpParameters {
+  std::vector<std::string> tokens;
+  MetadataBuilder* builder;
+
+  bool ExpectArgSize(size_t size) const {
+    CHECK(!tokens.empty());
+    auto actual = tokens.size() - 1;
+    if (actual != size) {
+      LOG(ERROR) << "Op " << op() << " expects " << size << " args, got " << actual;
+      return false;
+    }
+    return true;
+  }
+  const std::string& op() const {
+    CHECK(!tokens.empty());
+    return tokens[0];
+  }
+  const std::string& arg(size_t pos) const {
+    CHECK_LE(pos + 1, tokens.size());
+    return tokens[pos + 1];
+  }
+  std::optional<uint64_t> uint_arg(size_t pos, const std::string& name) const {
+    auto str = arg(pos);
+    uint64_t ret;
+    if (!ParseUint(str, &ret)) {
+      LOG(ERROR) << "Op " << op() << " expects uint64 for argument " << name << ", got " << str;
+      return std::nullopt;
+    }
+    return ret;
+  }
+};
+
+using OpFunction = std::function<bool(const OpParameters&)>;
+using OpMap = std::map<std::string, OpFunction>;
+
+bool PerformOpResize(const OpParameters& params) {
+  if (!params.ExpectArgSize(2)) return false;
+  const auto& partition_name = params.arg(0);
+  auto size = params.uint_arg(1, "size");
+  if (!size.has_value()) return false;
+
+  auto partition = params.builder->FindPartition(partition_name);
+  if (partition == nullptr) {
+    LOG(ERROR) << "Failed to find partition " << partition_name
+               << " in dynamic partition metadata.";
+    return false;
+  }
+  if (!UnmapPartitionOnDeviceMapper(partition_name)) {
+    LOG(ERROR) << "Cannot unmap " << partition_name << " before resizing.";
+    return false;
+  }
+  if (!params.builder->ResizePartition(partition, size.value())) {
+    LOG(ERROR) << "Failed to resize partition " << partition_name << " to size " << *size << ".";
+    return false;
+  }
+  return true;
+}
+
+bool PerformOpRemove(const OpParameters& params) {
+  if (!params.ExpectArgSize(1)) return false;
+  const auto& partition_name = params.arg(0);
+
+  if (!UnmapPartitionOnDeviceMapper(partition_name)) {
+    LOG(ERROR) << "Cannot unmap " << partition_name << " before removing.";
+    return false;
+  }
+  params.builder->RemovePartition(partition_name);
+  return true;
+}
+
+bool PerformOpAdd(const OpParameters& params) {
+  if (!params.ExpectArgSize(2)) return false;
+  const auto& partition_name = params.arg(0);
+  const auto& group_name = params.arg(1);
+
+  if (params.builder->AddPartition(partition_name, group_name, LP_PARTITION_ATTR_READONLY) ==
+      nullptr) {
+    LOG(ERROR) << "Failed to add partition " << partition_name << " to group " << group_name << ".";
+    return false;
+  }
+  return true;
+}
+
+bool PerformOpMove(const OpParameters& params) {
+  if (!params.ExpectArgSize(2)) return false;
+  const auto& partition_name = params.arg(0);
+  const auto& new_group = params.arg(1);
+
+  auto partition = params.builder->FindPartition(partition_name);
+  if (partition == nullptr) {
+    LOG(ERROR) << "Cannot move partition " << partition_name << " to group " << new_group
+               << " because it is not found.";
+    return false;
+  }
+
+  auto old_group = partition->group_name();
+  if (old_group != new_group) {
+    if (!params.builder->ChangePartitionGroup(partition, new_group)) {
+      LOG(ERROR) << "Cannot move partition " << partition_name << " from group " << old_group
+                 << " to group " << new_group << ".";
+      return false;
+    }
+  }
+  return true;
+}
+
+bool PerformOpAddGroup(const OpParameters& params) {
+  if (!params.ExpectArgSize(2)) return false;
+  const auto& group_name = params.arg(0);
+  auto maximum_size = params.uint_arg(1, "maximum_size");
+  if (!maximum_size.has_value()) return false;
+
+  auto group = params.builder->FindGroup(group_name);
+  if (group != nullptr) {
+    LOG(ERROR) << "Cannot add group " << group_name << " because it already exists.";
+    return false;
+  }
+
+  if (maximum_size.value() == 0) {
+    LOG(WARNING) << "Adding group " << group_name << " with no size limits.";
+  }
+
+  if (!params.builder->AddGroup(group_name, maximum_size.value())) {
+    LOG(ERROR) << "Failed to add group " << group_name << " with maximum size "
+               << maximum_size.value() << ".";
+    return false;
+  }
+  return true;
+}
+
+bool PerformOpResizeGroup(const OpParameters& params) {
+  if (!params.ExpectArgSize(2)) return false;
+  const auto& group_name = params.arg(0);
+  auto new_size = params.uint_arg(1, "maximum_size");
+  if (!new_size.has_value()) return false;
+
+  auto group = params.builder->FindGroup(group_name);
+  if (group == nullptr) {
+    LOG(ERROR) << "Cannot resize group " << group_name << " because it is not found.";
+    return false;
+  }
+
+  auto old_size = group->maximum_size();
+  if (old_size != new_size.value()) {
+    if (!params.builder->ChangeGroupSize(group_name, new_size.value())) {
+      LOG(ERROR) << "Cannot resize group " << group_name << " from " << old_size << " to "
+                 << new_size.value() << ".";
+      return false;
+    }
+  }
+  return true;
+}
+
+std::vector<std::string> ListPartitionNamesInGroup(MetadataBuilder* builder,
+                                                   const std::string& group_name) {
+  auto partitions = builder->ListPartitionsInGroup(group_name);
+  std::vector<std::string> partition_names;
+  std::transform(partitions.begin(), partitions.end(), std::back_inserter(partition_names),
+                 [](Partition* partition) { return partition->name(); });
+  return partition_names;
+}
+
+bool PerformOpRemoveGroup(const OpParameters& params) {
+  if (!params.ExpectArgSize(1)) return false;
+  const auto& group_name = params.arg(0);
+
+  auto partition_names = ListPartitionNamesInGroup(params.builder, group_name);
+  if (!partition_names.empty()) {
+    LOG(ERROR) << "Cannot remove group " << group_name << " because it still contains partitions ["
+               << android::base::Join(partition_names, ", ") << "]";
+    return false;
+  }
+  params.builder->RemoveGroupAndPartitions(group_name);
+  return true;
+}
+
+bool PerformOpRemoveAllGroups(const OpParameters& params) {
+  if (!params.ExpectArgSize(0)) return false;
+
+  auto group_names = params.builder->ListGroups();
+  for (const auto& group_name : group_names) {
+    auto partition_names = ListPartitionNamesInGroup(params.builder, group_name);
+    for (const auto& partition_name : partition_names) {
+      if (!UnmapPartitionOnDeviceMapper(partition_name)) {
+        LOG(ERROR) << "Cannot unmap " << partition_name << " before removing group " << group_name
+                   << ".";
+        return false;
+      }
+    }
+    params.builder->RemoveGroupAndPartitions(group_name);
+  }
+  return true;
+}
+
+}  // namespace
+
+Value* UpdateDynamicPartitionsFn(const char* name, State* state,
+                                 const std::vector<std::unique_ptr<Expr>>& argv) {
+  if (argv.size() != 1) {
+    ErrorAbort(state, kArgsParsingFailure, "%s expects 1 arguments, got %zu", name, argv.size());
+    return StringValue("");
+  }
+  std::vector<std::unique_ptr<Value>> args;
+  if (!ReadValueArgs(state, argv, &args)) {
+    return nullptr;
+  }
+  const std::unique_ptr<Value>& op_list_value = args[0];
+  if (op_list_value->type != Value::Type::BLOB) {
+    ErrorAbort(state, kArgsParsingFailure, "op_list argument to %s must be blob", name);
+    return StringValue("");
+  }
+
+  std::string updated_marker = Paths::Get().stash_directory_base() + kMetadataUpdatedMarker;
+  if (state->is_retry) {
+    struct stat sb;
+    int result = stat(updated_marker.c_str(), &sb);
+    if (result == 0) {
+      LOG(INFO) << "Skipping already updated dynamic partition metadata based on marker";
+      return StringValue("t");
+    }
+  } else {
+    // Delete the obsolete marker if any.
+    std::string err;
+    if (!android::base::RemoveFileIfExists(updated_marker, &err)) {
+      LOG(ERROR) << "Failed to remove dynamic partition metadata updated marker " << updated_marker
+                 << ": " << err;
+      return StringValue("");
+    }
+  }
+
+  auto super_device = GetSuperDevice();
+  auto builder = MetadataBuilder::New(PartitionOpener(), super_device, 0);
+  if (builder == nullptr) {
+    LOG(ERROR) << "Failed to load dynamic partition metadata.";
+    return StringValue("");
+  }
+
+  static const OpMap op_map{
+    // clang-format off
+    {"resize",                PerformOpResize},
+    {"remove",                PerformOpRemove},
+    {"add",                   PerformOpAdd},
+    {"move",                  PerformOpMove},
+    {"add_group",             PerformOpAddGroup},
+    {"resize_group",          PerformOpResizeGroup},
+    {"remove_group",          PerformOpRemoveGroup},
+    {"remove_all_groups",     PerformOpRemoveAllGroups},
+    // clang-format on
+  };
+
+  std::vector<std::string> lines = android::base::Split(op_list_value->data, "\n");
+  for (const auto& line : lines) {
+    auto comment_idx = line.find('#');
+    auto op_and_args = comment_idx == std::string::npos ? line : line.substr(0, comment_idx);
+    op_and_args = android::base::Trim(op_and_args);
+    if (op_and_args.empty()) continue;
+
+    auto tokens = android::base::Split(op_and_args, " ");
+    const auto& op = tokens[0];
+    auto it = op_map.find(op);
+    if (it == op_map.end()) {
+      LOG(ERROR) << "Unknown operation in op_list: " << op;
+      return StringValue("");
+    }
+    OpParameters params;
+    params.tokens = tokens;
+    params.builder = builder.get();
+    if (!it->second(params)) {
+      return StringValue("");
+    }
+  }
+
+  auto metadata = builder->Export();
+  if (metadata == nullptr) {
+    LOG(ERROR) << "Failed to export metadata.";
+    return StringValue("");
+  }
+
+  if (!UpdatePartitionTable(super_device, *metadata, 0)) {
+    LOG(ERROR) << "Failed to write metadata.";
+    return StringValue("");
+  }
+
+  if (!SetUpdatedMarker(updated_marker)) {
+    LOG(ERROR) << "Failed to set metadata updated marker.";
+    return StringValue("");
+  }
+
+  return StringValue("t");
+}
+
+void RegisterDynamicPartitionsFunctions() {
+  RegisterFunction("unmap_partition", UnmapPartitionFn);
+  RegisterFunction("map_partition", MapPartitionFn);
+  RegisterFunction("update_dynamic_partitions", UpdateDynamicPartitionsFn);
+}
diff --git a/updater/include/private/utils.h b/updater/include/private/utils.h
new file mode 100644
index 0000000..33cf615
--- /dev/null
+++ b/updater/include/private/utils.h
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <string>
+
+bool SetUpdatedMarker(const std::string& marker);
diff --git a/updater/include/updater/dynamic_partitions.h b/updater/include/updater/dynamic_partitions.h
new file mode 100644
index 0000000..31cf859
--- /dev/null
+++ b/updater/include/updater/dynamic_partitions.h
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+void RegisterDynamicPartitionsFunctions();
diff --git a/updater/updater.cpp b/updater/updater.cpp
index e87c57a..7b5a3f9 100644
--- a/updater/updater.cpp
+++ b/updater/updater.cpp
@@ -35,6 +35,7 @@
 #include "otautil/error_code.h"
 #include "otautil/sysutil.h"
 #include "updater/blockimg.h"
+#include "updater/dynamic_partitions.h"
 #include "updater/install.h"
 
 // Generated by the makefile, this function defines the
@@ -125,6 +126,7 @@
   RegisterBuiltins();
   RegisterInstallFunctions();
   RegisterBlockImageFunctions();
+  RegisterDynamicPartitionsFunctions();
   RegisterDeviceExtensions();
 
   // Parse the script.