diff --git a/CMakeLists.txt b/CMakeLists.txt index a3248d4203..ecdc71c896 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -178,10 +178,10 @@ if(WIN32) and CPU target ${WIRESHARK_TARGET_PROCESSOR_ARCHITECTURE}" ) + find_package(PowerShell REQUIRED) + # Determine where the 3rd party libraries will be if(USE_REPOSITORY) - find_package(PowerShell REQUIRED) - if( DEFINED ENV{WIRESHARK_LIB_DIR} ) # The buildbots set WIRESHARK_LIB_DIR but not WIRESHARK_BASE_DIR. file( TO_CMAKE_PATH "$ENV{WIRESHARK_LIB_DIR}" _PROJECT_LIB_DIR ) @@ -213,6 +213,8 @@ and CPU target ${WIRESHARK_TARGET_PROCESSOR_ARCHITECTURE}" set(EXTRA_INSTALLER_DIR ${_ws_lib_dir}) # XXX Add a dependency on ${_ws_lib_dir}/current_tag.txt? + else() + set(EXTRA_INSTALLER_DIR ${CMAKE_BINARY_DIR}/packaging/nsis) endif() endif(WIN32) @@ -1397,8 +1399,13 @@ if (QT_FOUND) ) # Use qmake to find windeployqt and macdeployqt. Ideally one of # the modules in ${QTDIR}/lib/cmake would do this for us. - if(WIN32 AND NOT USE_MSYSTEM) - find_program(QT_WINDEPLOYQT_EXECUTABLE windeployqt + if(WIN32) + if (USE_qt6 AND USE_MSYSTEM) + set(_windeployqt_name "windeployqt-qt6") + else() + set(_windeployqt_name "windeployqt") + endif() + find_program(QT_WINDEPLOYQT_EXECUTABLE ${_windeployqt_name} HINTS "${QT_BIN_PATH}" DOC "Path to the windeployqt utility." ) @@ -2263,7 +2270,7 @@ endif() # List of extra dependencies for the "copy_data_files" target set(copy_data_files_depends) -if(WIN32 AND NOT USE_MSYSTEM) +if(WIN32) foreach(_install_as_txt_file COPYING NEWS README.md README.windows) # On Windows, install some files with a .txt extension so that they're # double-clickable. @@ -2720,6 +2727,13 @@ if(BUILD_wireshark AND QT_FOUND) set_target_properties(copy_qt_dlls PROPERTIES FOLDER "Copy Tasks") # Will we ever need to use --debug? Windeployqt seems to # be smart enough to copy debug DLLs when needed. + if (USE_MSYSTEM AND Qt${qtver}Widgets_VERSION VERSION_EQUAL 6.5.0) + # windeployqt released with Qt 6.5.0 is broken. + # https://bugreports.qt.io/browse/QTBUG-112204 + message(WARNING "Qt Deploy Tool 6.5.0 is broken, skipping translations.") + list(APPEND QT_WINDEPLOYQT_EXTRA_ARGS --no-translations) + set(SKIP_QT_TRANSLATIONS True) + endif() add_custom_command(TARGET copy_qt_dlls POST_BUILD COMMAND set "PATH=${QT_BIN_PATH};%PATH%" @@ -3263,7 +3277,7 @@ if(BUILD_dcerpcidl2wrs) install(TARGETS idl2wrs RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) endif() -if(MSVC) +if(WIN32) find_package( MSVC_REDIST ) # Must come after executable targets are defined. diff --git a/CMakeOptions.txt b/CMakeOptions.txt index 5dc26a8a8c..67f8e7233f 100644 --- a/CMakeOptions.txt +++ b/CMakeOptions.txt @@ -104,7 +104,7 @@ option(ENABLE_NGHTTP2 "Build with HTTP/2 header decompression support" ON) option(ENABLE_LUA "Build with Lua dissector support" ON) option(ENABLE_SMI "Build with libsmi snmp support" ON) option(ENABLE_GNUTLS "Build with RSA decryption support" ON) -if(WIN32) +if(WIN32 AND NOT USE_MSYSTEM) option(ENABLE_WINSPARKLE "Enable automatic updates using WinSparkle" ON) endif() if (NOT WIN32) diff --git a/README.msys2 b/README.msys2 index a62b67e705..aa5aca5ed6 100644 --- a/README.msys2 +++ b/README.msys2 @@ -36,6 +36,23 @@ How to build Wireshark from source: The application should be launched using the same shell. +How to build an NSIS stand-alone binary installer: + +1. Follow the instructions above to compile Wireshark from source. + +2. Build the Wireshark User Guide. + + $ ninja user_guide_html + +3. Download Npcap and USBpcap and copy them to ${CMAKE_BINARY_DIR}/packaging/nsis. + +4. Build the installer + + $ ninja wireshark_nsis_prep + $ ninja wireshark_nsis + +If successful the installer can be found in ${CMAKE_BINARY_DIR}/packaging/nsis. + Alternatively you can also use the PKGBUILD included in the Wireshark source distribution to compile Wireshark into a binary package that can be installed using pacman[3]. @@ -53,9 +70,6 @@ the build using MSVC: * AirPcap is not available. AirPcap is EOL and currently there is no plan to add support for it with MinGW-w64 builds. -* TODO: Add a stand-alone distributable binary installer that can be - deployed outside the MSYS2 environment. - References: [1]https://packages.msys2.org/base/mingw-w64-wireshark diff --git a/docbook/release-notes.adoc b/docbook/release-notes.adoc index 454d6112e9..6be2af7205 100644 --- a/docbook/release-notes.adoc +++ b/docbook/release-notes.adoc @@ -39,6 +39,9 @@ That must be done explicitly using ``cmake --install --component Deve The Wireshark installation is relocatable on Linux (and other ELF platforms with support for relative RPATHs). +Support for building an NSIS Windows installer using the MinGW-w64 toolchain +and https://www.msys2.org/[MSYS2]. Read README.msys2 in the distribution for more information. + Many other improvements have been made. See the “New and Updated Features” section below for more details. diff --git a/packaging/nsis/CMakeLists.txt b/packaging/nsis/CMakeLists.txt index f5084293da..8a227ee7a4 100644 --- a/packaging/nsis/CMakeLists.txt +++ b/packaging/nsis/CMakeLists.txt @@ -18,13 +18,13 @@ set(WIRESHARK_NSIS_GENERATED_FILES set(WIRESHARK_NSIS_GENERATED_FILES ${WIRESHARK_NSIS_GENERATED_FILES} PARENT_SCOPE) set(WIRESHARK_NSIS_FILES - wireshark.nsi - uninstall-wireshark.nsi - wireshark-common.nsh - GetWindowsVersion.nsh - servicelib.nsh - NpcapPage.ini - USBPcapPage.ini + ${CMAKE_CURRENT_SOURCE_DIR}/wireshark.nsi + ${CMAKE_CURRENT_SOURCE_DIR}/uninstall-wireshark.nsi + ${CMAKE_CURRENT_SOURCE_DIR}/wireshark-common.nsh + ${CMAKE_CURRENT_SOURCE_DIR}/GetWindowsVersion.nsh + ${CMAKE_CURRENT_SOURCE_DIR}/servicelib.nsh + ${CMAKE_CURRENT_SOURCE_DIR}/NpcapPage.ini + ${CMAKE_CURRENT_SOURCE_DIR}/USBPcapPage.ini ${WIRESHARK_NSIS_GENERATED_FILES} PARENT_SCOPE ) @@ -161,26 +161,34 @@ if (BUILD_wireshark) # wireshark-manifest.nsh. Can be created at configure time. set(_all_manifest "${CMAKE_CURRENT_BINARY_DIR}/wireshark-manifest.nsh") set(_all_manifest_contents "# Files required for all sections. Generated by CMake.\n") - set(_all_manifest_contents "${_all_manifest_contents}!ifdef BUNDLE_DEBUG_DLLS\n") - foreach(_dll ${GLIB2_DLLS_DEBUG}) - set(_all_manifest_contents "${_all_manifest_contents}File \"\${STAGING_DIR}\\${_dll}\"\n") - endforeach() - set(_all_manifest_contents "${_all_manifest_contents}!else\n") - foreach(_dll ${GLIB2_DLLS_RELEASE}) - set(_all_manifest_contents "${_all_manifest_contents}File \"\${STAGING_DIR}\\${_dll}\"\n") - endforeach() - set(_all_manifest_contents "${_all_manifest_contents}!endif\n") - foreach(_dll ${CARES_DLL} ${PCRE2_DLL} ${GCRYPT_DLLS} - ${GNUTLS_DLLS} ${KERBEROS_DLLS} ${LIBSSH_DLLS} ${LUA_DLL} - ${LZ4_DLL} ${NGHTTP2_DLL} ${SBC_DLL} ${SMI_DLL} ${SNAPPY_DLL} - ${SPANDSP_DLL} ${BCG729_DLL} ${LIBXML2_DLLS} ${WINSPARKLE_DLL} - ${ZLIB_DLL} ${BROTLI_DLLS} ${ZSTD_DLL} ${ILBC_DLL} ${OPUS_DLL} - ${SPEEXDSP_DLL} - # Needed for mmdbresolve - ${MAXMINDDB_DLL} - ) - set(_all_manifest_contents "${_all_manifest_contents}File \"\${STAGING_DIR}\\${_dll}\"\n") - endforeach() + if(USE_REPOSITORY) + set(_all_manifest_contents "${_all_manifest_contents}!ifdef BUNDLE_DEBUG_DLLS\n") + foreach(_dll ${GLIB2_DLLS_DEBUG}) + set(_all_manifest_contents "${_all_manifest_contents}File \"\${STAGING_DIR}\\${_dll}\"\n") + endforeach() + set(_all_manifest_contents "${_all_manifest_contents}!else\n") + foreach(_dll ${GLIB2_DLLS_RELEASE}) + set(_all_manifest_contents "${_all_manifest_contents}File \"\${STAGING_DIR}\\${_dll}\"\n") + endforeach() + set(_all_manifest_contents "${_all_manifest_contents}!endif\n") + foreach(_dll ${CARES_DLL} ${PCRE2_DLL} ${GCRYPT_DLLS} + ${GNUTLS_DLLS} ${KERBEROS_DLLS} ${LIBSSH_DLLS} ${LUA_DLL} + ${LZ4_DLL} ${NGHTTP2_DLL} ${SBC_DLL} ${SMI_DLL} ${SNAPPY_DLL} + ${SPANDSP_DLL} ${BCG729_DLL} ${LIBXML2_DLLS} ${WINSPARKLE_DLL} + ${ZLIB_DLL} ${BROTLI_DLLS} ${ZSTD_DLL} ${ILBC_DLL} ${OPUS_DLL} + ${SPEEXDSP_DLL} + # Needed for mmdbresolve + ${MAXMINDDB_DLL} + ) + set(_all_manifest_contents "${_all_manifest_contents}File \"\${STAGING_DIR}\\${_dll}\"\n") + endforeach() + else() + include(${CMAKE_CURRENT_SOURCE_DIR}/InstallMSYS2.cmake) + foreach(_dll ${MINGW_DLLS}) + file(TO_NATIVE_PATH ${_dll} _path) + set(_all_manifest_contents "${_all_manifest_contents}File \"${_path}\"\n") + endforeach() + endif() foreach(_script "init.lua" "console.lua" "dtd_gen.lua") set(_all_manifest_contents "${_all_manifest_contents}File \"\${STAGING_DIR}\\${_script}\"\n") endforeach() @@ -291,14 +299,24 @@ macro( ADD_NSIS_PACKAGE_TARGETS ) #set (_nsis_package ${CMAKE_BINARY_DIR}/packaging/nsis/Wireshark-$(WIRESHARK_TARGET_PLATFORM)-$(VERSION).exe) # wireshark-qt-manifest.nsh. Created using Wireshark.exe. - add_custom_command(OUTPUT ${_nsis_binary_dir}/wireshark-qt-manifest.nsh - COMMAND set "PATH=${QT_BIN_PATH};%PATH%" - COMMAND ${POWERSHELL_COMMAND} "${_nsis_source_dir}/windeployqt-to-nsis.ps1" - -Executable $ - -FilePath ${_nsis_binary_dir}/wireshark-qt-manifest.nsh - $<$:-DebugConfig> - DEPENDS "${_nsis_source_dir}/windeployqt-to-nsis.ps1" - ) + if (USE_REPOSITORY) + add_custom_command(OUTPUT ${_nsis_binary_dir}/wireshark-qt-manifest.nsh + COMMAND set "PATH=${QT_BIN_PATH};%PATH%" + COMMAND ${POWERSHELL_COMMAND} "${_nsis_source_dir}/windeployqt-to-nsis.ps1" + -Executable $ + -FilePath ${_nsis_binary_dir}/wireshark-qt-manifest.nsh + $<$:-DebugConfig> + DEPENDS "${_nsis_source_dir}/windeployqt-to-nsis.ps1" + ) + else() + add_custom_command(OUTPUT ${_nsis_binary_dir}/wireshark-qt-manifest.nsh + COMMAND ${PYTHON_EXECUTABLE} "${_nsis_source_dir}/windeployqt-to-nsis.py" + $ + ${_nsis_binary_dir}/wireshark-qt-manifest.nsh + #$<$:-DebugConfig> + DEPENDS "${_nsis_source_dir}/windeployqt-to-nsis.py" + ) + endif() # Build NSIS package dependencies. We build the package in # two stages so that wireshark_nsis below doesn't trigger @@ -322,6 +340,9 @@ macro( ADD_NSIS_PACKAGE_TARGETS ) add_custom_target(wireshark_nsis COMMAND ${MAKENSIS_EXECUTABLE} ${NSIS_OPTIONS} $<$:/DBUNDLE_DEBUG_DLLS> + $<$:/DUSE_VCREDIST> + $<$:/DHAVE_ETWDUMP> + $<$:/DSKIP_QT_TRANSLATIONS> wireshark.nsi WORKING_DIRECTORY ${_nsis_source_dir} ) diff --git a/packaging/nsis/InstallMSYS2.cmake b/packaging/nsis/InstallMSYS2.cmake new file mode 100644 index 0000000000..d97a8d4dc9 --- /dev/null +++ b/packaging/nsis/InstallMSYS2.cmake @@ -0,0 +1,71 @@ +set(MINGW_BIN $ENV{MINGW_PREFIX}/bin) + +if(USE_MSYSTEM) + # mingw-w64 dlls + # (use msys2checkdeps.py to list required libraries / check for missing or unused libraries) + file(GLOB MINGW_DLLS + ${MINGW_BIN}/libLerc.dll + ${MINGW_BIN}/libb2-1.dll + ${MINGW_BIN}/libbrotlicommon.dll + ${MINGW_BIN}/libbrotlidec.dll + ${MINGW_BIN}/libbrotlienc.dll + ${MINGW_BIN}/libbz2-1.dll + ${MINGW_BIN}/libbcg729.dll + ${MINGW_BIN}/libcares-2.dll + ${MINGW_BIN}/libcrypto-3-x64.dll + ${MINGW_BIN}/libdeflate.dll + ${MINGW_BIN}/libdouble-conversion.dll + ${MINGW_BIN}/libexpat-1.dll + ${MINGW_BIN}/libffi-8.dll + ${MINGW_BIN}/libfreetype-6.dll + ${MINGW_BIN}/libgcc_s_seh-1.dll + ${MINGW_BIN}/libgcrypt-20.dll + ${MINGW_BIN}/libglib-2.0-0.dll + ${MINGW_BIN}/libgmodule-2.0-0.dll + ${MINGW_BIN}/libgmp-10.dll + ${MINGW_BIN}/libgnutls-30.dll + ${MINGW_BIN}/libgpg-error-0.dll + ${MINGW_BIN}/libgraphite2.dll + ${MINGW_BIN}/libharfbuzz-0.dll + ${MINGW_BIN}/libhogweed-6.dll + ${MINGW_BIN}/libiconv-2.dll + ${MINGW_BIN}/libicudt72.dll + ${MINGW_BIN}/libicuin72.dll + ${MINGW_BIN}/libicuuc72.dll + ${MINGW_BIN}/libilbc.dll + ${MINGW_BIN}/libidn2-0.dll + ${MINGW_BIN}/libintl-8.dll + ${MINGW_BIN}/libjbig-0.dll + ${MINGW_BIN}/libjpeg-8.dll + ${MINGW_BIN}/liblz4.dll + ${MINGW_BIN}/liblzma-5.dll + ${MINGW_BIN}/libmd4c.dll + ${MINGW_BIN}/libmaxminddb.dll + ${MINGW_BIN}/libminizip-1.dll + ${MINGW_BIN}/libnettle-8.dll + ${MINGW_BIN}/libnghttp2-14.dll + ${MINGW_BIN}/libopus-0.dll + ${MINGW_BIN}/libp11-kit-0.dll + ${MINGW_BIN}/libpcre2-16-0.dll + ${MINGW_BIN}/libpcre2-8-0.dll + ${MINGW_BIN}/libpng16-16.dll + ${MINGW_BIN}/libsbc-1.dll + ${MINGW_BIN}/libsharpyuv-0.dll + ${MINGW_BIN}/libsmi-2.dll + ${MINGW_BIN}/libsnappy.dll + ${MINGW_BIN}/libspandsp-2.dll + ${MINGW_BIN}/libspeexdsp-1.dll + ${MINGW_BIN}/libssh.dll + ${MINGW_BIN}/libstdc++-6.dll + ${MINGW_BIN}/libtasn1-6.dll + ${MINGW_BIN}/libtiff-6.dll + ${MINGW_BIN}/libunistring-2.dll + ${MINGW_BIN}/libwebp-7.dll + ${MINGW_BIN}/libunistring-5.dll + ${MINGW_BIN}/libwinpthread-1.dll + ${MINGW_BIN}/libxml2-2.dll + ${MINGW_BIN}/libzstd.dll + ${MINGW_BIN}/lua51.dll + ${MINGW_BIN}/zlib1.dll + ) +endif() diff --git a/packaging/nsis/windeployqt-to-nsis.py b/packaging/nsis/windeployqt-to-nsis.py new file mode 100644 index 0000000000..1c5d9dcd02 --- /dev/null +++ b/packaging/nsis/windeployqt-to-nsis.py @@ -0,0 +1,66 @@ +#!/bin/env python3 + +# windeployqt-to-nsh +# +# Windeployqt-to-nsh - Convert the output of windeployqt to an equivalent set of +# NSIS "File" function calls. +# +# Rewritten in python from windeployqt-to-nsis.ps1, that has the following copyright: +# +# Copyright 2014 Gerald Combs +# +# Wireshark - Network traffic analyzer +# By Gerald Combs +# Copyright 1998 Gerald Combs +# +# SPDX-License-Identifier: GPL-2.0-or-later + +import sys +import os +import subprocess + +EXECUTABLE = sys.argv[1] +OUTFILE = sys.argv[2] + +# Qt version +qmake_out = subprocess.run("qmake6 -query QT_VERSION", shell=True, check=True, capture_output=True, encoding="utf-8") +qt_version = qmake_out.stdout.strip() + +# XXX The powershell script asserts that the Qt version is greater than 5.3. We already require Qt6 to build the +# installer using MSYS2 (currently not enforced). + +# Windeploy output +windeploy_command = [ + "windeployqt6.exe", + "--no-compiler-runtime", + "--no-translations", + "--list", "mapping", + EXECUTABLE +] + +out = subprocess.run(windeploy_command, shell=True, check=True, capture_output=True, encoding="utf-8") + +with open(OUTFILE, 'w') as f: + command_name = os.path.split(sys.argv[0])[1] + header = """\ +# +# Automatically generated by {} +# +# Qt version {} +#""".format(command_name, qt_version) + + print(header, file=f) + + current_dir = "" + for line in out.stdout.splitlines(): + path, relative = line.split(" ") + rel_path = os.path.split(relative) + if len(rel_path) > 1: + base_dir = rel_path[0].strip('"') + if base_dir != current_dir: + set_out_path = 'SetOutPath "$INSTDIR\{}"'.format(base_dir) + print(set_out_path, file=f) + current_dir = base_dir + file_path = 'File {}'.format(path) + print(file_path, file=f) + diff --git a/packaging/nsis/wireshark.nsi b/packaging/nsis/wireshark.nsi index 872e4c9aad..9ad4d72459 100644 --- a/packaging/nsis/wireshark.nsi +++ b/packaging/nsis/wireshark.nsi @@ -521,6 +521,7 @@ File "${STAGING_DIR}\dumpcap.html" File "${STAGING_DIR}\extcap.html" File "${STAGING_DIR}\ipmap.html" +!ifdef USE_VCREDIST ; C-runtime redistributable ; vc_redist.x64.exe or vc_redist.x86.exe - copy and execute the redistributable installer File "${VCREDIST_DIR}\${VCREDIST_EXE}" @@ -553,6 +554,7 @@ ${Switch} $0 ${EndSwitch} Delete "$INSTDIR\${VCREDIST_EXE}" +!endif ; global config files - don't overwrite if already existing @@ -973,13 +975,15 @@ WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\App Pa WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\App Paths\${PROGRAM_NAME_PATH}" "Path" '$INSTDIR' !include wireshark-qt-manifest.nsh -${!defineifexist} TRANSLATIONS_FOLDER "${QT_DIR}\translations" -SetOutPath $INSTDIR -!ifdef TRANSLATIONS_FOLDER - ; Starting from Qt 5.5, *.qm files are put in a translations subfolder - File /r "${QT_DIR}\translations" -!else - File "${QT_DIR}\*.qm" +!ifndef SKIP_QT_TRANSLATIONS + ${!defineifexist} TRANSLATIONS_FOLDER "${QT_DIR}\translations" + SetOutPath $INSTDIR + !ifdef TRANSLATIONS_FOLDER + ; Starting from Qt 5.5, *.qm files are put in a translations subfolder + File /r "${QT_DIR}\translations" + !else + File "${QT_DIR}\*.qm" + !endif !endif ; Is the Start Menu check box checked? @@ -1162,11 +1166,13 @@ Section /o "Androiddump" SecAndroiddump SectionEnd !insertmacro CheckExtrasFlag "androiddump" +!ifdef HAVE_ETWDUMP Section "Etwdump" SecEtwdump ;------------------------------------------- !insertmacro InstallExtcap "Etwdump" SectionEnd !insertmacro CheckExtrasFlag "Etwdump" +!endif Section /o "Randpktdump" SecRandpktdump ;------------------------------------------- @@ -1253,7 +1259,9 @@ SectionEnd !insertmacro MUI_DESCRIPTION_TEXT ${SecExtcapGroup} "External Capture Interfaces" !insertmacro MUI_DESCRIPTION_TEXT ${SecAndroiddump} "Provide capture interfaces from Android devices." + !ifdef HAVE_ETWDUMP !insertmacro MUI_DESCRIPTION_TEXT ${SecEtwdump} "Provide an interface to read Event Tracing for Windows (ETW) event trace (ETL)." + !endif !insertmacro MUI_DESCRIPTION_TEXT ${SecRandpktdump} "Provide an interface to the random packet generator. (see also randpkt)" !insertmacro MUI_DESCRIPTION_TEXT ${SecSshdump} "Provide remote capture through SSH. (tcpdump, Cisco EPC, wifi)" !insertmacro MUI_DESCRIPTION_TEXT ${SecUDPdump} "Provide capture interface to receive UDP packets streamed from network devices." diff --git a/tools/msys2-setup.sh b/tools/msys2-setup.sh index 6056be5051..e658db58ce 100644 --- a/tools/msys2-setup.sh +++ b/tools/msys2-setup.sh @@ -82,6 +82,7 @@ BASIC_LIST="base-devel \ ${PACKAGE_PREFIX}-qt6-base \ ${PACKAGE_PREFIX}-qt6-multimedia \ ${PACKAGE_PREFIX}-qt6-tools \ + ${PACKAGE_PREFIX}-qt6-translations \ ${PACKAGE_PREFIX}-qt6-5compat \ ${PACKAGE_PREFIX}-sbc \ ${PACKAGE_PREFIX}-snappy \ @@ -97,7 +98,8 @@ ADDITIONAL_LIST="${PACKAGE_PREFIX}-asciidoctor \ ${PACKAGE_PREFIX}-docbook-xsl \ ${PACKAGE_PREFIX}-doxygen \ ${PACKAGE_PREFIX}-libxslt \ - ${PACKAGE_PREFIX}-perl" + ${PACKAGE_PREFIX}-perl \ + ${PACKAGE_PREFIX}-ntldd" TESTDEPS_LIST="${PACKAGE_PREFIX}-python-pytest \ ${PACKAGE_PREFIX}-python-pytest-xdist" diff --git a/tools/msys2checkdeps.py b/tools/msys2checkdeps.py new file mode 100644 index 0000000000..f46eb503b4 --- /dev/null +++ b/tools/msys2checkdeps.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python +# ------------------------------------------------------------------------------------------------------------------ +# list or check dependencies for binary distributions based on MSYS2 (requires the package mingw-w64-ntldd) +# +# run './msys2checkdeps.py --help' for usage information +# ------------------------------------------------------------------------------------------------------------------ +# +# SPDX-License-Identifier: GPL-2.0-or-later +# + +from __future__ import print_function + + +import argparse +import os +import subprocess +import sys + + +SYSTEMROOT = os.environ['SYSTEMROOT'] + + +class Dependency: + def __init__(self): + self.location = None + self.dependents = set() + + +def warning(msg): + print("Warning: " + msg, file=sys.stderr) + + +def error(msg): + print("Error: " + msg, file=sys.stderr) + exit(1) + + +def call_ntldd(filename): + try: + output = subprocess.check_output(['ntldd', '-R', filename], stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: + error("'ntldd' failed with '" + str(e) + "'") + except WindowsError as e: + error("Calling 'ntldd' failed with '" + str(e) + "' (have you installed 'mingw-w64-ntldd-git'?)") + except Exception as e: + error("Calling 'ntldd' failed with '" + str(e) + "'") + return output.decode('utf-8') + + +def get_dependencies(filename, deps): + raw_list = call_ntldd(filename) + + skip_indent = float('Inf') + parents = {} + parents[0] = os.path.basename(filename) + for line in raw_list.splitlines(): + line = line[1:] + indent = len(line) - len(line.lstrip()) + if indent > skip_indent: + continue + else: + skip_indent = float('Inf') + + # if the dependency is not found in the working directory ntldd tries to find it on the search path + # which is indicated by the string '=>' followed by the determined location or 'not found' + if ('=>' in line): + (lib, location) = line.lstrip().split(' => ') + if location == 'not found': + location = None + else: + location = location.rsplit('(', 1)[0].strip() + else: + lib = line.rsplit('(', 1)[0].strip() + location = os.getcwd() + + parents[indent+1] = lib + + # we don't care about Microsoft libraries and their dependencies + if location and SYSTEMROOT in location: + skip_indent = indent + continue + + if lib not in deps: + deps[lib] = Dependency() + deps[lib].location = location + deps[lib].dependents.add(parents[indent]) + return deps + + +def collect_dependencies(path): + # collect dependencies + # - each key in 'deps' will be the filename of a dependency + # - the corresponding value is an instance of class Dependency (containing full path and dependents) + deps = {} + if os.path.isfile(path): + deps = get_dependencies(path, deps) + elif os.path.isdir(path): + extensions = ['.exe', '.pyd', '.dll'] + exclusions = ['distutils/command/wininst'] # python + for base, dirs, files in os.walk(path): + for f in files: + filepath = os.path.join(base, f) + (_, ext) = os.path.splitext(f) + if (ext.lower() not in extensions) or any(exclusion in filepath for exclusion in exclusions): + continue + deps = get_dependencies(filepath, deps) + return deps + + +if __name__ == '__main__': + modes = ['list', 'list-compact', 'check', 'check-missing', 'check-unused'] + + # parse arguments from command line + parser = argparse.ArgumentParser(description="List or check dependencies for binary distributions based on MSYS2.\n" + "(requires the package 'mingw-w64-ntldd')", + formatter_class=argparse.RawTextHelpFormatter) + parser.add_argument('mode', metavar="MODE", choices=modes, + help="One of the following:\n" + " list - list dependencies in human-readable form\n" + " with full path and list of dependents\n" + " list-compact - list dependencies in compact form (as a plain list of filenames)\n" + " check - check for missing or unused dependencies (see below for details)\n" + " check-missing - check if all required dependencies are present in PATH\n" + " exits with error code 2 if missing dependencies are found\n" + " and prints the list to stderr\n" + " check-unused - check if any of the libraries in the root of PATH are unused\n" + " and prints the list to stderr") + parser.add_argument('path', metavar='PATH', + help="full or relative path to a single file or a directory to work on\n" + "(directories will be checked recursively)") + parser.add_argument('-w', '--working-directory', metavar="DIR", + help="Use custom working directory (instead of 'dirname PATH')") + args = parser.parse_args() + + # check if path exists + args.path = os.path.abspath(args.path) + if not os.path.exists(args.path): + error("Can't find file/folder '" + args.path + "'") + + # get root and set it as working directory (unless one is explicitly specified) + if args.working_directory: + root = os.path.abspath(args.working_directory) + elif os.path.isdir(args.path): + root = args.path + elif os.path.isfile(args.path): + root = os.path.dirname(args.path) + os.chdir(root) + + # get dependencies for path recursively + deps = collect_dependencies(args.path) + + # print output / prepare exit code + exit_code = 0 + for dep in sorted(deps): + location = deps[dep].location + dependents = deps[dep].dependents + + if args.mode == 'list': + if (location is None): + location = '---MISSING---' + print(dep + " - " + location + " (" + ", ".join(dependents) + ")") + elif args.mode == 'list-compact': + print(dep) + elif args.mode in ['check', 'check-missing']: + if ((location is None) or (root not in os.path.abspath(location))): + warning("Missing dependency " + dep + " (" + ", ".join(dependents) + ")") + exit_code = 2 + + # check for unused libraries + if args.mode in ['check', 'check-unused']: + installed_libs = [file for file in os.listdir(root) if file.endswith(".dll")] + deps_lower = [dep.lower() for dep in deps] + top_level_libs = [lib for lib in installed_libs if lib.lower() not in deps_lower] + for top_level_lib in top_level_libs: + warning("Unused dependency " + top_level_lib) + + exit(exit_code)