Merge "[scripts/symbol] Switch to clang-r416183d"
diff --git a/scripts/update_crate_tests.py b/scripts/update_crate_tests.py
index 53bfd83..192f50e 100755
--- a/scripts/update_crate_tests.py
+++ b/scripts/update_crate_tests.py
@@ -13,170 +13,251 @@
 # 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.
-"""Add tests to TEST_MAPPING. Include tests for reverse dependencies."""
+"""Add or update tests to TEST_MAPPING.
+
+This script uses Bazel to find reverse dependencies on a crate and generates a
+TEST_MAPPING file. It accepts the absolute path to a crate as argument. If no
+argument is provided, it assumes the crate is the current directory.
+
+  Usage:
+  $ . build/envsetup.sh
+  $ lunch aosp_arm64-eng
+  $ update_crate_tests.py $ANDROID_BUILD_TOP/external/rust/crates/libc
+
+This script is automatically called by external_updater.
+"""
+
 import json
 import os
 import platform
 import subprocess
 import sys
 
-test_options = {
+# Some tests requires specific options. Consider fixing the upstream crate
+# before updating this dictionary.
+TEST_OPTIONS = {
     "ring_device_test_tests_digest_tests": [{"test-timeout": "600000"}],
     "ring_device_test_src_lib": [{"test-timeout": "100000"}],
 }
-test_exclude = [
+
+# Excluded tests. These tests will be ignored by this script.
+TEST_EXCLUDE = [
         "aidl_test_rust_client",
         "aidl_test_rust_service"
-    ]
-exclude_paths = [
+]
+
+# Excluded modules.
+EXCLUDE_PATHS = [
         "//external/adhd",
         "//external/crosvm",
         "//external/libchromeos-rs",
         "//external/vm_tools"
-    ]
+]
+
+
+class UpdaterException(Exception):
+    """Exception generated by this script."""
+
 
 class Env(object):
-    def __init__(self, path):
+    """Env captures the execution environment.
+
+    It ensures this script is executed within an AOSP repository.
+
+    Attributes:
+      ANDROID_BUILD_TOP: A string representing the absolute path to the top
+        of the repository.
+    """
+    def __init__(self):
         try:
             self.ANDROID_BUILD_TOP = os.environ['ANDROID_BUILD_TOP']
-        except:
-            sys.exit('ERROR: this script must be run from an Android tree.')
-        if path == None:
-            self.cwd = os.getcwd()
-        else:
-            self.cwd = path
-        try:
-            self.cwd_relative = self.cwd.split(self.ANDROID_BUILD_TOP)[1]
-            self.setup = True
-        except:
-            # Mark setup as failed if a path to a rust crate is not provided.
-            self.setup = False
+        except KeyError:
+            raise UpdaterException('$ANDROID_BUILD_TOP is not defined; you '
+                                   'must first source build/envsetup.sh and '
+                                   'select a target.')
+
 
 class Bazel(object):
-    # set up the Bazel queryview
+    """Bazel wrapper.
+
+    The wrapper is used to call bazel queryview and generate the list of
+    reverse dependencies.
+
+    Attributes:
+      path: The path to the bazel executable.
+    """
     def __init__(self, env):
-        os.chdir(env.ANDROID_BUILD_TOP)
-        print("Building Bazel Queryview. This can take a couple of minutes...")
-        cmd = "./build/soong/soong_ui.bash --build-mode --all-modules --dir=. queryview"
-        try:
-            out = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT)
-            self.setup = True
-        except subprocess.CalledProcessError as e:
-            print("Error: Unable to update TEST_MAPPING due to the following build error:")
-            print(e.output)
-            # Mark setup as failed if the Bazel queryview fails to build.
-            self.setup = False
-        os.chdir(env.cwd)
+        """Constructor.
 
-    def path(self):
-        # Only tested on Linux.
+        Note that the current directory is changed to ANDROID_BUILD_TOP.
+
+        Args:
+          env: An instance of Env.
+
+        Raises:
+          UpdaterException: an error occurred while calling soong_ui.
+        """
         if platform.system() != 'Linux':
