diff options
Diffstat (limited to 'bin/update')
-rwxr-xr-x | bin/update/create_build_config.py | 59 | ||||
-rwxr-xr-x | bin/update/create_full_mar.py | 80 | ||||
-rwxr-xr-x | bin/update/create_full_mar_for_languages.py | 71 | ||||
-rwxr-xr-x | bin/update/create_partial_update.py | 169 | ||||
-rw-r--r-- | bin/update/path.py | 68 | ||||
-rw-r--r-- | bin/update/signing.py | 15 | ||||
-rw-r--r-- | bin/update/tools.py | 61 | ||||
-rwxr-xr-x | bin/update/uncompress_mar.py | 58 | ||||
-rwxr-xr-x | bin/update/upload_build_config.py | 42 | ||||
-rwxr-xr-x | bin/update/upload_builds.py | 34 |
10 files changed, 657 insertions, 0 deletions
diff --git a/bin/update/create_build_config.py b/bin/update/create_build_config.py new file mode 100755 index 0000000000..de39b645ce --- /dev/null +++ b/bin/update/create_build_config.py @@ -0,0 +1,59 @@ +#! /usr/bin/env python3 + +import json +import sys +import os + +from tools import replace_variables_in_string + + +def update_all_url_entries(data, **kwargs): + data['complete']['url'] = replace_variables_in_string(data['complete']['url'], **kwargs) + + if sys.platform != "cygwin": + for language in data['languages']: + language['complete']['url'] = replace_variables_in_string(language['complete']['url'], **kwargs) + + if 'partials' in data: + for partial in data['partials']: + partial['file']['url'] = replace_variables_in_string(partial['file']['url'], **kwargs) + + if sys.platform == "cygwin": + continue + + for lang, lang_file in partial['languages'].items(): + lang_file['url'] = replace_variables_in_string(lang_file['url'], **kwargs) + + +def main(argv): + if len(argv) < 7: + print("Usage: create_build_config.py $PRODUCTNAME $VERSION $BUILDID $PLATFORM $TARGETDIR $CHANNEL") + sys.exit(1) + + data = {'productName': argv[1], + 'version': argv[2], + 'buildNumber': argv[3], + 'updateChannel': argv[6], + 'platform': argv[4] + } + + extra_data_files = ['complete_info.json', 'partial_update_info.json'] + if sys.platform != "cygwin": + extra_data_files.append('complete_lang_info.json') + + for extra_file in extra_data_files: + extra_file_path = os.path.join(argv[5], extra_file) + if not os.path.exists(extra_file_path): + continue + with open(extra_file_path, "r") as f: + extra_data = json.load(f) + data.update(extra_data) + + update_all_url_entries(data, channel=argv[6], platform=argv[4], buildid=argv[3], version=argv[2]) + + with open(os.path.join(argv[5], "build_config.json"), "w") as f: + json.dump(data, f, indent=4) + + +if __name__ == "__main__": + main(sys.argv) diff --git a/bin/update/create_full_mar.py b/bin/update/create_full_mar.py new file mode 100755 index 0000000000..b4f53c48f1 --- /dev/null +++ b/bin/update/create_full_mar.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 + +import sys +import glob +import os +import re +import subprocess +import json +import argparse + +from tools import uncompress_file_to_dir, get_file_info, make_complete_mar_name +from signing import sign_mar_file +from path import UpdaterPath, convert_to_unix, convert_to_native + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('product_name') + parser.add_argument('workdir') + parser.add_argument('filename_prefix') + parser.add_argument('certificate_path') + parser.add_argument('certificate_name') + parser.add_argument('base_url') + parser.add_argument('version') + args = parser.parse_args() + + certificate_path = args.certificate_path + certificate_name = args.certificate_name + base_url = args.base_url + filename_prefix = args.filename_prefix + workdir = args.workdir + product_name = args.product_name + version = args.version + + update_path = UpdaterPath(workdir) + update_path.ensure_dir_exist() + + target_dir = update_path.get_update_dir() + temp_dir = update_path.get_current_build_dir() + + tar_file_glob = os.path.join(update_path.get_workdir(), "installation", product_name, "archive", "install", "*", f'{product_name}_*_archive*') + tar_files = glob.glob(tar_file_glob) + if len(tar_files) != 1: + raise Exception(f'`{tar_file_glob}` does not match exactly one file') + tar_file = tar_files[0] + + uncompress_dir = uncompress_file_to_dir(tar_file, temp_dir) + + metadatafile = os.path.join( + update_path.get_workdir(), 'installation', product_name, 'archive', 'install', 'metadata') + ifsfile = os.path.join(update_path.get_mar_dir(), 'ifs') + with open(metadatafile) as meta, open(ifsfile, 'w') as ifs: + for l in meta: + m = re.fullmatch('(skip|cond) (.*)', l.rstrip()) + if m and m.group(2).startswith(f'{product_name}/'): + path = m.group(2)[len(f'{product_name}/'):] + if m.group(1) == 'skip': + os.remove(os.path.join(uncompress_dir, path)) + else: + ifs.write(f'"{path}" "{path}"\n') + + mar_file = make_complete_mar_name(target_dir, filename_prefix) + path = os.path.join( + workdir, 'UnpackedTarball/onlineupdate/tools/update-packaging/make_full_update.sh') + os.putenv('MOZ_PRODUCT_VERSION', version) + os.putenv('MAR_CHANNEL_ID', 'LOOnlineUpdater') + subprocess.call([ + path, convert_to_native(mar_file), convert_to_native(uncompress_dir), + convert_to_native(ifsfile)]) + + sign_mar_file(target_dir, certificate_path, certificate_name, mar_file, filename_prefix) + + file_info = {'complete': get_file_info(mar_file, base_url)} + + with open(os.path.join(target_dir, 'complete_info.json'), "w") as complete_info_file: + json.dump(file_info, complete_info_file, indent=4) + + +if __name__ == '__main__': + main() diff --git a/bin/update/create_full_mar_for_languages.py b/bin/update/create_full_mar_for_languages.py new file mode 100755 index 0000000000..d431ecaf6d --- /dev/null +++ b/bin/update/create_full_mar_for_languages.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 + +import sys +import os +import subprocess +import json + +from tools import uncompress_file_to_dir, get_file_info + +from path import UpdaterPath +from signing import sign_mar_file + + +def make_complete_mar_name(target_dir, filename_prefix, language): + filename = filename_prefix + "_" + language + "_complete_langpack.mar" + return os.path.join(target_dir, filename) + + +def create_lang_infos(mar_file_name, language, url): + data = {'lang': language, + 'complete': get_file_info(mar_file_name, url) + } + return data + + +def main(): + if len(sys.argv) < 8: + print( + "Usage: create_full_mar_for_languages.py $PRODUCTNAME $WORKDIR $TARGETDIR $TEMPDIR $FILENAMEPREFIX $CERTIFICATEPATH $CERTIFICATENAME $BASEURL $VERSION") + sys.exit(1) + + certificate_path = sys.argv[4] + certificate_name = sys.argv[5] + base_url = sys.argv[6] + filename_prefix = sys.argv[3] + workdir = sys.argv[2] + product_name = sys.argv[1] + version = sys.argv[7] + + updater_path = UpdaterPath(workdir) + target_dir = updater_path.get_update_dir() + temp_dir = updater_path.get_language_dir() + + language_pack_dir = os.path.join(workdir, "installation", product_name + "_languagepack", "archive", "install") + language_packs = os.listdir(language_pack_dir) + lang_infos = [] + for language in language_packs: + if language == 'log': + continue + + language_dir = os.path.join(language_pack_dir, language) + language_file = os.path.join(language_dir, os.listdir(language_dir)[0]) + + directory = uncompress_file_to_dir(language_file, os.path.join(temp_dir, language)) + + mar_file_name = make_complete_mar_name(target_dir, filename_prefix, language) + + os.putenv('MOZ_PRODUCT_VERSION', version) + os.putenv('MAR_CHANNEL_ID', 'LOOnlineUpdater') + subprocess.call([os.path.join(workdir, 'UnpackedTarball/onlineupdate/tools/update-packaging/make_full_update.sh'), mar_file_name, directory]) + + sign_mar_file(target_dir, certificate_path, certificate_name, mar_file_name, filename_prefix) + + lang_infos.append(create_lang_infos(mar_file_name, language, base_url)) + + with open(os.path.join(target_dir, "complete_lang_info.json"), "w") as language_info_file: + json.dump({'languages': lang_infos}, language_info_file, indent=4) + + +if __name__ == '__main__': + main() diff --git a/bin/update/create_partial_update.py b/bin/update/create_partial_update.py new file mode 100755 index 0000000000..2730c4765f --- /dev/null +++ b/bin/update/create_partial_update.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +import json +import os +import subprocess +import sys + +import requests + +from path import UpdaterPath, mkdir_p, convert_to_unix, convert_to_native +from signing import sign_mar_file +from tools import get_file_info, get_hash +from uncompress_mar import extract_mar + +BUF_SIZE = 1024 +current_dir_path = os.path.dirname(os.path.realpath(convert_to_unix(__file__))) + + +class InvalidFileException(Exception): + + def __init__(self, *args, **kwargs): + super().__init__(self, *args, **kwargs) + + +def download_file(filepath, url, hash_string): + with open(filepath, "wb") as f: + response = requests.get(url, stream=True) + + if not response.ok: + return + + for block in response.iter_content(1024): + f.write(block) + + file_hash = get_hash(filepath) + + if file_hash != hash_string: + raise InvalidFileException( + "file hash does not match for file %s: Expected %s, Got: %s" % (url, hash_string, file_hash)) + + +def handle_language(lang_entries, filedir): + langs = {} + for lang, data in lang_entries.items(): + lang_dir = os.path.join(filedir, lang) + lang_file = os.path.join(lang_dir, "lang.mar") + mkdir_p(lang_dir) + download_file(lang_file, data["url"], data["hash"]) + dir_path = os.path.join(lang_dir, "lang") + mkdir_p(dir_path) + extract_mar(lang_file, dir_path) + langs[lang] = dir_path + + return langs + + +def download_mar_for_update_channel_and_platform(server_url, channel, platform, temp_dir): + base_url = server_url + "update/partial-targets/1/" + url = base_url + platform + "/" + channel + r = requests.get(url) + if r.status_code != 200: + print(r.content) + raise Exception("download failed") + + update_info = json.loads(r.content.decode("utf-8")) + update_files = update_info['updates'] + downloaded_updates = {} + for update_file in update_files: + build = update_file["build"] + filedir = os.path.join(temp_dir, build) + + mkdir_p(filedir) + + filepath = filedir + "/complete.mar" + url = update_file["update"]["url"] + expected_hash = update_file["update"]["hash"] + download_file(filepath, url, expected_hash) + + dir_path = os.path.join(filedir, "complete") + mkdir_p(dir_path) + extract_mar(filepath, dir_path) + + downloaded_updates[build] = {"complete": dir_path} + + langs = handle_language(update_file["languages"], filedir) + downloaded_updates[build]["languages"] = langs + + return downloaded_updates + + +def generate_file_name(old_build_id, mar_name_prefix): + name = "%s_from_%s_partial.mar" % (mar_name_prefix, old_build_id) + return name + + +def generate_lang_file_name(old_build_id, mar_name_prefix, lang): + name = "%s_%s_from_%s_partial.mar" % (mar_name_prefix, lang, old_build_id) + return name + + +def add_single_dir(path): + dir_name = [os.path.join(path, name) for name in os.listdir(path) if os.path.isdir(os.path.join(path, name))] + return dir_name[0] + + +def main(): + workdir = sys.argv[1] + + updater_path = UpdaterPath(workdir) + updater_path.ensure_dir_exist() + + mar_name_prefix = sys.argv[2] + server_url = sys.argv[3] + channel = sys.argv[4] + certificate_path = sys.argv[5] + certificate_name = sys.argv[6] + base_url = sys.argv[7] + platform = sys.argv[8] + build_id = sys.argv[9] + + current_build_path = updater_path.get_current_build_dir() + mar_dir = updater_path.get_mar_dir() + temp_dir = updater_path.get_previous_build_dir() + update_dir = updater_path.get_update_dir() + + current_build_path = add_single_dir(current_build_path) + if sys.platform == "cygwin": + current_build_path = add_single_dir(current_build_path) + + updates = download_mar_for_update_channel_and_platform(server_url, channel, platform, temp_dir) + + data = {"partials": []} + + for build, update in updates.items(): + file_name = generate_file_name(build, mar_name_prefix) + mar_file = os.path.join(update_dir, file_name) + subprocess.call([os.path.join(current_dir_path, 'make_incremental_update.sh'), convert_to_native(mar_file), + convert_to_native(update["complete"]), convert_to_native(current_build_path)]) + sign_mar_file(update_dir, certificate_path, certificate_name, mar_file, mar_name_prefix) + + partial_info = {"file": get_file_info(mar_file, base_url), "from": build, "to": build_id, + "languages": {}} + + # on Windows we don't use language packs + if sys.platform != "cygwin": + for lang, lang_info in update["languages"].items(): + lang_name = generate_lang_file_name(build, mar_name_prefix, lang) + + # write the file into the final directory + lang_mar_file = os.path.join(update_dir, lang_name) + + # the directory of the old language file is of the form + # workdir/mar/language/en-US/LibreOffice_<version>_<os>_archive_langpack_<lang>/ + language_dir = add_single_dir(os.path.join(mar_dir, "language", lang)) + subprocess.call( + [os.path.join(current_dir_path, 'make_incremental_update.sh'), convert_to_native(lang_mar_file), + convert_to_native(lang_info), convert_to_native(language_dir)]) + sign_mar_file(update_dir, certificate_path, certificate_name, lang_mar_file, mar_name_prefix) + + # add the partial language info + partial_info["languages"][lang] = get_file_info(lang_mar_file, base_url) + + data["partials"].append(partial_info) + + with open(os.path.join(update_dir, "partial_update_info.json"), "w") as f: + json.dump(data, f) + + +if __name__ == '__main__': + main() diff --git a/bin/update/path.py b/bin/update/path.py new file mode 100644 index 0000000000..d91e9e7fba --- /dev/null +++ b/bin/update/path.py @@ -0,0 +1,68 @@ +# -*- tab-width: 4; indent-tabs-mode: nil; py-indent-offset: 4 -*- +# +# This file is part of the LibreOffice project. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# + +import os +import errno +import subprocess +from sys import platform + +def mkdir_p(path): + try: + os.makedirs(path) + except OSError as exc: # Python >2.5 + if exc.errno == errno.EEXIST and os.path.isdir(path): + pass + else: + raise + +def convert_to_unix(path): + if platform == "cygwin": + return subprocess.check_output(["cygpath", "-u", path]).decode("utf-8", "strict").rstrip() + else: + return path + + +def convert_to_native(path): + if platform == "cygwin": + return subprocess.check_output(["cygpath", "-m", path]).decode("utf-8", "strict").rstrip() + else: + return path + + +class UpdaterPath(object): + + def __init__(self, workdir): + self._workdir = convert_to_unix(workdir) + + def get_workdir(self): + return self._workdir + + def get_update_dir(self): + return os.path.join(self._workdir, "update-info") + + def get_current_build_dir(self): + return os.path.join(self._workdir, "mar", "current-build") + + def get_mar_dir(self): + return os.path.join(self._workdir, "mar") + + def get_previous_build_dir(self): + return os.path.join(self._workdir, "mar", "previous-build") + + def get_language_dir(self): + return os.path.join(self.get_mar_dir(), "language") + + def ensure_dir_exist(self): + os.makedirs(self.get_update_dir(), exist_ok=True) + os.makedirs(self.get_current_build_dir(), exist_ok=True) + os.makedirs(self.get_mar_dir(), exist_ok=True) + os.makedirs(self.get_previous_build_dir(), exist_ok=True) + os.makedirs(self.get_language_dir(), exist_ok=True) + +# vim: set shiftwidth=4 softtabstop=4 expandtab: diff --git a/bin/update/signing.py b/bin/update/signing.py new file mode 100644 index 0000000000..e8546dc83b --- /dev/null +++ b/bin/update/signing.py @@ -0,0 +1,15 @@ +from tools import make_complete_mar_name + +import os +import subprocess +import path + + +def sign_mar_file(target_dir, certificate_path, certificate_name, mar_file, filename_prefix): + signed_mar_file = make_complete_mar_name(target_dir, filename_prefix + '_signed') + mar_executable = os.environ.get('MAR', 'mar') + subprocess.check_call([mar_executable, '-C', path.convert_to_native(target_dir), '-d', + path.convert_to_native(certificate_path), '-n', certificate_name, '-s', + path.convert_to_native(mar_file), path.convert_to_native(signed_mar_file)]) + + os.rename(signed_mar_file, mar_file) diff --git a/bin/update/tools.py b/bin/update/tools.py new file mode 100644 index 0000000000..ab38d10f4b --- /dev/null +++ b/bin/update/tools.py @@ -0,0 +1,61 @@ +import os +import hashlib +import zipfile +import tarfile + + +def uncompress_file_to_dir(compressed_file, uncompress_dir): + extension = os.path.splitext(compressed_file)[1] + + os.makedirs(uncompress_dir, exist_ok=True) + + if extension == '.gz': + with tarfile.open(compressed_file) as tar: + tar.extractall(uncompress_dir) + elif extension == '.zip': + with zipfile.ZipFile(compressed_file) as zip_file: + zip_file.extractall(uncompress_dir) + + uncompress_dir = os.path.join(uncompress_dir, os.listdir(uncompress_dir)[0]) + if " " in os.listdir(uncompress_dir)[0]: + print("replacing whitespace in directory name") + os.rename(os.path.join(uncompress_dir, os.listdir(uncompress_dir)[0]), + os.path.join(uncompress_dir, os.listdir(uncompress_dir)[0].replace(" ", "_"))) + else: + print("Error: unknown extension " + extension) + + return os.path.join(uncompress_dir, os.listdir(uncompress_dir)[0]) + + +BUF_SIZE = 1048576 + + +def get_hash(file_path): + sha512 = hashlib.sha512() + with open(file_path, 'rb') as f: + while data := f.read(BUF_SIZE): + sha512.update(data) + return sha512.hexdigest() + + +def get_file_info(mar_file, url): + filesize = os.path.getsize(mar_file) + data = {'hash': get_hash(mar_file), + 'hash_function': 'sha512', + 'size': filesize, + 'url': url + os.path.basename(mar_file)} + + return data + + +def replace_variables_in_string(string, **kwargs): + new_string = string + for key, val in kwargs.items(): + new_string = new_string.replace('$(%s)' % key, val) + + return new_string + + +def make_complete_mar_name(target_dir, filename_prefix): + filename = filename_prefix + "_complete.mar" + return os.path.join(target_dir, filename) diff --git a/bin/update/uncompress_mar.py b/bin/update/uncompress_mar.py new file mode 100755 index 0000000000..14726dd961 --- /dev/null +++ b/bin/update/uncompress_mar.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# -*- tab-width: 4; indent-tabs-mode: nil; py-indent-offset: 4 -*- +# +# This file is part of the LibreOffice project. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# + +# Extract a mar file and uncompress the content + +import os +import re +import sys +import subprocess +from path import convert_to_native + + +def uncompress_content(file_path): + bzip2 = os.environ.get('BZIP2', 'bzip2') + file_path_compressed = file_path + ".bz2" + os.rename(file_path, file_path_compressed) + subprocess.check_call([bzip2, "-d", convert_to_native(file_path_compressed)]) + + +def extract_mar(mar_file, target_dir): + mar = os.environ.get('MAR', 'mar') + subprocess.check_call([mar, "-C", convert_to_native(target_dir), "-x", convert_to_native(mar_file)]) + file_info = subprocess.check_output([mar, "-t", convert_to_native(mar_file)]) + lines = file_info.splitlines() + prog = re.compile(r"\d+\s+\d+\s+(.+)") + for line in lines: + match = prog.match(line.decode("utf-8", "strict")) + if match is None: + continue + info = match.groups()[0] + # ignore header line + if info == b'NAME': + continue + + uncompress_content(os.path.join(target_dir, info)) + + +def main(): + if len(sys.argv) != 3: + print("Help: This program takes exactly two arguments pointing to a mar file and a target location") + sys.exit(1) + + mar_file = sys.argv[1] + target_dir = sys.argv[2] + extract_mar(mar_file, target_dir) + + +if __name__ == "__main__": + main() + +# vim: set shiftwidth=4 softtabstop=4 expandtab: diff --git a/bin/update/upload_build_config.py b/bin/update/upload_build_config.py new file mode 100755 index 0000000000..ec5a94bf3e --- /dev/null +++ b/bin/update/upload_build_config.py @@ -0,0 +1,42 @@ +#! /usr/bin/env python3 + +import sys +import os +import configparser +import requests + +dir_path = os.path.dirname(os.path.realpath(__file__)) + + +def main(argv): + updater_config = argv[2] + + config = configparser.ConfigParser() + config.read(os.path.expanduser(updater_config)) + + user = config["Updater"]["User"] + password = config["Updater"]["Password"] + base_address = config["Updater"]["ServerURL"] + + login_url = base_address + "accounts/login/" + + session = requests.session() + session.get(login_url) + csrftoken = session.cookies['csrftoken'] + + login_data = {'username': user, 'password': password, + 'csrfmiddlewaretoken': csrftoken} + session.post(login_url, data=login_data, headers={"Referer": login_url}) + + url = base_address + "update/upload/release" + data = {'csrfmiddlewaretoken': csrftoken} + + build_config = os.path.join(argv[1], "build_config.json") + r = session.post(url, files={'release_config': open(build_config, "r")}, data=data) + print(r.content) + if r.status_code != 200: + sys.exit(1) + + +if __name__ == "__main__": + main(sys.argv) diff --git a/bin/update/upload_builds.py b/bin/update/upload_builds.py new file mode 100755 index 0000000000..97a2f28484 --- /dev/null +++ b/bin/update/upload_builds.py @@ -0,0 +1,34 @@ +#! /usr/bin/env python3 + +import sys +import os +import subprocess + +from path import convert_to_unix + +from tools import replace_variables_in_string + + +def main(): + # product_name = sys.argv[1] + buildid = sys.argv[2] + platform = sys.argv[3] + update_dir = sys.argv[4] + upload_url_arg = sys.argv[5] + channel = sys.argv[6] + + upload_url = replace_variables_in_string(upload_url_arg, channel=channel, buildid=buildid, + platform=platform) + + target_url, target_dir = upload_url.split(':') + + command = "ssh %s 'mkdir -p %s'" % (target_url, target_dir) + print(command) + subprocess.call(command, shell=True) + for file in os.listdir(update_dir): + if file.endswith('.mar'): + subprocess.call(['scp', convert_to_unix(os.path.join(update_dir, file)), upload_url]) + + +if __name__ == '__main__': + main() |