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'"
- />
-  
+ />  
<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>
+  
+ <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)