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 towardsmy-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:
- The directory of the running executable
- The working directory
- System directories
- 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
.exewrapper
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:
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.