MSYS2: Add support for building a stand-alone NSIS installer

This changes the existing code for the MSVC installer as little
as possible to allow building the Wireshark .exe Windows installer
using the MinGW-w64 toolchain.

Currently the DLL dependency list is static, this may change in
the future. Ideally we would use CPack and install() logic
to copy the DLLs.

The msys2checkdeps.py script is copied from the Inkscape project[1].
It doesn't have a specific license identifier. The Inkscape project
is licensed under the GPL version 2 or later.

TODO: Download Npcap and USBPcap using CMake instead of requiring
manual action.

[1]https://gitlab.com/inkscape/inkscape

Ping #17771.
This commit is contained in:
João Valverde 2023-05-21 15:44:35 +01:00
parent 83cebf9563
commit aa6b8368b7
10 changed files with 429 additions and 53 deletions

View File

@ -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.

View File

@ -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)

View File

@ -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

View File

@ -39,6 +39,9 @@ That must be done explicitly using ``cmake --install <builddir> --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.

View File

@ -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 $<TARGET_FILE:wireshark>
-FilePath ${_nsis_binary_dir}/wireshark-qt-manifest.nsh
$<$<CONFIG:Debug>:-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 $<TARGET_FILE:wireshark>
-FilePath ${_nsis_binary_dir}/wireshark-qt-manifest.nsh
$<$<CONFIG:Debug>:-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"
$<TARGET_FILE:wireshark>
${_nsis_binary_dir}/wireshark-qt-manifest.nsh
#$<$<CONFIG:Debug>:-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}
$<$<CONFIG:Debug>:/DBUNDLE_DEBUG_DLLS>
$<$<BOOL:${MSVC}>:/DUSE_VCREDIST>
$<$<BOOL:${BUILD_etwdump}>:/DHAVE_ETWDUMP>
$<$<BOOL:${SKIP_QT_TRANSLATIONS}>:/DSKIP_QT_TRANSLATIONS>
wireshark.nsi
WORKING_DIRECTORY ${_nsis_source_dir}
)

View File

@ -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()

View File

@ -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 <gerald@wireshark.org>
#
# Wireshark - Network traffic analyzer
# By Gerald Combs <gerald@wireshark.org>
# 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)

View File

@ -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."

View File

@ -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"

177
tools/msys2checkdeps.py Normal file
View File

@ -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)