diff --git a/buildscripts/README.md b/buildscripts/README.md new file mode 100644 index 00000000..3a533c27 --- /dev/null +++ b/buildscripts/README.md @@ -0,0 +1,98 @@ +# Windows + +## Prerequisites + +To build extensions for + * PHP 7.0* or PHP 7.1*, install Visual Studio 2015 and make sure C++ tools are enabled. + * PHP 7.2*, install Visual Studio 2017, including Visual C++ toolset, the Windows SDK components, and Git for Windows. + +To use the sample build scripts `builddrivers.py` and `buildtools.py`, install Python 3.x in Windows. + +## Compile the drivers + +You must first be able to build PHP 7.* without including these extensions. For help with doing this, see the [official PHP website](https://wiki.php.net/internals/windows/stepbystepbuild) for building PHP 7.0* or PHP 7.1* on Windows or [PHP SDK page](https://github.com/OSTC/php-sdk-binary-tools) for the new instructions for building PHP 7.2 and/or above. + +The Microsoft Drivers for PHP for SQL Server have been compiled and tested with PHP 7.0.* and 7.1.* using the Visual C++ 2015 as well as PHP 7.2.0 beta using the Visual C++ 2017 v15.0. + +### Manually building from source + +1. Download the *source* directory from this repository + +2. Make a copy of the *shared* folder as a subfolder in *sqlsrv* and/or *pdo_sqlsrv* folder + +3. Copy the *sqlsrv* and/or *pdo_sqlsrv* folder(s) into the PHP source ext subdirectory + +4. Run `buildconf --force` to rebuild the configure.js script to include the *sqlsrv* and/or *pdo_sqlsrv* driver(s). + +5. Run `configure.bat` with the desired driver options (as shown below) to generate the makefile. You can run `configure.bat --help` to see what other options are available. For example, for non-thread safe build, add this option `--disable-zts`. + * For SQLSRV add: `--enable-sqlsrv=shared` + * For PDO_SQLSRV add: `--enable-pdo --with-pdo-sqlsrv=shared` + +6. Run `nmake`. Optionally, you can run `nmake clean` first. + +7. To install the drivers, there are two ways: + * Run `nmake install`, or + * Copy the drivers: + * Find the directory where the newly compiled `php.exe` is + * Locate the compiled php_sqlsrv.dll and/or php_pdo_sqlsrv.dll + * Copy the dll(s) to the `ext` subfolder + +### Using the sample build scripts + +The sample build scripts, `builddrivers.py` and `buildtools.py`, are expected to build our extensions for PHP in Windows. + +#### Overview + +When asked to provide the PHP version, you should enter values like `7.1.7`. If it's alpha, beta, or RC version, make sure the name you provide matches the PHP tag name without the prefix `php-`. For example, for PHP 7.2 beta 2, the tag name is `php-7.2.0beta2`, so you should enter `7.2.0beta2`. Visit [PHP SRC]( https://github.com/php/php-src) to find the appropriate tag names. + +PHP recommends to unzip the PHP SDK into the shortest possible path, preferrably somewhere near the root drive. Therefore, this script will by default create a `php-sdk` folder in the C:\ drive, and this `php-sdk` directory tree will remain unless you remove it yourself. For ongoing development, we suggest you keep it around. The build scripts will handle updating the PHP SDK if a new version is available. + +#### Steps + +1. Launch a regular `cmd` prompt + +2. Change to the directory where the Python scripts `builddrivers.py` and `buildtools.py` are + +3. Interactive mode: + * Run `py builddrivers.py` to use the interactive mode. Use lower cases to answer the following questions: + * PHP Version (e.g. `7.1.7` or `7.2.0beta2`) + * 64-bit? + * Thread safe? + * Driver? + * Debug enabled? + * Download source from GitHub? + * For `yes/no` questions, you can simply hit `ENTER` key for `yes`. Other questions are self-explanatory. + +4. Use Command-line arguments + * Run `py builddrivers.py -h` to get a list of options and their descriptions + * For example, + * `py builddrivers.py --PHPVER=7.0.22 --ARCH=x64 --THREAD=nts --DRIVER=sqlsrv --SOURCE` + * `py builddrivers.py --PHPVER=7.1.8 --ARCH=x86 --THREAD=ts --DEBUG` + +5. Based on the given configuration, if the script detects the presence of the PHP source directory, you can choose whether to rebuild, clean or superclean: + * `rebuild` to build again using the same configuration (32 bit, thread safe, etc.) + * `clean` to remove previous builds (binaries) + * `superclean` to remove the entire `php--src` directory, which is often unnecessary + +6. If you choose not to download from a GitHub repository, you will be asked to provide the full path to your local Source folder. + +7. If the compilation is successful, you will be given the option to rebuild or quit. + +#### Troubleshooting + +If something went wrong or the build failed, the log file will be launched (you can find the log files in `C:\php-sdk`). Otherwise, the log file will not be shown, and they remain in `C:\php-sdk` until you remove them manually. + +In addition to the log files in `C:\php-sdk`, you can examine the contents of `C:\php-sdk\phpsdk-build-task.bat`, which is overwritten every time you run the build scripts. + +#### Testing mode and/or setting alternative destination + +If your main goal is to build the drivers for testing, and/or there is no need to keep the `php-sdk` directory around, you can invoke `py builddrivers.py` with the necessary command-line arguments plus `--TESTING`, which turns on the *testing* mode (it is False by default). + +Setting the testing mode automatically turns off the looping mechanism. When the build is finished, you will find a copy of the drivers (unless the build failed) and the `php-sdk` folder in the same directory of these Python scripts. + +In addition, you can set an alternative destination using `--DESTPATH=`, which is **None** by default. Note that these two options are *not* available in the interactive mode. However, they are particularly useful for testing purposes (such as testing in a virtual machine) in which these build scripts are copied to a temporary folder. After the drivers have been successfully compiled and copied to the designated location, the temporary folder can be safely removed. + + + + + diff --git a/buildscripts/builddrivers.py b/buildscripts/builddrivers.py new file mode 100644 index 00000000..f4e14e34 --- /dev/null +++ b/buildscripts/builddrivers.py @@ -0,0 +1,288 @@ +#!/usr/bin/python3 +######################################################################################### +# +# Description: This script helps to build drivers in a Windows environment for PHP 7+ (32-bit/64-bit) +# +# Requirement: +# python 3.x +# PHP SDK and PHP Source +# Driver source code folder / GitHub repository +# Visual Studio 2015 (PHP 7.0* and 7.1*) and Visual Studio 2017 (PHP 7.2*) +# +# Execution: Run with command line with required options. +# Examples: +# py builddrivers.py (for interactive mode) +# py builddrivers.py --PHPVER=7.0.22 --ARCH=x64 --THREAD=nts --DRIVER=all --DEBUG +# +# Output: Build the drivers using PHP SDK. When running for local development, if build is unsuccessful, +# the log file will be launched for examination. Otherwise, the drivers will be renamed +# and copied to the designated location (if defined). +# +############################################################################################# + +import sys +import shutil +import os.path +import argparse +from buildtools import BuildUtil + +class BuildDriver(object): + """Build sqlsrv and/or pdo_sqlsrv drivers with PHP source with the following properties: + + Attributes: + util # BuildUtil object whose constructor takes phpver, driver, arch, thread, debug + repo # GitHub repository + branch # GitHub repository branch + download_source # download source from GitHub or not + dest_path # alternative destination for the drivers (None for development builds) + rebuild # a boolean flag - whether the user is rebuilding + make_clean # a boolean flag - whether make clean is necessary + source_path # path to a local source folder + testing # whether the user has turned on testing mode + """ + + def __init__(self, phpver, driver, arch, thread, debug, repo, branch, download, path, testing): + self.util = BuildUtil(phpver, driver, arch, thread, debug) + self.repo = repo + self.branch = branch + self.download_source = download + self.dest_path = path + self.testing = testing + self.rebuild = False + self.make_clean = False + self.source_path = None # None initially but will be set later if not downloading from GitHub + + def show_config(self): + print() + print('PHP Version: ', self.util.phpver) + print('Arch: ', self.util.arch) + print('Thread: ', self.util.thread) + print('Driver: ', self.util.driver) + print('Debug enabled: ', self.util.debug_enabled) + print() + + def clean_or_remove(self, root_dir, work_dir): + """Only check this for local development and not rebuilding. If the php source directory + already exists, this will prompt user whether to rebuild, clean, or superclean, the last option + will remove the entire php source directory. + + :param root_dir: the C:\ drive + :param work_dir: the directory of this script + :outcome: the old binaries, if exist, will be removed + """ + phpsrc = self.util.phpsrc_root(root_dir) + 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 + if build_choice == 'r': + print('Will rebuild the binaries') + # only the old binaries based on the current configuration will be removed + self.util.remove_prev_build(root_dir) + elif build_choice == 'c': + print('Will make clean') + self.make_clean = True + # all old builds are removed, and this step is necessary because + # the user might have changed the configuration + self.util.remove_old_builds(root_dir) + else: + print('Will remove ' + phpsrc) + os.system('RMDIR /s /q ' + phpsrc) + + os.chdir(work_dir) # change back to the working directory + + 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. + + :param root_dir: the root directory + :param logfile: the name of the logfile + :outcome: the drivers and symbols will renamed and placed in the appropriate location(s) + + """ + work_dir = os.path.dirname(os.path.realpath(__file__)) + + if self.download_source: + # This will download from the specified branch on GitHub repo and copy the source + self.util.download_msphpsql_source(repo, branch) + else: + # This case only happens when building for development + while True: + if self.source_path is None: + source = input('Enter the full path to the Source folder: ') + else: + source = input("Hit ENTER to reuse '" + self.source_path + "' or provide another path to the Source folder: ") + if len(source) == 0: + source = self.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 + + print("The path provided is invalid. Please re-enter.") + + print('Copying source files from', 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 ') + + print('Start building PHP with the extension...') + + # If not testing, dest should be the root drive. Otherwise, dest should be None. + dest = None if self.testing else root_dir + + # ext_dir is the directory where we can find the built extension(s) + ext_dir = self.util.build_drivers(self.make_clean, dest, logfile) + + # Copy the binaries if a destination path is defined + if self.dest_path is not None: + dest_drivers = os.path.join(self.dest_path, self.util.major_version(), self.util.arch) + 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) + + # Now copy all the binaries + if self.util.driver == 'all': + self.util.copy_binary(ext_dir, dest_drivers, 'sqlsrv', '.dll') + self.util.copy_binary(ext_dir, dest_symbols, 'sqlsrv', '.pdb') + self.util.copy_binary(ext_dir, dest_drivers, 'pdo_sqlsrv', '.dll') + self.util.copy_binary(ext_dir, dest_symbols, 'pdo_sqlsrv', '.pdb') + else: + self.util.copy_binary(ext_dir, dest_drivers, self.util.driver, '.dll') + self.util.copy_binary(ext_dir, dest_symbols, self.util.driver, '.pdb') + + + def build(self): + """This is the main entry point of building drivers for PHP. + For development, this will loop till the user decides to quit. + """ + self.show_config() + + work_dir = os.path.dirname(os.path.realpath(__file__)) + root_dir = 'C:' + os.sep + + quit = False + while not quit: + if not self.rebuild and not self.testing: + self.clean_or_remove(root_dir, work_dir) + + logfile = self.util.get_logfile_name() + + try: + self.build_extensions(root_dir, logfile) + print('Build Completed') + except: + print('Something went wrong, launching log file', logfile) + # display log file only when not testing + if not self.testing: + os.startfile(os.path.join(root_dir, 'php-sdk', logfile)) + os.chdir(work_dir) + break + + 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 + else: + quit = True + + os.chdir(work_dir) + +def validate_input(question, values): + """Return the user selected value, and it must be valid based on *values*.""" + while True: + options = values.split('/') + prompt = '[' + values + ']' + value = input(question + prompt + ': ') + value = value.lower() + if not value in options: + print("An invalid choice is entered. Choose from", prompt) + else: + break + return value + +################################### Main Function ################################### +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--PHPVER', help="PHP version, e.g. 7.1.*, 7.2.* etc.") + parser.add_argument('--ARCH', choices=['x64', 'x86']) + parser.add_argument('--THREAD', choices=['nts', 'ts']) + parser.add_argument('--DRIVER', default='all', choices=['all', 'sqlsrv', 'pdo_sqlsrv'], help="driver to build (default: all)") + parser.add_argument('--DEBUG', action='store_true', help="enable debug mode (default: False)") + parser.add_argument('--REPO', default='Microsoft', help="GitHub repository (default: Microsoft)") + parser.add_argument('--BRANCH', default='dev', help="GitHub repository branch (default: dev)") + parser.add_argument('--SOURCE', action='store_true', help="get source from a local path (default: False)") + parser.add_argument('--TESTING', action='store_true', help="turns on testing mode (default: False)") + parser.add_argument('--DESTPATH', default=None, help="an alternative destination for the drivers (default: None)") + + args = parser.parse_args() + + phpver = args.PHPVER + arch = args.ARCH + thread = args.THREAD + driver = args.DRIVER + debug = args.DEBUG + repo = args.REPO + branch = args.BRANCH + download = args.SOURCE is False + path = args.DESTPATH + testing = args.TESTING + + if phpver is None: + # starts interactive mode, testing mode is False + # will not prompt for drivers' destination path, which is None by default + while True: + # perform some minimal checks + phpver = input("PHP Version (e.g. 7.1.* or 7.2.*): ") + if 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 + + arch_version = input("64-bit? [y/n]: ") + thread = validate_input("Thread safe? ", "nts/ts") + driver = validate_input("Driver to build? ", "all/sqlsrv/pdo_sqlsrv") + debug_mode = input("Debug enabled? [y/n]: ") + + answer = input("Download source from a GitHub repo? [y/n]: ") + download = False + if answer == 'yes' or answer == 'y' or answer == '': + download = True + repo = input("Name of the repo (hit enter for 'Microsoft'): ") + branch = input("Name of the branch (hit enter for 'dev'): ") + if repo == '': + repo = 'Microsoft' + if branch == '': + branch = 'dev' + + arch_version = arch_version.lower() + arch = 'x64' if arch_version == 'yes' or arch_version == 'y' or arch_version == '' else 'x86' + + debug_mode = debug_mode.lower() + debug = debug_mode == 'yes' or debug_mode == 'y' or debug_mode == '' + + builder = BuildDriver(phpver, + driver, + arch, + thread, + debug, + repo, + branch, + download, + path, + testing) + builder.build() diff --git a/buildscripts/buildtools.py b/buildscripts/buildtools.py new file mode 100644 index 00000000..2176b94e --- /dev/null +++ b/buildscripts/buildtools.py @@ -0,0 +1,476 @@ +#!/usr/bin/python3 +######################################################################################### +# +# Description: The class BuildUtil will build Microsoft SQL Server PHP 7+ Drivers +# for 32 bit and 64 bit. +# +# Requirement: +# python 3.x +# PHP SDK and PHP Source +# Driver source code folder +# Git for Windows +# Visual Studio 2015 (PHP 7.0* and 7.1*) and Visual Studio 2017 (PHP 7.2*) +# +# Output: The drivers will be renamed and copied to the specified location. +# +############################################################################################# + +import shutil +import os.path +import stat +import datetime +import urllib.request +import zipfile +import fileinput + +class BuildUtil(object): + """Build sqlsrv and/or pdo_sqlsrv drivers with PHP source with the following properties: + + Attributes: + phpver # PHP version, e.g. 7.1.*, 7.2.* etc. + driver # all, sqlsrv, or pdo_sqlsrv + arch # x64 or x86 + thread # nts or ts + debug_enabled # whether debug is enabled + """ + + def __init__(self, phpver, driver, arch, thread, debug_enabled = False): + self.phpver = phpver + self.driver = driver.lower() + self.arch = arch.lower() + self.thread = thread.lower() + self.debug_enabled = debug_enabled + + def major_version(self): + """Return the major version number based on the PHP version.""" + return self.phpver[0:3] + + def version_label(self): + """Return the version label based on the PHP version.""" + major_ver = self.major_version() + + if major_ver[2] == '0': + version = major_ver[0] + else: + version = major_ver[0] + major_ver[2] + return version + + def driver_name(self, driver, suffix): + """Return the *driver* name with *suffix* after PHP is successfully compiled.""" + return 'php_' + driver + suffix + + def driver_new_name(self, driver, suffix): + """Return the *driver* name with *suffix* based on PHP version and thread.""" + version = self.version_label() + return 'php_' + driver + '_' + version + '_' + self.thread + suffix + + def compiler_version(self): + """Return the appropriate compiler version based on PHP version.""" + VC = 'vc14' + version = self.version_label() + if version >= '72': # Compiler version for PHP 7.2 or above + VC = 'vc15' + return VC + + def phpsrc_root(self, sdk_dir): + """Return the path to the PHP source folder based on *sdk_dir*.""" + vc = self.compiler_version() + return os.path.join(sdk_dir, 'php-sdk', 'phpdev', vc, self.arch, 'php-'+self.phpver+'-src') + + def build_abs_path(self, sdk_dir): + """Return the absolute path to the PHP build folder based on *sdk_dir*.""" + phpsrc = self.phpsrc_root(sdk_dir) + + build_dir = 'Release' + if self.debug_enabled: + build_dir = 'Debug' + + if self.thread == 'ts': + build_dir = build_dir + '_TS' + + if self.arch == 'x64': + build_dir = self.arch + os.sep + build_dir + + return os.path.join(phpsrc, build_dir) + + def remove_old_builds(self, sdk_dir): + """Remove the extensions, e.g. the driver subfolders in php-7.*-src\ext.""" + print('Removing old builds...') + + phpsrc = self.phpsrc_root(sdk_dir) + ext_path = os.path.join(phpsrc, 'ext') + if os.path.exists( ext_path ): + shutil.rmtree(os.path.join(ext_path, 'sqlsrv'), ignore_errors=True) + shutil.rmtree(os.path.join(ext_path, 'pdo_sqlsrv'), ignore_errors=True) + + if self.arch == 'x64': + shutil.rmtree(os.path.join(phpsrc, self.arch), ignore_errors=True) + else: + shutil.rmtree(os.path.join(phpsrc, 'Debug'), ignore_errors=True) + shutil.rmtree(os.path.join(phpsrc, 'Debug_TS'), ignore_errors=True) + shutil.rmtree(os.path.join(phpsrc, 'Release'), ignore_errors=True) + shutil.rmtree(os.path.join(phpsrc, 'Release_TS'), ignore_errors=True) + + def remove_prev_build(self, sdk_dir): + """Remove all binaries and source code in the Release* or Debug* + folders according to the current configuration + """ + print('Removing previous build...') + build_dir = self.build_abs_path(sdk_dir) + if not os.path.exists(build_dir): + return + + os.chdir(build_dir) + os.system('DEL *sqlsrv*') + + # remove the extensions in the phpsrc's release* or debug* folder's ext subfolder + release_ext_path = os.path.join(build_dir, 'ext') + if os.path.exists( release_ext_path ): + shutil.rmtree(os.path.join(release_ext_path, 'sqlsrv'), ignore_errors=True) + shutil.rmtree(os.path.join(release_ext_path, 'pdo_sqlsrv'), ignore_errors=True) + + # next remove the binaries too + os.chdir(release_ext_path) + os.system('DEL *sqlsrv*') + + @staticmethod + def get_logfile_name(): + """Return the filename for the log file based on timestamp.""" + return 'Build_' + datetime.datetime.now().strftime("%Y%m%d_%H%M") + '.log' + + @staticmethod + def update_file_content(file, search_str, new_str): + """Find *search_str* and replace it by *new_str* in a *file*""" + os.chmod(file, stat.S_IWRITE) + with fileinput.FileInput(file, inplace=True) as f: + for line in f: + print(line.replace(search_str, new_str), end='') + + @staticmethod + def generateMMDD(): + """Return the generated Microsoft PHP Build Version Number""" + d = datetime.date.today() + + startYear = 2009 + startMonth = 4 + passYear = int( '%02d' % d.year ) - startYear + passMonth = int( '%02d' % d.month ) - startMonth + MM = passYear * 12 + passMonth + dd = d.day + + MMDD = "" + str( MM ) + if( dd < 10 ): + return MMDD + "0" + str( dd ) + else: + return MMDD + str( dd ) + + @staticmethod + def get_driver_version(version_file): + """Read the *version_file* and return the driver version.""" + with open(version_file) as f: + for line in f: + if 'SQLVERSION_MAJOR' in line: # major version + major = line.split()[2] + elif 'SQLVERSION_MINOR' in line: # minor version + minor = line.split()[2] + elif 'SQLVERSION_PATCH' in line: # patch + patch = line.split()[2] + break + + return major + '.' + minor + '.' + patch + + @staticmethod + def write_lines_to_copy_source(driver, file): + """Write to file the commands to copy *driver* source.""" + source = '%currDir%' + os.sep + 'Source' + os.sep + driver + dest = '%phpSrc%' + os.sep + 'ext' + os.sep + driver + file.write('@CALL ROBOCOPY ' + source + ' ' + dest + ' /s /xx /xo' + os.linesep) + + source = '%currDir%' + os.sep + 'Source' + os.sep + 'shared' + dest = '%phpSrc%' + os.sep + 'ext' + os.sep + driver + os.sep + 'shared' + file.write('@CALL ROBOCOPY ' + source + ' ' + dest + ' /s /xx /xo' + os.linesep) + + @staticmethod + def download_msphpsql_source(repo, branch, dest_folder = 'Source', clean_up = True): + """Download to *dest_folder* the msphpsql archive of the specified + GitHub *repo* and *branch*. The downloaded files will be removed by default. + """ + try: + work_dir = os.path.dirname(os.path.realpath(__file__)) + + temppath = os.path.join(work_dir, 'temp') + if os.path.exists(temppath): + shutil.rmtree(temppath) + os.makedirs(temppath) + os.chdir(temppath) + + file = branch + '.zip' + url = 'https://github.com/' + repo + '/msphpsql/archive/' + branch + '.zip' + + print('Downloading ' + url + ' ...') + try: + with urllib.request.urlopen(url) as response, open(file, 'wb') as out_file: + shutil.copyfileobj(response, out_file) + except: + print ("Resort to skip ssl verification...") + # need to skip ssl verification on some agents + # see https://www.python.org/dev/peps/pep-0476/ + with urllib.request.urlopen(url, context=ssl._create_unverified_context()) as response, open(file, 'wb') as out_file: + shutil.copyfileobj(response, out_file) + + print('Extracting ' + file + ' ...') + zip = zipfile.ZipFile(file) + zip.extractall() + zip.close() + + msphpsqlFolder = os.path.join(temppath, 'msphpsql-' + branch) + source = os.path.join(msphpsqlFolder, 'source') + os.chdir(work_dir) + + os.system('ROBOCOPY ' + source + '\shared ' + dest_folder + '\shared /xx /xo') + os.system('ROBOCOPY ' + source + '\pdo_sqlsrv ' + dest_folder + '\pdo_sqlsrv /xx /xo') + os.system('ROBOCOPY ' + source + '\sqlsrv ' + dest_folder + '\sqlsrv /xx /xo') + + if clean_up: + shutil.rmtree(temppath) + + except: + print('Error occurred when downloading source') + raise + + def update_driver_source(self, source_dir, driver): + """Update the *driver* source in *source_path* with the + latest version, file descriptions, etc. + If debug is enabled, will remove the optimization flag + """ + driver_dir = os.path.join(source_dir, driver) + + if self.debug_enabled: + # Remove the optimization flag in the config file for this driver + # because '/O2' option is incompatible with Debug mode + print('Removing optimization flag for', driver) + config_file = os.path.join(driver_dir, 'config.w32') + if driver == 'sqlsrv': + self.update_file_content(config_file, 'ADD_FLAG( "CFLAGS_SQLSRV", "/O2" );', '') + elif driver == 'pdo_sqlsrv': + self.update_file_content(config_file, 'ADD_FLAG( "CFLAGS_PDO_SQLSRV", "/O2" );', '') + + # Update Template.rc + template_file = os.path.join(driver_dir, 'template.rc') + if driver == 'sqlsrv': + drivername = self.driver_new_name(driver, '.dll') + self.update_file_content(template_file, 'FILE_NAME \"\\0\"', '"' + drivername + '\\0"') + self.update_file_content(template_file, '\"Microsoft Drivers for PHP for SQL Server\\0\"', '"Microsoft Drivers for PHP for SQL Server (SQLSRV Driver)\\0"') + elif driver == 'pdo_sqlsrv': + drivername = self.driver_new_name(driver, '.dll') + self.update_file_content(template_file, 'FILE_NAME \"\\0\"', '"' + drivername + '\\0"') + self.update_file_content(template_file, '\"Microsoft Drivers for PHP for SQL Server\\0\"', '"Microsoft Drivers for PHP for SQL Server (PDO Driver)\\0"') + + # Update Version.h + version_file = os.path.join(source_dir, 'shared', 'version.h') + build_number = self.generateMMDD() + self.update_file_content(version_file, 'SQLVERSION_BUILD 0', 'SQLVERSION_BUILD ' + build_number) + + # get the latest version + version = self.get_driver_version(version_file) + '.' + build_number + print('Driver version is: ', version) + + # Update CREDIT file + credits_file = os.path.join(driver_dir, 'CREDITS') + if driver == 'sqlsrv': + self.update_file_content(credits_file, 'Microsoft Drivers for PHP for SQL Server', 'Microsoft Drivers ' + version + ' for PHP for SQL Server (' + self.driver.upper() + ' driver)') + elif driver == 'pdo_sqlsrv': + self.update_file_content(credits_file, 'Microsoft Drivers for PHP for SQL Server (PDO driver)', 'Microsoft Drivers ' + version + ' for PHP for SQL Server (' + self.driver.upper() + ' driver)') + + def generate_build_options(self): + """Return the generated build configuration and arguments""" + cmd_line = '' + if self.debug_enabled: + cmd_line = ' --enable-debug ' + + if self.driver == 'all': + cmd_line = ' --enable-sqlsrv=shared --enable-pdo --with-pdo-sqlsrv=shared ' + cmd_line + else: + if self.driver == 'sqlsrv': + cmd_line = ' --enable-sqlsrv=shared ' + cmd_line + else: # pdo_sqlsrv + cmd_line = ' --enable-pdo --with-pdo-sqlsrv=shared ' + cmd_line + + cmd_line = 'cscript configure.js --disable-all --enable-cli --enable-cgi --enable-embed' + cmd_line + if self.thread == 'nts': + cmd_line = cmd_line + ' --disable-zts' + return cmd_line + + def create_local_batch_file(self, make_clean, cmd_line, log_file): + """Generate the batch file to be picked up by the PHP starter script.""" + filename = 'phpsdk-build-task.bat' + print('Generating ', filename) + try: + file = open(filename, 'w') + file.write('@ECHO OFF' + os.linesep) + file.write('SET currDir=%CD%' + os.linesep) + file.write('SET LOG_NAME=%currDir%\\' + log_file + os.linesep) + file.write('@CALL phpsdk_buildtree phpdev > %LOG_NAME% 2>&1' + os.linesep) + + # for PHP version with release tags, such as 'RC', 'beta', etc. + # we need to remove the hyphen '-' between the version number and tag + # because in https://github.com/php/php-src the released tags have no hyphens + + php_tag = 'php-' + self.phpver.replace('-', '') + php_src = 'php-' + self.phpver +'-src' + + # if not exists, check out the specified tag + file.write('IF NOT EXIST ' + php_src + ' @CALL git clone -b ' + php_tag + ' --depth 1 --single-branch https://github.com/php/php-src.git ' + php_src + os.linesep) + file.write('CD ' + php_src + os.linesep) + file.write('SET phpSrc=%CD%' + os.linesep) + file.write('@CALL phpsdk_deps -u >> %LOG_NAME% 2>&1' + os.linesep) + + # copy source files to extension + if self.driver == 'all': + self.write_lines_to_copy_source('sqlsrv', file) + self.write_lines_to_copy_source('pdo_sqlsrv', file) + else: + self.write_lines_to_copy_source(self.driver, file) + + # configure and build + file.write('@CALL buildconf --force >> %LOG_NAME% 2>&1' + os.linesep) + file.write('@CALL ' + cmd_line + ' >> %LOG_NAME% 2>&1' + os.linesep) + if make_clean: + file.write('nmake clean >> %LOG_NAME% 2>&1' + os.linesep) + file.write('nmake >> %LOG_NAME% 2>&1' + os.linesep) + file.write('exit' + os.linesep) + file.close() + return filename + except: + print('Cannot create ', filename) + + def build_drivers(self, make_clean = False, dest = None, log_file = None): + """Build sqlsrv/pdo_sqlsrv extensions for PHP, assuming the Source folder + exists in the working directory, and this folder will be removed when the build + is complete. + """ + work_dir = os.path.dirname(os.path.realpath(__file__)) + + # First, update the driver source file contents + source_dir = os.path.join(work_dir, 'Source') + if self.driver == 'all': + self.update_driver_source(source_dir, 'sqlsrv') + self.update_driver_source(source_dir, 'pdo_sqlsrv') + else: + self.update_driver_source(source_dir, self.driver) + + # Next, generate the build configuration and arguments + cmd_line = self.generate_build_options() + print('cmd_line: ' + cmd_line) + + # Generate a batch file based on the inputs + if log_file is None: + log_file = self.get_logfile_name() + + batch_file = self.create_local_batch_file(make_clean, cmd_line, log_file) + + # Reference: https://github.com/OSTC/php-sdk-binary-tools + # Clone the master branch of PHP sdk if the directory does not exist + print('Downloading the latest php SDK...') + + # if *dest* is None, simply use the current working directory + sdk_dir = dest + copy_to_ext = True # this determines where to copy the binaries to + if dest is None: + sdk_dir = work_dir + copy_to_ext = False + + phpSDK = os.path.join(sdk_dir, 'php-sdk') + if not os.path.exists( phpSDK ): + os.system('git clone https://github.com/OSTC/php-sdk-binary-tools.git --branch master --single-branch --depth 1 ' + phpSDK) + os.chdir(phpSDK) + os.system('git pull ') + + # Move the generated batch file to phpSDK for the php starter script + sdk_batch_file = os.path.join(phpSDK, batch_file) + if os.path.exists(sdk_batch_file): + os.remove(sdk_batch_file) + shutil.move(os.path.join(work_dir, batch_file), phpSDK) + + sdk_source = os.path.join(phpSDK, 'Source') + # Sometimes, for various reasons, the Source folder from previous build + # might exist in phpSDK. If so, remove it first + if os.path.exists(sdk_source): + os.chmod(sdk_source, stat.S_IWRITE) + shutil.rmtree(sdk_source, ignore_errors=True) + shutil.move(source_dir, phpSDK) + + # Invoke phpsdk--.bat + vc = self.compiler_version() + starter_script = 'phpsdk-' + vc + '-' + self.arch + '.bat' + print('Running starter script: ', starter_script) + os.system(starter_script + ' -t ' + batch_file) + + # Now we can safely remove the Source folder, because its contents have + # already been modified prior to building the extensions + shutil.rmtree(os.path.join(phpSDK, 'Source'), ignore_errors=True) + + # Next, rename the newly compiled PHP extensions + self.rename_binaries(sdk_dir) + + # Final step, copy the binaries to the right place + ext_dir = self.copy_binaries(sdk_dir, copy_to_ext) + return ext_dir + + def rename_binary(self, path, driver): + """Rename the *driver* binary (sqlsrv or pdo_sqlsrv) (only the dlls).""" + driver_old_name = self.driver_name(driver, '.dll') + driver_new_name = self.driver_new_name(driver, '.dll') + + os.rename(os.path.join(path, driver_old_name), os.path.join(path, driver_new_name)) + + def rename_binaries(self, sdk_dir): + """Rename the sqlsrv and/or pdo_sqlsrv dlls according to the PHP + version and thread. + """ + + # Derive the path to where the extensions are located + ext_dir = self.build_abs_path(sdk_dir) + print("Renaming binaries in ", ext_dir) + + if self.driver == 'all': + self.rename_binary(ext_dir, 'sqlsrv') + self.rename_binary(ext_dir, 'pdo_sqlsrv') + else: + self.rename_binary(ext_dir, self.driver) + + def copy_binary(self, from_dir, dest_dir, driver, suffix): + """Copy sqlsrv or pdo_sqlsrv binary (based on *suffix*) to *dest_dir*.""" + if suffix == '.dll': + binary = self.driver_new_name(driver, suffix) + else: + binary = self.driver_name(driver, suffix) + shutil.copy2(os.path.join(from_dir, binary), dest_dir) + + def copy_binaries(self, sdk_dir, copy_to_ext): + """Copy the sqlsrv and/or pdo_sqlsrv binaries, including the pdb files, + to the right place, depending on *copy_to_ext*. The default is to + copy them to the 'ext' folder. + """ + build_dir = self.build_abs_path(sdk_dir) + print('Copying the binaries from', build_dir) + if copy_to_ext: + dest_dir = os.path.join(build_dir, 'ext') + else: + # Simply make a copy of the binaries in sdk_dir + dest_dir = sdk_dir + + print('Destination:', dest_dir) + + # Now copy the binaries + if self.driver == 'all': + self.copy_binary(build_dir, dest_dir, 'sqlsrv', '.dll') + self.copy_binary(build_dir, dest_dir, 'sqlsrv', '.pdb') + self.copy_binary(build_dir, dest_dir, 'pdo_sqlsrv', '.dll') + self.copy_binary(build_dir, dest_dir, 'pdo_sqlsrv', '.pdb') + else: + self.copy_binary(build_dir, dest_dir, self.driver, '.dll') + self.copy_binary(build_dir, dest_dir, self.driver, '.pdb') + + return dest_dir +