-            sys.exit('ERROR: this script has only been tested on Linux.')
-        return "/usr/bin/bazel"
+            raise UpdaterException('This script has only been tested on Linux.')
+        self.path = os.path.join(env.ANDROID_BUILD_TOP, "tools", "bazel")
+        soong_ui = os.path.join(env.ANDROID_BUILD_TOP, "build", "soong", "soong_ui.bash")
 
-    # Return all modules for a given path.
+        # soong_ui requires to be at the root of the repository.
+        os.chdir(env.ANDROID_BUILD_TOP)
+        print("Generating Bazel files...")
+        cmd = [soong_ui, "--make-mode", "GENERATE_BAZEL_FILES=1", "nothing"]
+        try:
+            subprocess.check_output(cmd, stderr=subprocess.STDOUT, text=True)
+        except subprocess.CalledProcessError as e:
+            raise UpdaterException('Unable to generate bazel workspace: ' + e.output)
+
+        print("Building Bazel Queryview. This can take a couple of minutes...")
+        cmd = [soong_ui, "--build-mode", "--all-modules", "--dir=.", "queryview"]
+        try:
+            subprocess.check_output(cmd, stderr=subprocess.STDOUT, text=True)
+        except subprocess.CalledProcessError as e:
+            raise UpdaterException('Unable to update TEST_MAPPING: ' + e.output)
+
     def query_modules(self, path):
-        with open(os.devnull, 'wb') as DEVNULL:
-            cmd = self.path() + " query --config=queryview /" + path + ":all"
-            out = subprocess.check_output(cmd, shell=True, stderr=DEVNULL, text=True).strip().split("\n")
-            modules = set()
-            for line in out:
-                # speed up by excluding unused modules.
-                if "windows_x86" in line:
-                    continue
-                modules.add(line)
-            return modules
+        """Returns all modules for a given path."""
+        cmd = self.path + " query --config=queryview /" + path + ":all"
+        out = subprocess.check_output(cmd, shell=True, stderr=subprocess.DEVNULL, text=True).strip().split("\n")
+        modules = set()
+        for line in out:
+            # speed up by excluding unused modules.
+            if "windows_x86" in line:
+                continue
+            modules.add(line)
+        return modules
 
-    # Return all reverse dependencies for a single module.
     def query_rdeps(self, module):
-        with open(os.devnull, 'wb') as DEVNULL:
-            cmd = (self.path() + " query --config=queryview \'rdeps(//..., " +
-                    module + ")\' --output=label_kind")
-            out = (subprocess.check_output(cmd, shell=True, stderr=DEVNULL, text=True)
-                    .strip().split("\n"))
-            if '' in out:
-                out.remove('')
-            return out
+        """Returns all reverse dependencies for a single module."""
+        cmd = (self.path + " query --config=queryview \'rdeps(//..., " +
+                module + ")\' --output=label_kind")
+        out = (subprocess.check_output(cmd, shell=True, stderr=subprocess.DEVNULL, text=True)
+                .strip().split("\n"))
+        if '' in out:
+            out.remove('')
+        return out
 
     def exclude_module(self, module):
-        for path in exclude_paths:
+        for path in EXCLUDE_PATHS:
             if module.startswith(path):
                 return True
         return False
 
-    # Return all reverse dependency tests for modules in this package.
     def query_rdep_tests(self, modules):
+        """Returns all reverse dependency tests for modules in this package."""
         rdep_tests = set()
         for module in modules:
             for rdep in self.query_rdeps(module):
-                rule_type, tmp, mod = rdep.split(" ")
+                rule_type, _, mod = rdep.split(" ")
                 if rule_type == "rust_test_" or rule_type == "rust_test":
                     if self.exclude_module(mod) == False:
                         rdep_tests.add(mod.split(":")[1].split("--")[0])
         return rdep_tests
 
 
