Skip to content

Winget Portable Package Problem

The Situation

I created a portable Winget package which does install perfectly where the executable apps in the package depend on a DLL in their executable directory.

The Winget portable manifest version 1.12.0 creates one and allows more when needed symbolic links to the apps in a directory that is within the PATH environment variable.

What is needed is that when the package is installed, the apps need to be called from the command line out of the box.

The Problem

The problem is that since when the executable is called from the symbolic link, Windows does not find the required DLL's in the executable directory.

Winget Constraints

The symbolic links in the manifest are only allowed to be created to executables.

Note: When created to something else like a script my-app.bat, Winget creates a symbolic link to the script but towards my-app.bat.exe (tested with only manifest version 1.6.0).

Currently, I'm using manifest version 1.12.0, since it is the latest production-ready schema version.

The Solution

How can this be solved?...

Solve the problem in the document and answer it in the document if possible in "The Solution" paragraph?


AI Offered Solutions

Microsoft's Copilot

Windows resolves DLLs using the DLL search order, and symbolic links do not preserve the original executable directory as the working directory. When a symlink inside a PATH directory is executed, Windows uses the directory of the symlink, not the directory of the real executable, causing dependent DLLs to be missed.

Since Winget portable packages cannot create symlinks to scripts and cannot modify PATH beyond placing symlinks, the reliable solution is:

✔ Create a tiny launcher executable per app

Instead of linking directly to the real executable, generate a small launcher EXE that:

  • Sets its working directory to the real executable directory
  • Calls the actual program

This guarantees Windows loads DLLs from the correct directory.

Example launcher logic (C/C++ or Rust)

#include <windows.h>
#include <process.h>

int main() {
    // Path to the real executable inside the portable package
    const char* realExe = "C:/Users/<user>/AppData/Local/Programs/MyApp/myapp.exe";

    // Set working directory to the executable directory
    SetCurrentDirectoryA("C:/Users/<user>/AppData/Local/Programs/MyApp/");

    // Launch the real executable
    return _spawnl(_P_WAIT, realExe, realExe, NULL);
}

✔ Winget portable manifest then creates symlinks to these launchers

These launchers are executables, so they comply with Winget's constraints.

✔ DLLs load correctly

Because the launcher sets the working directory before launching the real program, Windows resolves DLLs from the correct directory.

Why this works

Windows DLL resolution rules:

  1. The directory of the running executable
  2. The working directory
  3. System directories
  4. PATH

A symlink breaks rule 1, but a launcher restores rule 2.

Alternative (less ideal) options

  • Modify the real executable to use SetDllDirectory()
  • Bundle DLLs next to the launcher (duplicate copies)
  • Use a wrapper PowerShell script and ship a compiled .exe wrapper

But the launcher EXE is the cleanest and most portable solution.

Open AI's - ChatGPT

The solution is to avoid the WinGet symbolic links and add the package's installation directory itself to PATH.

For a portable archive, manifest schema 1.12.0 provides the ArchiveBinariesDependOnPath property. Set it to true in the Installer section:

Installer:
  ...
  ArchiveBinariesDependOnPath: true

With this setting, WinGet adds the directory containing the portable executables directly to PATH, instead of relying on the WinGet\Links symbolic-link directory. The executable is therefore started from its real installation directory, allowing Windows to resolve DLLs located beside that executable.

For example:

Installers:
  - Architecture: x64
    InstallerType: zip
    InstallerUrl: https://example.com/my-app.zip
    InstallerSha256: <hash>
    NestedInstallerType: portable
    NestedInstallerFiles:
      - RelativeFilePath: my-app.exe
        PortableCommandAlias: my-app
    ArchiveBinariesDependOnPath: true

This is the key distinction: PortableCommandAlias provides the command alias/symbolic-link mechanism, whereas ArchiveBinariesDependOnPath: true puts the archive's installation directory itself on PATH. The latter avoids the DLL lookup problem caused by invoking the executable through a link in another directory.

If the archive contains multiple portable executables, they can likewise be invoked directly from the command line while retaining their real installation directory.

A new terminal may be required after installation, because WinGet does not update the PATH of an already-running shell during the installation workflow.

Anthropic's - Claude

This is a known, confirmed limitation of Winget's symlink-based portable mechanism, not a configuration mistake. When Windows executes a program through the reparse point symlink in WinGet\Links, the DLL search order's "directory of the executable" resolves to the symlink's location, not the real install directory — so DLLs sitting next to the actual .exe are never found. Winget-cli's own maintainers acknowledged this directly in the issue "Symlinked executables have missing dependencies," noting that since not all executables are statically built, symlinked executables often have missing dependencies on DLLs, so dynamically linked executables won't work properly when exposed through a symlink.

Winget addressed this with a dedicated manifest field, ArchiveBinariesDependOnPath, introduced in manifest schema 1.9.0 and still valid in 1.12.0. Per the official schema documentation, this field is an optional indication to add the install location directly to PATH, and only applies to an archive containing portable packages. Internally, when this flag is set, Winget skips symlink creation entirely and instead adds the real install directory directly to the user's PATH environment variable — so the executable runs from its own directory and correctly finds its neighboring DLLs, with no symlink involved at all.

Manifest change

Add ArchiveBinariesDependOnPath: true to your installer manifest, e.g.:

InstallerType: zip
NestedInstallerType: portable
NestedInstallerFiles:
  - RelativeFilePath: platform-tools\your-app.exe
    PortableCommandAlias: your-app
ArchiveBinariesDependOnPath: true

This is the exact fix used by real-world packages that hit this same problem — for example, Google's Android Platform Tools (adb.exe + fastboot.exe, which share DLLs) were migrated to this field specifically to resolve missing-DLL failures, with maintainers requiring contributors to update to ManifestVersion: 1.9.0 and set ArchiveBinariesDependOnPath: true to support zipped binaries with dependent DLLs.

Trade-off to be aware of

Since no symlink is created, PortableCommandAlias can no longer rename the command — because there's nothing to rename, only a raw directory added to PATH. The command available on the command line will be whatever the .exe is literally named inside your package (as seen with adb/fastboot above, where the alias matches the real filename exactly). If you need the public command name to differ from the actual executable filename, you'll need to rename the .exe itself at packaging time rather than relying on the alias.