GraphViz DLLs on Windows

Which GraphViz DLLs on Windows do I need to include in an installer for an app of mine which uses GraphViz as a dependency? Most of all, I just need the core engines and renderers, but there are many things of late such as cgraph++.dll which I’m sure my app will not be needing.

Many of the gvplugins, I assume, won’t be found by “depends.exe” because they are loaded dynamically? Would it help to look at the “config8” file – is there any documentation for that?

Also, I noticed that some of the Microsoft redistributables are included. If I clobber any of those with versions from MSVC 2022 Community edition, would that break anything? (I know not to redistribute any debug DLLs).

Sorry, this should have been tagged with “Dev”. But I can’t seem to change that?

Done

It’s possible none of the forum owners or regular posters can answer these questions.

Can we recruit an expert, maybe from the graphviz dotnet world, or chocolatey?

In the meantime, I tried to use “depends.exe” which is pretty hopeless on Windows 10 or 11. But it is possible to leverage the output of dumpbin /IMPORTS <dll filename> and construct a directed graph from it. I’m probably going to script this soon, but in the meantime, it has solved the problem (albeit with lots of manual input for now).

Has nobody ever done this before? At least it is pretty fast … I had some issues with DLLs having a dot or a hyphen in the name, but was too much in a hurry to look up how it should be escaped (re: DOT syntax).

Big PS about the MSVC redistributables … it looks like these are also from recent MSVC, but how can I be sure that they are compatible with what MSVC 2022 Community gives me?

Here is the GV file I came up with (the “SBBL” at the end is my exectuable, everything else is a DLL either from GraphViz or from another source needed by my executable). I left out all of the Win64Api dependencies (KERNEL32.DLL, etc.) as far as possible to simplify matters.
dll_dependencies.gv (2.8 KB)

Hi, @Robert_Hairgrove

If my understanding is correct, you are not sure what dlls of graphviz to include as dependencies when you pack your exe file as an installer?

Hi @pavlopylypchuk321 - Well, I could just include them all, but there are so many of them. So I was trying to find just the ones I actually need.

I see. Do you load your dlls implicitly or explicitly?

I don’t think depends.exe can load dlls that you linked explicitly (i.e. using LoadLibrary or similar method in your code) and I think depends.exe can load implicitly linked dlls (using .lib at compilation)

For dynamically loaded dlls (using LoadLibrary or so), I used to load manually when packing.
I used InstallShield to pack the exe as a single installar but was a while ago.

I build my app on Windows and link the import libraries for cgraph.dll, cdt.dll, and gvc.dll. Plugins (dlls that have names starting with “gvplugin_”) are loaded by the app at runtime through the cgraph and gvc API upon request, so I need to include those as well as all of their dependencies (cairo, pango, fontconfig, etc.) It looks like I do need most of them eventually.

The “Dependency Walker” tool doesn’t provide anything but information you can get by using “dumpbin /IMPORTS”, and it hangs when you run it on Windows 10 or later (or any 64-bit system, I believe).

As to the installer, I am using the WiX Toolset, but as far as the DLLs go, there really isn’t much you can do except for doing it manually. At least with WiX 5 you can put the DLLs all in one directory or subdirectory, and just point the WiX tools at it.

It seems that there are DLLs to support “Kitty” in the Windows installer, but Kitty doesn’t appear to run on Windows? I see only MacOS and Linux mentioned on their website.

Another thing i noticed is that some Microsoft redistributable DLLs are packaged “as is”, i.e. without the installer which is normally supplied by Microsoft. I have learned that this is now discouraged, and that they should be installed through the installer so that they can be updated by Windows Updates, etc. You should probably be including the appropriate installer .exe file instead of plain DLLs (information here: Redistributing Visual C++ Files | Microsoft Learn).

The Kitty plugin speaks Kitty’s terminal graphics protocol. It is not Kitty-specific. Having said that, I do not know if there are any terminals on Windows that speak this protocol.

FWIW I believe the dependencies in Graphviz’ CMakeLists.txt files are more or less complete. You could look at all the target_link_libraries lines and figure out what depends on what. The only exception to this would be, as you’ve observed, the plugins.

