router-for-me/CLIProxyAPI · error
zip contains multiple target dynamic libraries
Error message
zip contains multiple target dynamic libraries
What it means
Thrown by readTargetLibrary in internal/pluginstore/install.go when a plugin release archive contains more than one entry whose cleaned name equals the expected root-level library name (e.g. myplugin.so or myplugin-v1.2.3.so). The installer needs exactly one target library per archive so it can deterministically pick the artifact to install; a second match makes the choice ambiguous, so the install aborts before anything is written.
Source
Thrown at internal/pluginstore/install.go:343
return nil, 0, errClean
}
if file.FileInfo().IsDir() {
continue
}
if !regularZipFile(file) {
return nil, 0, fmt.Errorf("zip entry %s is not a regular file", file.Name)
}
if !hasDynamicLibraryExtension(cleanedName) {
continue
}
if cleanedName != targetName && cleanedName != versionedTargetName {
if path.Base(cleanedName) == targetName || path.Base(cleanedName) == versionedTargetName {
return nil, 0, fmt.Errorf("target dynamic library must be at zip root")
}
return nil, 0, fmt.Errorf("dynamic library filename must be %s or %s", targetName, versionedTargetName)
}
if target != nil {
return nil, 0, fmt.Errorf("zip contains multiple target dynamic libraries")
}
target = file
}
if target == nil {
return nil, 0, fmt.Errorf("zip does not contain %s", targetName)
}
handle, errOpen := target.Open()
if errOpen != nil {
return nil, 0, fmt.Errorf("open %s: %w", targetName, errOpen)
}
defer func() {
if errClose := handle.Close(); errClose != nil {
log.WithError(errClose).Debug("failed to close plugin archive entry")
}
}()
data, errRead := io.ReadAll(handle)
if errRead != nil {View on GitHub (pinned to 78f0c4079e)
Solutions
- Rebuild the release archive so it contains exactly one library entry at the zip root, named either <id><ext> or <id>-v<version><ext>
- Delete the duplicate entry from the zip (unzip, remove the extra library, re-zip) and retry the install
- If you do not control the archive, fetch a release whose asset zip is produced by the standard plugin release template instead of a manual zip
Example fix
# before: zip contains # myplugin.so # myplugin-v1.2.3.so zip -d plugin.zip 'myplugin-v1.2.3.so' # after: zip contains only # myplugin.so
Defensive patterns
Strategy: validation
Validate before calling
// before InstallArchive, ensure exactly one root-level target library
func countTargetLibs(archiveData []byte, id, version, goos string) (int, error) {
r, err := zip.NewReader(bytes.NewReader(archiveData), int64(len(archiveData)))
if err != nil { return 0, err }
target := id + pluginExtension(goos)
versioned := id + "-v" + version + pluginExtension(goos)
n := 0
for _, f := range r.File {
name := path.Clean(f.Name)
if name == target || name == versioned { n++ }
}
return n, nil
}
// n != 1 -> reject the archive before calling InstallArchive Try / catch
if _, err := store.InstallArchive(data, plugin, opts); err != nil {
if strings.Contains(err.Error(), "multiple target dynamic libraries") {
// packaging defect: fail the release, do not retry
}
} Prevention
- Generate release zips from a single build step that writes exactly one library named <id>-v<version><ext> at the root
- Add an archive-lint step to CI: unzip -l and assert one .so/.dylib/.dll entry
- Never append to an existing zip during releases
When it happens
Trigger: Calling Client.Install, Client.InstallVersion, Client.InstallDirect, or InstallArchive with an archive zip that lists both 'myplugin.so' and 'myplugin-v1.2.3.so' at the root, or the same filename in two directories that both clean to the root name (e.g. './myplugin.so' alongside 'myplugin.so'). The loop at install.go:322-346 sets target on the first match and errors on the second.
Common situations: A release workflow that zips the whole build directory and accidentally includes both the unversioned build output and the version-renamed copy; CI jobs that append previously-built artifacts to an existing zip; a repackaged third-party zip that duplicated the binary in a subfolder that path.Clean collapses to the root name.
Related errors
- zip does not contain %s
- plugin_update_requires_restart
- target dynamic library must be at zip root
- dynamic library filename must be %s or %s
- open %s: %w
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/e224994ca0e54603.
Report an issue: GitHub.