diff --git a/Makefile b/Makefile index b83b7b4e5..3989fec52 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -PACKAGER_PATH = src//tools/Package-All.py +PACKAGER_PATH = src//tools//packager//Package-All.py BUILD_PATH = build ZIP_SRC_PATH = src//out//LinuxPatchExtension.zip MANIFEST_PATH = src//extension//src//manifest.xml diff --git a/README.md b/README.md index 7a374a836..a4504ddc6 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Of these, only `operation`, `activityId`, `startTime` are required for Assessmen ## 2. Build and Test locally -* Run `python \src\tools\Package-All.py`. This will generate LinuxPatchExtension.zip under `\out\` +* Run `python \src\tools\packager\Package-All.py`. This will generate LinuxPatchExtension.zip under `\out\` * Extract files from the zip to any location on your Linux machine. Note down this path. * Add `HandlerEnvironment.json` following the reference `\src\tools\references\HandlerEnvironment.json` within the folder containing extracted files. `HandlerEnvironment.json` defines the location where log, config and status files will be saved. Make sure to specify a directory/folder path for all 3 (can be any location within the machine) diff --git a/src/build.bat b/src/build.bat new file mode 100644 index 000000000..3f2d661a5 --- /dev/null +++ b/src/build.bat @@ -0,0 +1,2 @@ +@echo off +python tools\packager\Package-All.py diff --git a/src/build.sh b/src/build.sh new file mode 100644 index 000000000..74eaad6bf --- /dev/null +++ b/src/build.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +# Copyright 2025 Microsoft Corporation +# +# 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. +# +# Requires Python 2.7+ + +COMMAND="tools/packager/Package-All.py" + +function find_python(){ + local python_exec_command=$1 + + # Check if there is python defined. + for p in python3 /usr/share/oem/python/bin/python3 python python2 /usr/libexec/platform-python /usr/share/oem/python/bin/python; do + if command -v "${p}" ; then + eval ${python_exec_command}=${p} + return + fi + done +} + +find_python PYTHON + +${PYTHON} "${COMMAND}" diff --git a/src/core/src/bootstrap/Bootstrapper.py b/src/core/src/bootstrap/Bootstrapper.py index 080b94393..9b7a6f840 100644 --- a/src/core/src/bootstrap/Bootstrapper.py +++ b/src/core/src/bootstrap/Bootstrapper.py @@ -122,7 +122,7 @@ def build_core_components(self, container): return lifecycle_manager, status_handler def bootstrap_splash_text(self): - self.composite_logger.log("\n\n[%exec_name%] \t -- \t Copyright (c) Microsoft Corporation. All rights reserved. \nApplication version: 3.0.[%exec_sub_ver%]\n\n") + self.composite_logger.log("\n\n[%exec_name%] \t -- \t Copyright (c) Microsoft Corporation. All rights reserved. \nVersion: [%exec_ver%], Build Date: [%exec_build_date%].\n\n") def basic_environment_health_check(self): self.composite_logger.log("Python version: " + " ".join(sys.version.splitlines())) diff --git a/src/core/src/bootstrap/Constants.py b/src/core/src/bootstrap/Constants.py index b4b7e6633..0946b2b5d 100644 --- a/src/core/src/bootstrap/Constants.py +++ b/src/core/src/bootstrap/Constants.py @@ -30,8 +30,8 @@ def __iter__(self): GLOBAL_EXCLUSION_LIST = "" # if a package needs to be blocked across all of Azure UNKNOWN = "Unknown" - # Extension version (todo: move to a different file) - EXT_VERSION = "1.6.66" + # Extension version (will be updated from manifest.xml at compile time) + EXT_VERSION = "[%exec_ver%]" # Runtime environments TEST = 'Test' diff --git a/src/extension/src/Constants.py b/src/extension/src/Constants.py index 0a98a02a2..de5bda732 100644 --- a/src/extension/src/Constants.py +++ b/src/extension/src/Constants.py @@ -27,8 +27,8 @@ def __iter__(self): if item == self.__dict__[item]: yield item - # Extension version (todo: move to a different file) - EXT_VERSION = "1.6.66" + # Extension version (will be updated from manifest.xml at compile time) + EXT_VERSION = "[%exec_ver%]" # Runtime environments TEST = 'Test' diff --git a/src/publish.bat b/src/publish.bat new file mode 100644 index 000000000..cb0469ecc --- /dev/null +++ b/src/publish.bat @@ -0,0 +1,3 @@ +@echo off +python tools\packager\Publish.py +git status diff --git a/src/publish.sh b/src/publish.sh new file mode 100644 index 000000000..95a815a14 --- /dev/null +++ b/src/publish.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +# Copyright 2025 Microsoft Corporation +# +# 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. +# +# Requires Python 2.7+ + +COMMAND="tools/packager/Publish.py" + +function find_python(){ + local python_exec_command=$1 + + # Check if there is python defined. + for p in python3 /usr/share/oem/python/bin/python3 python python2 /usr/libexec/platform-python /usr/share/oem/python/bin/python; do + if command -v "${p}" ; then + eval ${python_exec_command}=${p} + return + fi + done +} + +find_python PYTHON + +${PYTHON} "${COMMAND}" diff --git a/src/tools/misc/EnableVirtualTerminal.reg b/src/tools/misc/EnableVirtualTerminal.reg deleted file mode 100644 index 21291ee1b..000000000 Binary files a/src/tools/misc/EnableVirtualTerminal.reg and /dev/null differ diff --git a/src/tools/Package-All.py b/src/tools/packager/Package-All.py similarity index 75% rename from src/tools/Package-All.py rename to src/tools/packager/Package-All.py index b7729dac1..dbf1c7202 100644 --- a/src/tools/Package-All.py +++ b/src/tools/packager/Package-All.py @@ -4,7 +4,7 @@ # 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 +# https://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, @@ -16,9 +16,12 @@ """ Merges individual python modules from src to the MsftLinuxPatchExt files in the out directory. Relative source and destination paths for the extension are auto-detected if the optional src parameter is not present. -How to use: python Package.py """ +How to use: python Package-All.py +Note: Package-All.py internally invokes Package-Core.py to generate MsftLinuxPatchCore.py """ from __future__ import print_function + +import shutil import sys import os import errno @@ -26,11 +29,13 @@ from shutil import copyfile from shutil import make_archive import subprocess +import xml.etree.ElementTree as et # imports in VERY_FIRST_IMPORTS, order should be kept VERY_FIRST_IMPORTS = [ 'from __future__ import print_function\n', - 'from abc import ABCMeta, abstractmethod\n'] + 'from abc import ABCMeta, abstractmethod\n', + 'from distutils.version import LooseVersion\n'] GLOBAL_IMPORTS = set() @@ -59,15 +64,16 @@ def write_merged_code(code, merged_file_full_path): def insert_copyright_notice(merged_file_full_path, merged_file_name): - notice = '# --------------------------------------------------------------------------------------------------------------------\n' + notice = '# coding=utf-8\n' + notice += '# --------------------------------------------------------------------------------------------------------------------\n' notice += '# \n' - notice += '# Copyright 2020 Microsoft Corporation\n' \ + notice += '# Copyright ' + str(datetime.date.today().year) + ' Microsoft Corporation\n' \ '#\n' \ '# Licensed under the Apache License, Version 2.0 (the "License");\n' \ '# you may not use this file except in compliance with the License.\n' \ '# You may obtain a copy of the License at\n' \ '#\n' \ - '# http://www.apache.org/licenses/LICENSE-2.0\n' \ + '# https://www.apache.org/licenses/LICENSE-2.0\n' \ '#\n' \ '# Unless required by applicable law or agreed to in writing, software\n' \ '# distributed under the License is distributed on an "AS IS" BASIS,\n' \ @@ -104,15 +110,15 @@ def prepend_content_to_file(content, file_name): os.rename(temp_file, file_name) -def generate_compiled_script(source_code_path, merged_file_full_path, merged_file_name, environment): +def generate_compiled_script(source_code_path, merged_file_full_path, merged_file_name, environment, new_version): try: - print('\n\n=============================== GENERATING ' + merged_file_name + '... =============================================================\n') + print('\n=============================== (2/3) GENERATING ' + merged_file_name + '... ================================\n') - print('========== Delete old extension file if it exists.') + print('------------- Deleting old extension file if it exists.') if os.path.exists(merged_file_full_path): os.remove(merged_file_full_path) - print('\n========== Merging modules: \n') + print('------------- Merging modules: ') modules_to_be_merged = [] for root, dirs, files in os.walk(source_code_path): for file_name in files: @@ -137,19 +143,26 @@ def generate_compiled_script(source_code_path, merged_file_full_path, merged_fil write_merged_code(codes, merged_file_full_path) print("") - print('\n========== Prepend all import statements\n') + print('------------- Prepend all import statements') insert_imports(GLOBAL_IMPORTS, merged_file_full_path) insert_imports(VERY_FIRST_IMPORTS, merged_file_full_path) - print('========== Set Copyright, Version and Environment. Also enforce UNIX-style line endings.\n') + print('------------- Set Copyright, Version and Environment. Also enforce UNIX-style line endings.') insert_copyright_notice(merged_file_full_path, merged_file_name) - timestamp = datetime.datetime.utcnow().strftime("%y%m%d-%H%M") - replace_text_in_file(merged_file_full_path, '[%exec_name%]', merged_file_name.split('.')[0]) - replace_text_in_file(merged_file_full_path, '[%exec_sub_ver%]', timestamp) + try: + # Python 3.2+ + now = datetime.datetime.now(datetime.timezone.utc) + except AttributeError: + # Older Python fallback (naive UTC) + now = datetime.datetime.utcnow() + date = now.strftime("%y.%m.%d") + replace_text_in_file(merged_file_full_path, '[%exec_name%]', merged_file_name) + replace_text_in_file(merged_file_full_path, '[%exec_ver%]', str(new_version)) + replace_text_in_file(merged_file_full_path, '[%exec_build_date%]', date) replace_text_in_file(merged_file_full_path, 'Constants.UNKNOWN_ENV', environment) replace_text_in_file(merged_file_full_path, '\r\n', '\n') - print("========== Merged extension code was saved to:\n{0}\n".format(merged_file_full_path)) + print("------------- Merged extension code was saved to:\n{0}\n".format(merged_file_full_path)) except Exception as error: print('Exception during merge python modules: ' + repr(error)) @@ -165,7 +178,7 @@ def main(argv): # Determine code path if not specified if len(argv) < 2: # auto-detect src path - source_code_path = os.path.dirname(os.path.realpath(__file__)).replace("tools", os.path.join("extension", "src")) + source_code_path = os.path.dirname(os.path.realpath(__file__)).replace(os.path.join("tools", "packager"), os.path.join("extension", "src")) if os.path.exists(os.path.join(source_code_path, "__main__.py")) is False: print("Invalid extension source code path. Check enlistment.\n") return @@ -180,39 +193,39 @@ def main(argv): working_directory = os.path.abspath(os.path.join(source_code_path, os.pardir, os.pardir)) merge_file_directory = os.path.join(working_directory, 'out') try: + if os.path.exists(merge_file_directory): + shutil.rmtree(merge_file_directory) os.makedirs(merge_file_directory) except OSError as e: if e.errno != errno.EEXIST: raise # Invoke core business logic code packager - exec_core_build_path = os.path.join(working_directory, 'tools', 'Package-Core.py') + exec_core_build_path = os.path.join(working_directory, 'tools', 'packager', 'Package-Core.py') subprocess.call('python ' + exec_core_build_path, shell=True) + # Get version from manifest for code + new_version = None + manifest_xml_file_path = os.path.join(working_directory, 'extension', 'src', 'manifest.xml') + manifest_tree = et.parse(manifest_xml_file_path) + manifest_root = manifest_tree.getroot() + for i in range(0, len(manifest_root)): + if 'Version' in str(manifest_root[i]): + new_version = manifest_root[i].text + if new_version is None: + raise Exception("Unable to determine target version.") + # Generated compiled scripts at the destination merged_file_details = [('MsftLinuxPatchExt.py', 'Constants.PROD')] for merged_file_detail in merged_file_details: merged_file_destination = os.path.join(working_directory, 'out', merged_file_detail[0]) - generate_compiled_script(source_code_path, merged_file_destination, merged_file_detail[0], merged_file_detail[1]) + generate_compiled_script(source_code_path, merged_file_destination, merged_file_detail[0], merged_file_detail[1], new_version) # GENERATING EXTENSION - print('\n\n=============================== GENERATING LinuxPatchExtension.zip... =============================================================\n') - # Rev handler version - # print('\n========== Revising extension version.') - # manifest_xml_file_path = os.path.join(working_directory, 'extension', 'src', 'manifest.xml') - # manifest_tree = et.parse(manifest_xml_file_path) - # manifest_root = manifest_tree.getroot() - # for i in range(0, len(manifest_root)): - # if 'Version' in str(manifest_root[i]): - # current_version = manifest_root[i].text - # version_split = current_version.split('.') - # version_split[len(version_split)-1] = str(int(version_split[len(version_split)-1]) + 1) - # new_version = '.'.join(version_split) - # print("Changing extension version from {0} to {1}.".format(current_version, new_version)) - # replace_text_in_file(manifest_xml_file_path, current_version, new_version) + print('\n=============================== (3/3) GENERATING LinuxPatchExtension.zip... ==============================\n') # Copy extension files - print('\n========== Copying extension files + enforcing UNIX style line endings.\n') + print('------------- Copying extension files + enforcing UNIX style line endings.') ext_files = ['HandlerManifest.json', 'manifest.xml', 'MsftLinuxPatchExtShim.sh'] for ext_file in ext_files: ext_file_src = os.path.join(working_directory, 'extension', 'src', ext_file) @@ -230,18 +243,18 @@ def main(argv): os.remove(ext_zip_file_path_dest) # Generate zip - print('\n========== Generating extension zip.\n') - make_archive(os.path.splitext(ext_zip_file_path_src)[0], 'zip', os.path.join(working_directory, 'out'), '.') + print('------------- Generating extension zip.') + make_archive(os.path.splitext(ext_zip_file_path_src)[0], 'zip', os.path.join(working_directory, 'out'), '..') copyfile(ext_zip_file_path_src, ext_zip_file_path_dest) os.remove(ext_zip_file_path_src) # Remove extension file copies - print('\n========== Cleaning up environment.\n') + print('------------- Cleaning up environment.') for ext_file in ext_files: ext_file_path = os.path.join(working_directory, 'out', ext_file) os.remove(ext_file_path) - print("========== Extension ZIP was saved to:\n{0}\n".format(ext_zip_file_path_dest)) + print("------------- Extension ZIP was saved to:\n{0}\n".format(ext_zip_file_path_dest)) except Exception as error: print('Exception during merge python modules: ' + repr(error)) diff --git a/src/tools/Package-Core.py b/src/tools/packager/Package-Core.py similarity index 75% rename from src/tools/Package-Core.py rename to src/tools/packager/Package-Core.py index afbcc01fb..52c1cd71e 100644 --- a/src/tools/Package-Core.py +++ b/src/tools/packager/Package-Core.py @@ -4,7 +4,7 @@ # 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 +# https://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, @@ -14,9 +14,9 @@ # # Requires Python 2.7+ -""" Merges individual python modules from src to the PatchMicrosoftOMSLinuxComputer.py and MsftLinuxPatchCore.py files in the out directory. -Relative source and destination paths for the patch runbook are auto-detected if the optional src parameter is not present. -How to use: python Package.py """ +""" Merges individual python modules from src to the MsftLinuxPatchCore.py files in the out directory. +Relative source and destination paths for the extension Core are auto-detected if the optional src parameter is not present. +How to use: python Package-Core.py """ from __future__ import print_function @@ -25,6 +25,7 @@ import os import errno import datetime +import xml.etree.ElementTree as et # imports in VERY_FIRST_IMPORTS, order should be kept @@ -62,15 +63,16 @@ def write_merged_code(code, merged_file_full_path): def insert_copyright_notice(merged_file_full_path, merged_file_name): - notice = '# --------------------------------------------------------------------------------------------------------------------\n' + notice = '# coding=utf-8\n' + notice += '# --------------------------------------------------------------------------------------------------------------------\n' notice += '# \n' - notice += '# Copyright 2020 Microsoft Corporation\n' \ + notice += '# Copyright ' + str(datetime.date.today().year) + ' Microsoft Corporation\n' \ '#\n' \ '# Licensed under the Apache License, Version 2.0 (the "License");\n' \ '# you may not use this file except in compliance with the License.\n' \ '# You may obtain a copy of the License at\n' \ '#\n' \ - '# http://www.apache.org/licenses/LICENSE-2.0\n' \ + '# https://www.apache.org/licenses/LICENSE-2.0\n' \ '#\n' \ '# Unless required by applicable law or agreed to in writing, software\n' \ '# distributed under the License is distributed on an "AS IS" BASIS,\n' \ @@ -107,15 +109,15 @@ def prepend_content_to_file(content, file_name): os.rename(temp_file, file_name) -def generate_compiled_script(source_code_path, merged_file_full_path, merged_file_name, environment): +def generate_compiled_script(source_code_path, merged_file_full_path, merged_file_name, environment, new_version): try: - print('\n\n=============================== GENERATING ' + merged_file_name + '... =============================================================\n') + print('\n=============================== (1/3) GENERATING ' + merged_file_name + '... ===============================\n') - print('========== Delete old core file if it exists.') + print('------------- Delete old core file if it exists.') if os.path.exists(merged_file_full_path): os.remove(merged_file_full_path) - print('\n========== Merging modules: \n') + print('------------- Merging modules:') modules_to_be_merged = [] for root, dirs, files in os.walk(source_code_path): for file_name in files: @@ -128,7 +130,7 @@ def generate_compiled_script(source_code_path, merged_file_full_path, merged_fil continue elif 'external_dependencies' in file_path: continue - elif os.path.basename(file_path) in ('PackageManager.py', 'Constants.py', 'LifecycleManager.py', 'SystemctlManager.py'): + elif os.path.basename(file_path) in ('PatchOperator.py', 'PackageManager.py', 'Constants.py', 'LifecycleManager.py', 'SystemctlManager.py'): modules_to_be_merged.insert(0, file_path) elif os.path.basename(file_path) == 'TdnfPackageManager.py': # Insert before `AzL3PackageManager.py`; fallback to append. @@ -152,18 +154,25 @@ def generate_compiled_script(source_code_path, merged_file_full_path, merged_fil write_merged_code(codes, merged_file_full_path) print("") - print('\n========== Prepend all import statements\n') + print('------------- Prepend all import statements') insert_imports(GLOBAL_IMPORTS, merged_file_full_path) insert_imports(VERY_FIRST_IMPORTS, merged_file_full_path) - print('========== Set Copyright, Version and Environment. Also enforce UNIX-style line endings.\n') + print('------------- Set Copyright, Version and Environment. Also enforce UNIX-style line endings.') insert_copyright_notice(merged_file_full_path, merged_file_name) - timestamp = datetime.datetime.utcnow().strftime("%y%m%d-%H%M") - replace_text_in_file(merged_file_full_path, '[%exec_name%]', merged_file_name.split('.')[0]) - replace_text_in_file(merged_file_full_path, '[%exec_sub_ver%]', timestamp) + try: + # Python 3.2+ + now = datetime.datetime.now(datetime.timezone.utc) + except AttributeError: + # Older Python fallback (naive UTC) + now = datetime.datetime.utcnow() + date = now.strftime("%y.%m.%d") + replace_text_in_file(merged_file_full_path, '[%exec_name%]', merged_file_name) + replace_text_in_file(merged_file_full_path, '[%exec_ver%]', str(new_version)) + replace_text_in_file(merged_file_full_path, '[%exec_build_date%]', date) replace_text_in_file(merged_file_full_path, '\r\n', '\n') - print("========== Merged core code was saved to:\n{0}\n".format(merged_file_full_path)) + print('------------- Merged core code was saved to:\n{0}\n'.format(merged_file_full_path)) except Exception as error: print('Exception during merge python modules: ' + repr(error)) @@ -172,13 +181,13 @@ def generate_compiled_script(source_code_path, merged_file_full_path, merged_fil def add_external_dependencies(external_dependencies_destination, external_dependencies_source_code_path): try: - print('\n========= ADDING EXTERNAL DEPENDENCIES\n') + print('\n------------- ADDING EXTERNAL DEPENDENCIES') - print('========== Deleting old dependencies if they exists.') + print('------------- Deleting old dependencies if they exist.') if os.path.exists(external_dependencies_destination): shutil.rmtree(external_dependencies_destination) - print('\n========== Adding all dependencies to external_dependencies directory: \n') + print('------------- Adding all dependencies to external_dependencies directory: ') dependencies_to_be_added = [] for root, dirs, files in os.walk(external_dependencies_source_code_path): for file_name in files: @@ -191,7 +200,7 @@ def add_external_dependencies(external_dependencies_destination, external_depend print(format(os.path.basename(dependency)), end=', ') shutil.copyfile(dependency, os.path.join(external_dependencies_destination, os.path.basename(dependency))) - print("\n\n========== External dependencies saved to:\n{0}\n".format(external_dependencies_destination)) + print('\n------------- External dependencies saved to:\n{0}\n'.format(external_dependencies_destination)) except Exception as error: print('Exception during adding external dependencies: ' + repr(error)) @@ -204,10 +213,15 @@ def main(argv): # Clear os.system('cls' if os.name == 'nt' else 'clear') + # Pro packager branding + print("==========================================================================================================\n") + print(" * AzGPS LINUX PATCH EXTENSION PACKAGER") + print(" * Microsoft Azure \\ Compute Platform \\ Azure Guest Patching Service") + # Determine code path if not specified if len(argv) < 2: # auto-detect src path - source_code_path = os.path.dirname(os.path.realpath(__file__)).replace("tools", os.path.join("core","src")) + source_code_path = os.path.dirname(os.path.realpath(__file__)).replace(os.path.join("tools", "packager"), os.path.join("core","src")) if os.path.exists(os.path.join(source_code_path, "__main__.py")) is False: print("Invalid core source code path. Check enlistment.\n") return @@ -227,11 +241,22 @@ def main(argv): if e.errno != errno.EEXIST: raise + # Get version from manifest for code + new_version = None + manifest_xml_file_path = os.path.join(working_directory, 'extension', 'src', 'manifest.xml') + manifest_tree = et.parse(manifest_xml_file_path) + manifest_root = manifest_tree.getroot() + for i in range(0, len(manifest_root)): + if 'Version' in str(manifest_root[i]): + new_version = manifest_root[i].text + if new_version is None: + raise Exception("Unable to determine target version.") + # Generated compiled scripts at the destination merged_file_details = [('MsftLinuxPatchCore.py', 'Constants.PROD')] for merged_file_detail in merged_file_details: merged_file_destination = os.path.join(working_directory, 'out', merged_file_detail[0]) - generate_compiled_script(source_code_path, merged_file_destination, merged_file_detail[0], merged_file_detail[1]) + generate_compiled_script(source_code_path, merged_file_destination, merged_file_detail[0], merged_file_detail[1], new_version) # add all dependencies under core/src/external_dependencies to destination directory external_dependencies_destination = os.path.join(merge_file_directory, 'external_dependencies') @@ -245,4 +270,3 @@ def main(argv): if __name__ == "__main__": main(sys.argv) - diff --git a/src/tools/packager/Publish.py b/src/tools/packager/Publish.py new file mode 100644 index 000000000..90fb7ad1e --- /dev/null +++ b/src/tools/packager/Publish.py @@ -0,0 +1,101 @@ +# Copyright 2025 Microsoft Corporation +# +# 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 +# +# https://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. +# +# Requires Python 2.7+ + +""" Publishes a new extension version by incrementing the version number in the manifest.xml file.""" + +from __future__ import print_function +import sys +import os +import errno +import subprocess +import xml.etree.ElementTree as et + +# noinspection PyPep8 +def replace_text_in_file(file_path, old_text, new_text): + with open(file_path, 'rb') as file_handle: text = file_handle.read() + text = text.replace(old_text.encode(encoding='UTF-8'), new_text.encode(encoding='UTF-8')) + with open(file_path, 'wb') as file_handle: file_handle.write(text) + + +def main(argv): + try: + # Clear + os.system('cls' if os.name == 'nt' else 'clear') + + # Determine code path if not specified + if len(argv) < 2: + # auto-detect src path + source_code_path = os.path.dirname(os.path.realpath(__file__)).replace(os.path.join("tools", "packager"), os.path.join("extension", "src")) + if os.path.exists(os.path.join(source_code_path, "__main__.py")) is False: + print("Invalid extension source code path. Check enlistment.\n") + return + else: + # explicit src path parameter + source_code_path = argv[1] + if os.path.exists(os.path.join(source_code_path, "ActionHandler.py")) is False: + print("Invalid extension source code path. Check src parameter.\n") + return + + # Prepare destination for compiled scripts + working_directory = os.path.abspath(os.path.join(source_code_path, os.pardir, os.pardir)) + merge_file_directory = os.path.join(working_directory, 'out') + try: + os.makedirs(merge_file_directory) + except OSError as e: + if e.errno != errno.EEXIST: + raise + + # Get version from manifest for code + new_version = None + manifest_xml_file_path = os.path.join(working_directory, 'extension', 'src', 'manifest.xml') + manifest_tree = et.parse(manifest_xml_file_path) + manifest_root = manifest_tree.getroot() + for i in range(0, len(manifest_root)): + if 'Version' in str(manifest_root[i]): + new_version = manifest_root[i].text + if new_version is None: + raise Exception("Unable to determine target version.") + + # Rev handler version + current_version = "Unknown" + manifest_xml_file_path = os.path.join(working_directory, 'extension', 'src', 'manifest.xml') + manifest_tree = et.parse(manifest_xml_file_path) + manifest_root = manifest_tree.getroot() + for i in range(0, len(manifest_root)): + if 'Version' in str(manifest_root[i]): + current_version = manifest_root[i].text + version_split = current_version.split('.') + version_split[len(version_split)-1] = str(int(version_split[len(version_split)-1]) + 1) + new_version = '.'.join(version_split) + replace_text_in_file(manifest_xml_file_path, current_version, new_version) + + # Invoke core business logic code packager + exec_core_build_path = os.path.join(working_directory, 'tools', 'packager', 'Package-All.py') + subprocess.call('python ' + exec_core_build_path, shell=True) + + # Report extension version change + print("==========================================================================================================\n") + print("! PUBLISHER > THE EXTENSION VERSION WAS CHANGED FROM {0} to {1}. DO NOT RE-RUN.".format(current_version, new_version)) + print("! > This is only meant to be run once prior to extension publish and pushed as a PR. Not for automation.") + print("! > If this was an error, revert the extension manifest, and only use the build script instead of publish.\n") + + except Exception as error: + print('Exception during merge python modules: ' + repr(error)) + raise + + +if __name__ == "__main__": + main(sys.argv)