Graphviz’ CMake build system doesn’t have as many options for disabling components as the Autotools build system does. If your aim is to build a minimal Graphviz, please feel free to file Gitlab issues requesting additional CMake options to disable things.

I did such a script (find_dependencies.py) a couple of years back in order to do exactly what you want to do, i.e., find the dependencies of Graphviz executables and DLLs. I used it to get the dependencies in the CMakeLists.txt right. I hope it still works.

Unfortunately, the forum prohibits me from attaching the file, but here’s the code:

#!/usr/bin/env python3

"""Find Windows DLL dependencies"""

import argparse
import os
import pathlib
import re
import shlex
import subprocess
import sys
from typing import List

dumpbin = "C:/Program Files (x86)/Microsoft Visual Studio/2022/BuildTools/VC/Tools/MSVC/14.42.34433/bin/Hostx64/x64/dumpbin.exe"

#
assert os.path.exists(dumpbin)

def run(args: List[str]):
    p = subprocess.run(
        args,
        stdout=subprocess.PIPE,
        universal_newlines=True,
    )

    if (p.returncode != 0):
        print(p.stdout)
        print(p.stderr, file=sys.stderr)
        sys.exit(p.returncode)

    return p.stdout

def determine_architecture(file):
    if not file.exists():
        print(f"Error: File not found: {file}", file=sys.stderr)
        sys.exit(1)
    dumpbin_output = run([dumpbin, "-headers", file])
    for line in dumpbin_output.split("\n"):
        if "10B magic # (PE32)" in line:
            return "x86"
        if "20B magic # (PE32+)" in line:
            return "x64"
    return None

def get_architecture(files):
    architecture_candidates = list(set([determine_architecture(pathlib.Path(file)) for file in files]))
    if len(architecture_candidates) > 1:
        print(f"Error: Ambiguous architecture: {' or '.join(architecture_candidates)}", file=sys.stderr)
        sys.exit(1)
    architecture = architecture_candidates[0]
    if architecture is None:
        print(f"Error: Could not determine architecture")
        sys.exit(1)
    assert architecture in ["x86", "x64"]

    return architecture

def get_search_directories(files, architecture):
    search_directories = list(set([pathlib.Path(file).parent for file in files]))
    # note that the system directory names are very counterintuitive
    windows_directory = pathlib.Path("C:/Windows")
    windows_system_directory_name = "SysWOW64" if architecture == "x86" else "SYSTEM32"
    windows_system_directory = windows_directory / windows_system_directory_name
    program_files_directory = pathlib.Path("C:/Program Files (x86)" if architecture == "x86" else "C:/Program Files")
    visual_studio_community_directory = program_files_directory / "Microsoft Visual Studio/2022/Community"
    visual_studio_buildtools_directory = program_files_directory / "Microsoft Visual Studio/2022/BuildTools"
    visual_studio_community_ide_directory = visual_studio_community_directory / "Common7/IDE"
    visual_studio_community_ide2_directory = visual_studio_buildtools_directory / "Common7/IDE"
    visual_studio_community_llvm_directory = visual_studio_community_directory / "VC/Tools/Llvm" / architecture / "bin"
    visual_studio_community_llvm2_directory = visual_studio_buildtools_directory / "VC/Tools/Llvm" / "bin"
#     visual_studio_community_clang_directory = visual_studio_buildtools_directory / "VC/Tools/MSVC/14.42.34433/bin/Hostx64/x64"
    visual_studio_community_clang_directory = pathlib.Path("C:/Program Files (x86)/Microsoft Visual Studio/2022/BuildTools/VC/Tools/MSVC/14.42.34433/bin/Hostx86/x64")

    assert os.path.exists(visual_studio_community_clang_directory / "clang_rt.asan_dynamic-x86_64.dll")

    windows_directories = [
        windows_system_directory,
        visual_studio_community_ide_directory,
        visual_studio_community_ide2_directory,
        visual_studio_community_llvm_directory,
        visual_studio_community_llvm2_directory,
        visual_studio_community_clang_directory,
    ]
    search_directories += windows_directories
    search_directories += "C:/Users/magjac/graphviz/build/_CPack_Packages/win64/NSIS/Graphviz-13.0.0~dev.20250106.1542-win64/bin"

    return search_directories

