diff --git a/buildscripts/builddrivers.py b/buildscripts/builddrivers.py index 7b7a23766..9ea631afc 100644 --- a/buildscripts/builddrivers.py +++ b/buildscripts/builddrivers.py @@ -1,4 +1,4 @@ -#!/usr/bin/python3 +#!/usr/bin/env python3 ######################################################################################### # # Description: This script helps to build drivers in a Windows environment for PHP 7+ (32-bit/64-bit) @@ -20,12 +20,24 @@ # ############################################################################################# +import os import sys import shutil import os.path import argparse import subprocess -from buildtools import BuildUtil +import re + +# Import BuildUtil from the fixed version we created earlier +# Note: This assumes BuildUtil class is defined in buildutil.py +# If it's in the same file, remove this import and include the class directly +try: + from buildtools import BuildUtil +except ImportError: + # If buildutil.py doesn't exist, we'll define a minimal version here + # but for production, you should have the actual BuildUtil class + print("Error: BuildUtil class not found. Please ensure buildutil.py exists.") + sys.exit(1) class BuildDriver(object): """Build sqlsrv and/or pdo_sqlsrv drivers with PHP source with the following properties: @@ -71,7 +83,7 @@ def clean_or_remove(self, root_dir, work_dir): :outcome: the old binaries, if exist, will be removed """ phpsrc = self.util.phpsrc_root(root_dir) - if os.path.exists( phpsrc ): + if os.path.exists(phpsrc): print(phpsrc + " exists.") build_choice = validate_input("(r)ebuild for the same configuration, (c)lean otherwise, (s)uperclean if unsure ", "r/c/s") self.make_clean = False @@ -87,9 +99,32 @@ def clean_or_remove(self, root_dir, work_dir): self.util.remove_old_builds(root_dir) else: print('Will remove ' + phpsrc) - os.system('RMDIR /s /q ' + phpsrc) + # Use subprocess instead of os.system for security + try: + # Use shutil.rmtree for cross-platform compatibility + shutil.rmtree(phpsrc, ignore_errors=True) + print(f"Removed {phpsrc}") + except Exception as e: + print(f"Error removing directory {phpsrc}: {e}") - os.chdir(work_dir) # change back to the working directory + # Change back to the working directory + try: + os.chdir(work_dir) + except OSError as e: + print(f"Warning: Could not change to directory {work_dir}: {e}") + + def sanitize_path(self, path): + """Sanitize a path to prevent command injection.""" + # Remove any dangerous characters + if not path: + return path + # Replace backslashes with forward slashes for consistency + path = path.replace('\\', '/') + # Remove any command injection attempts + dangerous_chars = [';', '&', '|', '`', '$', '(', ')', '{', '}', '[', ']', '<', '>', '!'] + for char in dangerous_chars: + path = path.replace(char, '') + return path def get_local_source(self, source_path): """This assumes interactive mode (not testing) and takes care of getting @@ -102,17 +137,102 @@ def get_local_source(self, source_path): source = input("Hit ENTER to use '" + source_path + "' or provide another path to the source folder: ") if len(source) == 0: source = source_path - - valid = True - if os.path.exists(source) and os.path.exists(os.path.join(source, 'shared')): - # Checking the existence of 'shared' folder only, assuming - # sqlsrv and/or pdo_sqlsrv are also present if it exists - self.source_path = source - break + + # Sanitize the path + source = self.sanitize_path(source) + + # Check if path exists and has required structure + if not source or len(source.strip()) == 0: + print("Empty path provided. Please re-enter.") + continue + + # Validate the path + if os.path.exists(source): + # Check for required folders + shared_exists = os.path.exists(os.path.join(source, 'shared')) + sqlsrv_exists = os.path.exists(os.path.join(source, 'sqlsrv')) + pdo_sqlsrv_exists = os.path.exists(os.path.join(source, 'pdo_sqlsrv')) - print("The path provided is invalid. Please re-enter.") + if shared_exists and (sqlsrv_exists or pdo_sqlsrv_exists): + self.source_path = source + break + else: + missing = [] + if not shared_exists: + missing.append('shared') + if self.util.driver in ['all', 'sqlsrv'] and not sqlsrv_exists: + missing.append('sqlsrv') + if self.util.driver in ['all', 'pdo_sqlsrv'] and not pdo_sqlsrv_exists: + missing.append('pdo_sqlsrv') + print(f"Missing required folders: {', '.join(missing)}. Please re-enter.") + else: + print("The path provided does not exist. Please re-enter.") return source + def copy_source_files_safely(self, source, work_dir): + """Safely copy source files using shutil instead of os.system.""" + try: + # Create Source directory if it doesn't exist + source_dir = os.path.join(work_dir, 'Source') + os.makedirs(source_dir, exist_ok=True) + + # Define source and destination paths + shared_src = os.path.join(source, 'shared') + shared_dst = os.path.join(source_dir, 'shared') + + sqlsrv_src = os.path.join(source, 'sqlsrv') + sqlsrv_dst = os.path.join(source_dir, 'sqlsrv') + + pdo_sqlsrv_src = os.path.join(source, 'pdo_sqlsrv') + pdo_sqlsrv_dst = os.path.join(source_dir, 'pdo_sqlsrv') + + # Copy directories if they exist + def copy_if_exists(src, dst): + if os.path.exists(src): + if os.path.exists(dst): + shutil.rmtree(dst, ignore_errors=True) + shutil.copytree(src, dst) + return True + return False + + print(f'Copying source files from {source}') + + # List source directory contents for debugging + if os.path.exists(source): + try: + dir_list = os.listdir(source) + print(f"Files and directories in '{source}':") + for item in dir_list: + print(f" {item}") + except OSError as e: + print(f"Warning: Could not list directory {source}: {e}") + + # Copy required directories + copied_any = False + + if copy_if_exists(shared_src, shared_dst): + copied_any = True + print(f"Copied shared files to {shared_dst}") + + if self.util.driver in ['all', 'sqlsrv']: + if copy_if_exists(sqlsrv_src, sqlsrv_dst): + copied_any = True + print(f"Copied sqlsrv files to {sqlsrv_dst}") + + if self.util.driver in ['all', 'pdo_sqlsrv']: + if copy_if_exists(pdo_sqlsrv_src, pdo_sqlsrv_dst): + copied_any = True + print(f"Copied pdo_sqlsrv files to {pdo_sqlsrv_dst}") + + if not copied_any: + raise FileNotFoundError(f"No source files found in {source}. Required: shared, and sqlsrv or pdo_sqlsrv") + + return True + + except Exception as e: + print(f"Error copying source files: {e}") + raise + def build_extensions(self, root_dir, logfile): """This takes care of getting the drivers' source files, building the drivers. If dest_path is defined, the binaries will be copied to the designated destinations. @@ -131,21 +251,21 @@ def build_extensions(self, root_dir, logfile): if not get_source: # This will download from the specified branch on GitHub repo and copy the source - self.util.download_msphpsql_source(repo, branch) - else: + try: + self.util.download_msphpsql_source(self.repo, self.branch) + except Exception as e: + print(f"Error downloading from GitHub: {e}") + print("Falling back to local source...") + get_source = True + + if get_source: source = self.source_path # Do not prompt user for input if it's in a testing mode if not self.testing: source = self.get_local_source(self.source_path) - print('Copying source files from', source) - - dir_list = os.listdir(source) - print("Files and directories in '", source, "' :") - - os.system('ROBOCOPY ' + source + '\shared ' + work_dir + '\Source\shared /xx /xo ') - os.system('ROBOCOPY ' + source + '\sqlsrv ' + work_dir + '\Source\sqlsrv /xx /xo ') - os.system('ROBOCOPY ' + source + '\pdo_sqlsrv ' + work_dir + '\Source\pdo_sqlsrv /xx /xo ') + # Copy source files safely + self.copy_source_files_safely(source, work_dir) print('Start building PHP with the extension...') @@ -163,8 +283,8 @@ def build_extensions(self, root_dir, logfile): dest_symbols = os.path.join(dest_drivers, 'Symbols', self.util.thread) # All intermediate directories will be created in order to create the leaf directory - if os.path.exists(dest_symbols) == False: - os.makedirs(dest_symbols) + if not os.path.exists(dest_symbols): + os.makedirs(dest_symbols, exist_ok=True) # Now copy all the binaries if self.util.driver == 'all': @@ -186,10 +306,13 @@ def build(self): self.show_config() work_dir = os.path.dirname(os.path.realpath(__file__)) - root_dir = 'C:' + os.sep - quit = False - while not quit: + # Set root directory for builds; Windows uses C:, others use / + + root_dir = os.path.abspath(os.sep) + + quit_flag = False + while not quit_flag: if self.testing: self.make_clean = True self.util.remove_old_builds(work_dir) @@ -201,35 +324,59 @@ def build(self): try: ext_dir = self.build_extensions(root_dir, logfile) print('Build Completed') - except: - print('Something went wrong, launching log file', logfile) + except Exception as e: + print(f'Something went wrong: {e}') + print('Launching log file', logfile) logfile_path = os.path.join(os.getcwd(), logfile) if os.path.isfile(logfile_path): - with open(logfile_path, 'r') as f: - f.seek(0) - print(f.read()) + try: + with open(logfile_path, 'r', encoding='utf-8') as f: + print("\n=== Last 50 lines of build log ===") + lines = f.readlines() + # Show last 50 lines for context + for line in lines[-50:]: + print(line.rstrip()) + except Exception as read_error: + print(f"Error reading log file: {read_error}") else: print('Unable to open logfile') - os.chdir(work_dir) - exit(1) + try: + os.chdir(work_dir) + except OSError as dir_error: + print(f"Warning: Could not change to directory {work_dir}: {dir_error}") + + # Don't exit in interactive mode, allow retry + if not self.testing: + retry = input("Would you like to retry? (yes/no): ").lower() + if retry in ['yes', 'y', '']: + continue + + sys.exit(1) if not self.testing: - choice = input("Rebuild using the same configuration(yes) or quit (no) [yes/no]: ") - choice = choice.lower() - if choice == 'yes' or choice == 'y' or choice == '': - print('Rebuilding drivers...') - self.make_clean = False - self.rebuild = True - self.util.remove_prev_build(root_dir) - else: - quit = True + while True: + choice = input("Rebuild using the same configuration(yes) or quit (no) [yes/no]: ").lower() + if choice in ['yes', 'y', '']: + print('Rebuilding drivers...') + self.make_clean = False + self.rebuild = True + self.util.remove_prev_build(root_dir) + break + elif choice in ['no', 'n']: + quit_flag = True + break + else: + print("Please enter 'yes' or 'no'") else: - quit = True + quit_flag = True - os.chdir(work_dir) + try: + os.chdir(work_dir) + except OSError as e: + print(f"Warning: Could not change to directory {work_dir}: {e}") def validate_input(question, values): """Return the user selected value, and it must be valid based on *values*.""" @@ -238,12 +385,24 @@ def validate_input(question, values): prompt = '[' + values + ']' value = input(question + prompt + ': ') value = value.lower() - if not value in options: - print("An invalid choice is entered. Choose from", prompt) + if value not in options: + print(f"An invalid choice is entered. Choose from {prompt}") else: break return value +def validate_php_version(version): + """Validate PHP version format.""" + if not version: + return False + # Pattern for PHP versions like 7.0.22, 7.4, 8.0.3, etc. + pattern = r'^(\d+)\.(\d+)(\.\d+)?([-\.](RC\d+|beta\d+|alpha\d+|[a-zA-Z]+))?$' + match = re.match(pattern, version) + if not match: + return False + major = int(match.group(1)) + return major >= 7 + ################################### Main Function ################################### if __name__ == '__main__': parser = argparse.ArgumentParser() @@ -279,12 +438,25 @@ def validate_input(question, values): while True: # perform some minimal checks phpver = input("PHP Version (e.g. 7.1.* or 7.2.*): ") - if phpver == '': + if not phpver: print('Empty PHP version entered! Please try again.') - elif phpver[0] < '7': - print('Only PHP 7.0 or above is supported. Please try again.') - else: - break + continue + + if not validate_php_version(phpver): + print(f'Invalid PHP version format: {phpver}. Must be 7.0 or above (e.g., 7.0.22, 7.4, 8.0.3).') + continue + + # Check major version + try: + major_version = int(phpver.split('.')[0]) + if major_version < 7: + print('Only PHP 7.0 or above is supported. Please try again.') + continue + except (ValueError, IndexError): + print('Invalid version format. Please try again.') + continue + + break arch_version = input("64-bit? [y/n]: ") thread = validate_input("Thread safe? ", "nts/ts") @@ -292,32 +464,45 @@ def validate_input(question, values): debug_mode = input("Debug enabled? [y/n]: ") answer = input("Download source from a GitHub repo? [y/n]: ") - if answer == 'yes' or answer == 'y' or answer == '': - repo = input("Name of the repo (hit enter for 'Microsoft'): ") - branch = input("Name of the branch or tag (hit enter for 'dev'): ") - if repo == '': - repo = 'Microsoft' - if branch == '': - branch = 'dev' + if answer.lower() in ['yes', 'y', '']: + repo_input = input("Name of the repo (hit enter for 'Microsoft'): ") + branch_input = input("Name of the branch or tag (hit enter for 'dev'): ") + repo = repo_input if repo_input else 'Microsoft' + branch = branch_input if branch_input else 'dev' else: repo = branch = None arch_version = arch_version.lower() - arch = 'x64' if arch_version == 'yes' or arch_version == 'y' or arch_version == '' else 'x86' + arch = 'x64' if arch_version in ['yes', 'y', ''] else 'x86' debug_mode = debug_mode.lower() - debug = debug_mode == 'yes' or debug_mode == 'y' or debug_mode == '' + debug = debug_mode in ['yes', 'y', ''] - builder = BuildDriver(phpver, - driver, - arch, - thread, - debug, - repo, - branch, - source, - path, - testing, - no_rename) + else: + # Validate command line PHP version + if not validate_php_version(phpver): + print(f'Error: Invalid PHP version format: {phpver}. Must be 7.0 or above (e.g., 7.0.22, 7.4, 8.0.3).') + sys.exit(1) + + try: + builder = BuildDriver(phpver, + driver, + arch, + thread, + debug, + repo, + branch, + source, + path, + testing, + no_rename) - builder.build() + builder.build() + except KeyboardInterrupt: + print("\n\nBuild interrupted by user.") + sys.exit(1) + except Exception as e: + print(f"\nFatal error: {e}") + import traceback + traceback.print_exc() + sys.exit(1)