-class Crate(object):
-    def __init__(self, path, bazel):
-        self.modules = bazel.query_modules(path)
-        self.rdep_tests = bazel.query_rdep_tests(self.modules)
+class Package(object):
+    """A Bazel package.
+
+    Attributes:
+      dir: The absolute path to this package.
+      dir_rel: The relative path to this package.
+      rdep_tests: The list of computed reverse dependencies.
+    """
+    def __init__(self, path, env, bazel):
+        """Constructor.
+
+        Note that the current directory is changed to the package location when
+        called.
+
+        Args:
+          path: Path to the package.
+          env: An instance of Env.
+          bazel: An instance of Bazel.
+
+        Raises:
+          UpdaterException: the package does not appear to belong to the
+            current repository.
+        """
+        if path == None:
+            self.dir = os.getcwd()
+        else:
+            self.dir = path
+        try:
+            self.dir_rel = self.dir.split(env.ANDROID_BUILD_TOP)[1]
+        except IndexError:
+            raise UpdaterException('The path ' + self.dir + ' is not under ' +
+                            env.ANDROID_BUILD_TOP + '; You must be in the '
+                            'directory of a crate or pass its absolute path '
+                            'as first argument.')
+
+        # Move to the package_directory.
+        os.chdir(self.dir)
+        modules = bazel.query_modules(self.dir_rel)
+        self.rdep_tests = bazel.query_rdep_tests(modules)
 
     def get_rdep_tests(self):
         return self.rdep_tests
 
 
 class TestMapping(object):
+    """A TEST_MAPPING file.
+
+    Attributes:
+      package: The package associated with this TEST_MAPPING file.
+    """
     def __init__(self, path):
-        self.env = Env(path)
-        self.bazel = Bazel(self.env)
+        """Constructor.
 
-    def is_setup(self):
-        return self.env.setup and self.bazel.setup
+        Args:
+          path: The absolute path to the package.
+        """
+        env = Env()
+        bazel = Bazel(env)
+        self.package = Package(path, env, bazel)
 
-    def create_test_mapping(self, path):
-        if not self.is_setup():
-            return
-        tests = self.get_tests(path)
+    def create(self):
+        """Generates the TEST_MAPPING file."""
+        tests = self.package.get_rdep_tests()
         if not bool(tests):
             return
         test_mapping = self.tests_to_mapping(tests)
         self.write_test_mapping(test_mapping)
 
-    def get_tests(self, path):
-        # for each path collect local Rust modules.
-        if path is not None and path != "":
-            return Crate(self.env.cwd_relative + "/" + path, self.bazel).get_rdep_tests()
-        else:
-            return Crate(self.env.cwd_relative, self.bazel).get_rdep_tests()
-
     def tests_to_mapping(self, tests):
+        """Translate the test list into a dictionary."""
         test_mapping = {"presubmit": []}
         for test in tests:
-            if test in test_exclude:
+            if test in TEST_EXCLUDE:
                 continue
-            if test in test_options:
-                test_mapping["presubmit"].append({"name": test, "options": test_options[test]})
+            if test in TEST_OPTIONS:
+                test_mapping["presubmit"].append({"name": test, "options": TEST_OPTIONS[test]})
             else:
                 test_mapping["presubmit"].append({"name": test})
         test_mapping["presubmit"] = sorted(test_mapping["presubmit"], key=lambda t: t["name"])
         return test_mapping
 
     def write_test_mapping(self, test_mapping):
+        """Writes the TEST_MAPPING file."""
         with open("TEST_MAPPING", "w") as json_file:
             json_file.write("// Generated by update_crate_tests.py for tests that depend on this crate.\n")
             json.dump(test_mapping, json_file, indent=2, separators=(',', ': '), sort_keys=True)
             json_file.write("\n")
         print("TEST_MAPPING successfully updated!")
 
+
 def main():
     if len(sys.argv) == 2:
         path = sys.argv[1]
     else:
         path = None
-    test_mapping = TestMapping(path)
-    test_mapping.create_test_mapping(None)
-    if not test_mapping.is_setup():
-        raise ValueError('Error getting crate tests.')
+    try:
+        test_mapping = TestMapping(path)
+    except UpdaterException as err:
+        sys.exit("Error: " + str(err))
+    test_mapping.create()
 
 if __name__ == '__main__':
   main()