def create_binary(name, path):
    return {
        "name": name,
        "path": path,
        "dependencies": [],
    }

def add_binary_and_dependencies(name, path, binaries, search_directories, skip_found_system_binaries):
    if name not in binaries:
        binaries[name] = create_binary(name, path)
    binary = binaries[name]

    if path is None:
        return binary

    if "C:\\Program Files" in str(path):
        return binary

    if "C:\\Windows" in str(path):
        return binary

    dependencies = binary["dependencies"]

    dumpbin_output = run([dumpbin, "-dependents", path])

    for line in dumpbin_output.split("\n"):
        mo = re.match(r" *([^ ]*\.dll)", line)
        if mo:
            dependency_name = mo.group(1)
            if dependency_name in binaries:
                dependency_binary = binaries[dependency_name]
            else:
                dependency_path = locate_dependency(dependency_name, search_directories)
                if skip_found_system_binaries:
                    if "C:\\Program Files" in str(dependency_path):
                        continue
                    if "C:\\Windows" in str(dependency_path):
                        continue
                dependency_binary = add_binary_and_dependencies(dependency_name, dependency_path, binaries, search_directories, skip_found_system_binaries)
            dependencies.append(dependency_binary)

    return binary

def locate_dependency(name, search_directories):
    for directory in search_directories:
        path = pathlib.Path(directory) / name
        if path.exists():
            return path

    return None

def generate_dot_src(binaries):
    dot_src = ""
    dot_src += "digraph {\n"
    dot_src += "  node [shape=box style=filled colorscheme=set312]\n"
    for binary in binaries.values():
        color = 4 if binary["path"] is None else 7
        dot_src += f'"{binary["name"]}" [color={color}]\n'
        for dependency_binary in binary["dependencies"]:
            dot_src += f'"{binary["name"]}" -> "{dependency_binary["name"]}"\n'
    dot_src += "}\n"

    return dot_src

def print_dependency_hierarchy(binary, missing_only=False, parents=""):
    if parents:
        hierarchy = parents + "->"
    else:
        hierarchy = ""
    hierarchy += binary["name"]
    dependency_binaries = binary["dependencies"]
    path = binary["path"]
    if len(dependency_binaries) == 0:
        if not missing_only or path is None:
            print(hierarchy)
    else:
        for dependency_binary in dependency_binaries:
            print_dependency_hierarchy(dependency_binary, missing_only, hierarchy)

def main(args: List[str]) -> int:  # pylint: disable=C0116

    parser = argparse.ArgumentParser(
        description="Find Windows DLL dependencies"
    )
    parser.add_argument("files", nargs="+", help="Binaries or DLLs to search for dependencies")
    parser.add_argument(
        "--generate-dot", "-g", action="store_true", help="Generate Graphviz DOT source"
    )
    parser.add_argument(
        "--print-dependency-hierarchy", "-d", action="store_true", help="Print dependency hierarchy"
    )
    parser.add_argument(
        "--missing-only", "-m", action="store_true", help="Print missing binaries only"
    )
    parser.add_argument(
        "--skip-found-system-binaries", "-s", action="store_true", help="Don't print found system binaries"
    )
    options = parser.parse_args(args[1:])

    architecture = get_architecture(options.files)

    search_directories = get_search_directories(options.files, architecture)

    binaries = {}

    for path in options.files:
        name = pathlib.Path(path).name
        binaries[name] = create_binary(name, path)

    for path in options.files:
        name = pathlib.Path(path).name
        add_binary_and_dependencies(name, path, binaries, search_directories, options.skip_found_system_binaries)

    if options.generate_dot:
        dot_src = generate_dot_src(binaries)
        print(dot_src)
        return 0

    if options.print_dependency_hierarchy:
        for path in options.files:
            name = pathlib.Path(path).name
            binary = binaries[name]
            print_dependency_hierarchy(binary, options.missing_only)
        return 0

    for name, binary in binaries.items():
        path = binary["path"]
        if not options.missing_only or path is None:
            print(f'{name:40}{binary["path"]}')

    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv))

Awesome replies … thank you, @smattr and @magjac (Matt and Magnus)! :grinning_face: This will help very much.