diff --git a/tools/otagui/.gitignore b/tools/otagui/.gitignore
index 21f295a..b81e39b 100644
--- a/tools/otagui/.gitignore
+++ b/tools/otagui/.gitignore
@@ -25,3 +25,4 @@
 *.njsproj
 *.sln
 *.sw?
+*.db
diff --git a/tools/otagui/.prettierrc.js b/tools/otagui/.prettierrc.js
index a6b80c2..0916798 100644
--- a/tools/otagui/.prettierrc.js
+++ b/tools/otagui/.prettierrc.js
@@ -1,4 +1,5 @@
 module.exports = {
   singleQuote: true,
-  semi: false
+  semi: false,
+  useTabs: false
 }
diff --git a/tools/otagui/ota_interface.py b/tools/otagui/ota_interface.py
index 387dddd..f5b5fb0 100644
--- a/tools/otagui/ota_interface.py
+++ b/tools/otagui/ota_interface.py
@@ -49,14 +49,10 @@
     def get_status(self):
         return [self.get_status_by_ID(id=id) for id in self.get_keys()]
 
-    def get_list(self, dir):
-        files = os.listdir(dir)
-        return files
-
     def ota_generate(self, args, id=0):
         command = ['ota_from_target_files']
         # Check essential configuration is properly set
-        if not os.path.isfile('target/' + args['target']):
+        if not os.path.isfile(args['target']):
             raise FileNotFoundError
         if not args['output']:
             raise SyntaxError
@@ -65,14 +61,17 @@
         command.append('-k')
         command.append(
             '../../../build/make/target/product/security/testkey')
-        if args['incremental']:
-            if not os.path.isfile('target/' + args['incremental']):
+        if args['isIncremental']:
+            if not os.path.isfile(args['incremental']):
                 raise FileNotFoundError
             command.append('-i')
-            command.append('target/' + args['incremental'])
-        command.append('target/' + args['target'])
+            command.append(args['incremental'])
+        if args['isPartial']:
+            command.append('--partial')
+            command.append(args['partial'])
+        command.append(args['target'])
         command.append(args['output'])
-
+        # Start a subprocess and collect the output
         stderr_pipes = pipes.Template()
         stdout_pipes = pipes.Template()
         ferr = stderr_pipes.open(os.path.join(
diff --git a/tools/otagui/src/components/BaseCheckbox.vue b/tools/otagui/src/components/BaseCheckbox.vue
index e9e0d24..21a6977 100644
--- a/tools/otagui/src/components/BaseCheckbox.vue
+++ b/tools/otagui/src/components/BaseCheckbox.vue
@@ -3,6 +3,7 @@
     type="checkbox"
     :checked="modelValue"
     class="field"
+    v-bind="$attrs"
     @change="$emit('update:modelValue', $event.target.checked)"
   >
   <label v-if="label"> {{ label }} </label>
diff --git a/tools/otagui/src/components/FileSelect.vue b/tools/otagui/src/components/FileSelect.vue
new file mode 100644
index 0000000..42e39a9
--- /dev/null
+++ b/tools/otagui/src/components/FileSelect.vue
@@ -0,0 +1,39 @@
+<template>
+  <label v-if="label"> {{ label }} </label>
+  <select
+    :value="modelValue"
+    class="field"
+    v-bind="$attrs"
+    @change="$emit('update:modelValue', $event.target.value)"
+  >
+    <option
+      v-for="option in options"
+      :key="option.file_name"
+      :value="option.path"
+      :selected="option.path === modelValue"
+    >
+      {{ option.file_name }}
+    </option>
+  </select>
+</template>
+
+
+
+<script>
+export default {
+  props: {
+    label: {
+      type: String,
+      default: '',
+    },
+    modelValue: {
+      type: [String, Number],
+      default: ''
+    },
+    options: {
+      type: Array,
+      required: true
+    }
+  }
+}
+</script>
\ No newline at end of file
diff --git a/tools/otagui/src/components/PartialCheckbox.vue b/tools/otagui/src/components/PartialCheckbox.vue
new file mode 100644
index 0000000..426b261
--- /dev/null
+++ b/tools/otagui/src/components/PartialCheckbox.vue
@@ -0,0 +1,37 @@
+<template>
+  <ul v-bind="$attrs">
+    <li
+      v-for="label in labels"
+      :key="label"
+    >
+      <input
+        type="checkbox"
+        :value="label"
+        :checked="modelValue.get(label)"
+        @change="updateSelected($event.target.value)"
+      >
+      <label v-if="label"> {{ label }} </label>
+    </li>
+  </ul>
+</template>
+
+<script>
+export default {
+  props: {
+    labels: {
+      type: Array,
+      default: new Array(),
+    },
+    modelValue: {
+      type: Map,
+      default: new Map(),
+    },
+  },
+  methods: {
+    updateSelected(newSelect) {
+      this.modelValue.set(newSelect, !this.modelValue.get(newSelect))
+      this.$emit('update:modelValue', this.modelValue)
+    },
+  },
+}
+</script>
\ No newline at end of file
diff --git a/tools/otagui/src/views/SimpleForm.vue b/tools/otagui/src/views/SimpleForm.vue
index 6357a35..6ec1e74 100644
--- a/tools/otagui/src/views/SimpleForm.vue
+++ b/tools/otagui/src/views/SimpleForm.vue
@@ -3,16 +3,16 @@
     <form @submit.prevent="sendForm">
       <UploadFile @file-uploaded="fetchTargetList" />
       <br>
-      <BaseSelect
-        v-if="input.incrementalStatus"
+      <FileSelect
+        v-if="input.isIncremental"
         v-model="input.incremental"
         label="Select the source file"
-        :options="targetList"
+        :options="targetDetails"
       />
-      <BaseSelect
+      <FileSelect
         v-model="input.target"
         label="Select the target file"
-        :options="targetList"
+        :options="targetDetails"
       />
       <button
         type="button"
@@ -24,13 +24,26 @@
         <BaseCheckbox
           v-model="input.verbose"
           :label="'Verbose'"
-        />
-        &emsp;
+        /> &emsp;
         <BaseCheckbox
-          v-model="input.incrementalStatus"
+          v-model="input.isIncremental"
           :label="'Incremental'"
         />
       </div>
+      <div>
+        <BaseCheckbox
+          v-model="input.isPartial"
+          :label="'Partial'"
+        />
+        <PartialCheckbox
+          v-if="input.isPartial"
+          v-model="partitionInclude"
+          :labels="updatePartitions"
+        />
+        <div v-if="input.isPartial">
+          Partial list: {{ partitionList }}
+        </div>
+      </div>
       <br>
       <BaseInput
         v-model="input.extra"
@@ -42,14 +55,53 @@
       </button>
     </form>
   </div>
+  <div>
+    <ul>
+      <h4>Build Library</h4>
+      <strong>
+        Careful: Use a same filename will overwrite the original build.
+      </strong>
+      <br>
+      <button @click="updateBuildLib">
+        Refresh the build Library (use with cautions)
+      </button>
+      <li
+        v-for="targetDetail in targetDetails"
+        :key="targetDetail.file_name"
+      >
+        <div>
+          <h5>Build File Name: {{ targetDetail.file_name }}</h5>
+          Uploaded time: {{ formDate(targetDetail.time) }}
+          <br>
+          Build ID: {{ targetDetail.build_id }}
+          <br>
+          Build Version: {{ targetDetail.build_version }}
+          <br>
+          Build Flavor: {{ targetDetail.build_flavor }}
+          <br>
+          <button
+            :disabled="!input.isIncremental"
+            @click="selectIncremental(targetDetail.path)"
+          >
+            Select as Incremental File
+          </button>
+          &emsp;
+          <button @click="selectTarget(targetDetail.path)">
+            Select as Target File
+          </button>
+        </div>
+      </li>
+    </ul>
+  </div>
 </template>
 
 <script>
 import BaseInput from '@/components/BaseInput.vue'
 import BaseCheckbox from '@/components/BaseCheckbox.vue'
-import BaseSelect from '@/components/BaseSelect.vue'
+import FileSelect from '@/components/FileSelect.vue'
 import ApiService from '../services/ApiService.js'
 import UploadFile from '@/components/UploadFile.vue'
+import PartialCheckbox from '@/components/PartialCheckbox.vue'
 import { uuid } from 'vue-uuid'
 
 export default {
@@ -57,58 +109,77 @@
     BaseInput,
     BaseCheckbox,
     UploadFile,
-    BaseSelect,
+    FileSelect,
+    PartialCheckbox,
   },
   data() {
     return {
       id: 0,
-      input: {
-        verbose: false,
-        target: '',
-        output: 'output/',
-        incremental: '',
-        incrementalStatus: false,
-        extra: '',
-      },
+      input: {},
       inputs: [],
       response_message: '',
-      targetList: [],
+      targetDetails: [],
+      partitionInclude: new Map(),
     }
   },
   computed: {
-    updateOutput() {
-      return 'output/' + String(this.id) + '.zip'
+    updatePartitions() {
+      let target = this.targetDetails.filter(
+        (d) => d.path === this.input.target
+      )
+      return target[0].partitions
+    },
+    partitionList() {
+      let list = ''
+      for (let [key, value] of this.partitionInclude) {
+        if (value) {
+          list += key + ' '
+        }
+      }
+      return list
+    },
+  },
+  watch: {
+    partitionList: {
+      handler: function () {
+        this.input.partial = this.partitionList
+      },
     },
   },
   created() {
+    this.resetInput()
     this.fetchTargetList()
     this.updateUUID()
   },
   methods: {
-    sendForm(e) {
-      // console.log(this.input)
-      ApiService.postInput(this.input, this.id)
-        .then((Response) => {
-          this.response_message = Response.data
-          alert(this.response_message)
-        })
-        .catch((err) => {
-          this.response_message = 'Error! ' + err
-        })
+    resetInput() {
       this.input = {
         verbose: false,
         target: '',
         output: 'output/',
         incremental: '',
-        incrementalStatus: false,
+        isIncremental: false,
+        partial: '',
+        isPartial: false,
         extra: '',
       }
+    },
+    async sendForm(e) {
+      try {
+        let response = await ApiService.postInput(this.input, this.id)
+        this.response_message = response.data
+        alert(this.response_message)
+      } catch (err) {
+        alert('Job cannot be started properly, please check.')
+        console.log(err)
+      }
+      this.resetInput()
       this.updateUUID()
     },
     async fetchTargetList() {
       try {
-        let response = await ApiService.getFileList('/target')
-        this.targetList = response.data
+        let response = await ApiService.getFileList('')
+        this.targetDetails = response.data
       } catch (err) {
         console.log('Fetch Error', err)
       }
@@ -117,6 +188,36 @@
       this.id = uuid.v1()
       this.input.output += String(this.id) + '.zip'
     },
+    formDate(unixTime) {
+      let formTime = new Date(unixTime * 1000)
+      let date =
+        formTime.getFullYear() +
+        '-' +
+        (formTime.getMonth() + 1) +
+        '-' +
+        formTime.getDate()
+      let time =
+        formTime.getHours() +
+        ':' +
+        formTime.getMinutes() +
+        ':' +
+        formTime.getSeconds()
+      return date + ' ' + time
+    },
+    selectTarget(path) {
+      this.input.target = path
+    },
+    selectIncremental(path) {
+      this.input.incremental = path
+    },
+    async updateBuildLib() {
+      try {
+        let response = await ApiService.getFileList('/target')
+        this.targetDetails = response.data
+      } catch (err) {
+        console.log('Fetch Error', err)
+      }
+    },
   },
 }
 </script>
diff --git a/tools/otagui/target_lib.py b/tools/otagui/target_lib.py
new file mode 100644
index 0000000..76d7a82
--- /dev/null
+++ b/tools/otagui/target_lib.py
@@ -0,0 +1,158 @@
+from dataclasses import dataclass, asdict, field
+import sqlite3
+import time
+import logging
+import os
+import zipfile
+import re
+import json
+
+
+@dataclass
+class BuildInfo:
+    """
+    A class for Android build information.
+    """
+    file_name: str
+    path: str
+    time: int
+    build_id: str = ''
+    build_version: str = ''
+    build_flavor: str = ''
+    partitions: list[str] = field(default_factory=list)
+
+    def analyse_buildprop(self):
+        """
+        Analyse the build's version info and partitions included
+        Then write them into the build_info
+        """
+        def extract_info(pattern, lines):
+            # Try to match a regex in a list of string
+            line = list(filter(pattern.search, lines))[0]
+            if line:
+                return pattern.search(line).group(0)
+            else:
+                return ''
+
+        build = zipfile.ZipFile(self.path)
+        try:
+            with build.open('SYSTEM/build.prop', 'r') as build_prop:
+                raw_info = build_prop.readlines()
+                pattern_id = re.compile(b'(?<=ro\.build\.id\=).+')
+                pattern_version = re.compile(
+                    b'(?<=ro\.build\.version\.incremental\=).+')
+                pattern_flavor = re.compile(b'(?<=ro\.build\.flavor\=).+')
+                self.build_id = extract_info(
+                    pattern_id, raw_info).decode('utf-8')
+                self.build_version = extract_info(
+                    pattern_version, raw_info).decode('utf-8')
+                self.build_flavor = extract_info(
+                    pattern_flavor, raw_info).decode('utf-8')
+        except KeyError:
+            pass
+        try:
+            with build.open('META/ab_partitions.txt', 'r') as partition_info:
+                raw_info = partition_info.readlines()
+                for line in raw_info:
+                    self.partitions.append(line.decode('utf-8').rstrip())
+        except KeyError:
+            pass
+
+    def to_sql_form_dict(self):
+        sql_form_dict = asdict(self)
+        sql_form_dict['partitions'] = ','.join(sql_form_dict['partitions'])
+        return sql_form_dict
+
+    def to_dict(self):
+        return asdict(self)
+
+
+class TargetLib:
+    def __init__(self, path='ota_database.db'):
+        """
+        Create a build table if not existing
+        """
+        self.path = path
+        with sqlite3.connect(self.path) as connect:
+            cursor = connect.cursor()
+            cursor.execute("""
+                CREATE TABLE if not exists Builds (
+                FileName TEXT,
+                UploadTime INTEGER,
+                Path TEXT,
+                BuildID TEXT,
+                BuildVersion TEXT,
+                BuildFlavor TEXT,
+                Partitions TEXT
+            )
+            """)
+
+    def new_build(self, filename, path):
+        """
+        Insert a new build into the database
+        Args:
+            filename: the name of the file
+            path: the relative path of the file
+        """
+        build_info = BuildInfo(filename, path, int(time.time()))
+        build_info.analyse_buildprop()
+        with sqlite3.connect(self.path) as connect:
+            cursor = connect.cursor()
+            cursor.execute("""
+            SELECT * FROM Builds WHERE FileName=:file_name and Path=:path
+            """, build_info.to_sql_form_dict())
+            if cursor.fetchall():
+                cursor.execute("""
+                DELETE FROM Builds WHERE FileName=:file_name and Path=:path
+                """, build_info.to_sql_form_dict())
+            cursor.execute("""
+            INSERT INTO Builds (FileName, UploadTime, Path, BuildID, BuildVersion, BuildFlavor, Partitions)
+            VALUES (:file_name, :time, :path, :build_id, :build_version, :build_flavor, :partitions)
+            """, build_info.to_sql_form_dict())
+
+    def new_build_from_dir(self, path):
+        """
+        Update the database using files under a directory
+        Args:
+            path: a directory
+        """
+        if os.path.isdir(path):
+            builds_name = os.listdir(path)
+            for build_name in builds_name:
+                self.new_build(build_name, os.path.join(path, build_name))
+        elif os.path.isfile(path):
+            self.new_build(os.path.split(path)[-1], path)
+        return self.get_builds()
+
+    def sql_to_buildinfo(self, row):
+        build_info = BuildInfo(*row[:6], row[6].split(','))
+        return build_info
+
+    def get_builds(self):
+        """
+        Get a list of builds in the database
+        Return:
+            A list of build_info, each of which is an object:
+            (FileName, UploadTime, Path, Build ID, Build Version, Build Flavor, Partitions)
+        """
+        with sqlite3.connect(self.path) as connect:
+            cursor = connect.cursor()
+            cursor.execute("""
+            SELECT FileName, Path, UploadTime, BuildID, BuildVersion, BuildFlavor, Partitions
+            FROM Builds""")
+            return list(map(self.sql_to_buildinfo, cursor.fetchall()))
+
+    def get_builds_by_path(self, path):
+        """
+        Get a build in the database by its path
+        Return:
+            A build_info, which is an object:
+            (FileName, UploadTime, Path, Build ID, Build Version, Build Flavor, Partitions)
+        """
+        with sqlite3.connect(self.path) as connect:
+            cursor = connect.cursor()
+            cursor.execute("""
+            SELECT FileName, Path, UploadTime, BuildID, BuildVersion, BuildFlavor, Partitions
+            WHERE Path==(?)
+            """, (path, ))
+        return self.sql_to_buildinfo(cursor.fetchone())
diff --git a/tools/otagui/web_server.py b/tools/otagui/web_server.py
index 3ecf18f..79c3599 100644
--- a/tools/otagui/web_server.py
+++ b/tools/otagui/web_server.py
@@ -9,12 +9,17 @@
   GET /check : check the status of all jobs
   GET /check/<id> : check the status of the job with <id>
   GET /file : fetch the target file list
+  GET /file/<path> : Add build file(s) in <path>, and return the target file list
   GET /download/<id> : download the ota package with <id>
   POST /run/<id> : submit a job with <id>,
                  arguments set in a json uploaded together
   POST /file/<filename> : upload a target file
   [TODO] POST /cancel/<id> : cancel a job with <id>
 
+TODO:
+  - Avoid unintentionally path leakage
+  - Avoid overwriting build when uploading build with same file name
+
 Other GET request will be redirected to the static request under 'dist' directory
 """
 
@@ -22,12 +27,14 @@
 from socketserver import ThreadingMixIn
 from threading import Lock
 from ota_interface import ProcessesManagement
+from target_lib import TargetLib
 import logging
 import json
 import pipes
 import cgi
 import subprocess
 import os
+import sys
 
 LOCAL_ADDRESS = '0.0.0.0'
 
@@ -77,10 +84,14 @@
             )
             return
         elif self.path.startswith('/file'):
-            file_list = jobs.get_list(self.path[6:])
+            if self.path == '/file' or self.path == '/file/':
+                file_list = target_lib.get_builds()
+            else:
+                file_list = target_lib.new_build_from_dir(self.path[6:])
+            builds_info = [build.to_dict() for build in file_list]
             self._set_response(type='application/json')
             self.wfile.write(
-                json.dumps(file_list).encode()
+                json.dumps(builds_info).encode()
             )
             logging.info(
                 "GET request:\nPath:%s\nHeaders:\n%s\nBody:\n%s\n",
@@ -133,6 +144,7 @@
                 file_length -= len(self.rfile.readline())
                 file_length -= len(self.rfile.readline())
                 output_file.write(self.rfile.read(file_length))
+                target_lib.new_build(self.path[6:], file_name)
             self._set_response(code=201)
             self.wfile.write(
                 "File received, saved into {}".format(
@@ -164,8 +176,16 @@
 if __name__ == '__main__':
     from sys import argv
     print(argv)
+    if not os.path.isdir('target'):
+        os.mkdir('target', 755)
+    if not os.path.isdir('output'):
+        os.mkdir('output', 755)
+    target_lib = TargetLib()
     jobs = ProcessesManagement()
-    if len(argv) == 2:
-        run_server(port=int(argv[1]))
-    else:
-        run_server()
+    try:
+        if len(argv) == 2:
+            run_server(port=int(argv[1]))
+        else:
+            run_server()
+    except KeyboardInterrupt:
+        sys.exit(0)