TL;DR Branching strategy and a locked build pool only get you so far. The build still needs the exact libraries your project pins, on every agent, every time. Copying
.libraryfiles from a shared drive does not survive a second machine, a second TwinCAT version, or a second release line. Beckhoff’s TcPkg is the tool that sets the machine up: TwinCAT workloads and official Beckhoff libraries on the box. Twinpack is the project-scoped package manager: dependencies in.Zeugwerk/config.json, with version and branch both part of the reference, so “latest onrelease/1.x” is a real answer. In a pipeline you usually do not call Twinpack by hand:zkmake build --update-snapshotsrestores through Twinpack, then compiles and tests.
This is the fifth post in our CI/CD for TwinCAT series. The earlier ones cover build tooling options, distributing CLIs to nodes, branching for machine software, and exclusive locking on a Windows build farm. This one picks up the row that keeps showing up in those posts and never quite gets its own treatment: dependency resolution.
The build that cannot find its libraries
A TwinCAT project does not compile from source alone. It needs every referenced library installed on the build machine, at the version the project expects, for the TwinCAT runtime that agent is running. Miss one of those and the failure is often not a clean “package not found.” It is a missing type, a wrong signature, or a library that silently resolved to whatever happened to be installed on that particular box.
On a single developer PC this stays invisible for a long time. Libraries accumulate on the machine. The project “just builds.” The moment CI arrives, the invisibility ends. A fresh agent has none of those libraries. Someone copies a folder from a network share onto the node, labels it done, and for a while that works too, until the second agent, the second TwinCAT version, or the hotfix on release/1.x that still needs last year’s library line.
The failure modes are boring and expensive:
- Agent A has
AcmeConveyor 1.4.0.12. Agent B still has1.3.0.8. Same commit, different results. - A library was updated on the share and never pinned. Friday’s green build is Monday’s interface mismatch on a machine in the field.
release/1.xandrelease/2.xboth build on the same pool. A global “latest” install on the agent cannot serve both honestly.- The engineer who knew which ZIP belonged to which project leaves. The share remains. The knowledge does not.
None of this is a TwinCAT quirk unique to you. It is what every ecosystem looks like before it gets a package manager. Python had it before pip. JavaScript had it before npm. TwinCAT still has it in a lot of shops.
What “restore” has to mean for TwinCAT
In web CI, npm ci or pip install -r requirements.txt is a solved gesture. For TwinCAT the gesture has three parts that general-purpose tooling does not know about:
- Resolve which package versions the project actually wants, including transitive library dependencies.
- Download those packages from somewhere authoritative into a known local cache.
- Install them into the TwinCAT library repository on that machine so the compiler can see them.
Step 3 is the awkward one. TwinCAT’s library repository is not a folder you can casually mutate from any process. Headless tooling can download and stage files; installing into the live TwinCAT installation usually goes through Beckhoff’s Automation Interface or RepTool.exe. A CI-friendly package manager has to be honest about that split rather than pretend a file copy is enough.
It also has to understand something the branching post already argued: TwinCAT’s own reference syntax only offers an exact four-part version or *. There is no native way to say “latest within release/1.x.” Once two major lines exist in the field, that gap is not academic.
Beckhoff already ships a package manager
Since TwinCAT 3.1.4026, Beckhoff has TcPkg: the TwinCAT Package Manager. Read it as the tool that sets the machine up. Install TwinCAT as workloads. Pull official Beckhoff PLC libraries onto the box (tcpkg install TwinCAT.XAE.PLC.Lib.…). Provision a developer PC or a CI agent once so TwinCAT itself is there. Twinpack can even use Beckhoff’s public feed as one of its sources, so those libraries and your own show up in the same catalog.
That is a different job from project-scoped restore. TcPkg does not live next to your PLC project the way package.json or requirements.txt does. It does not declare, per repository, which of your libraries this machine line needs on release/1.x, and it does not re-run that declaration on every agent before every compile.
You can stretch it that way: publish custom library packages into a TcPkg feed. Those packages are NuGet archives with a Chocolatey-shaped layout. Beckhoff’s own packaging templates put the .library next to tools/chocolateyinstall.ps1. The library is not installed by being present in the archive. It is installed by that PowerShell script, baked into the nupkg at pack time, which has to find RepTool.exe on the target machine and call it. The published examples do that with path wildcards against a fixed install tree, roughly:
$RepToolLocations = @(
Join-Path "C:\Program Files (x86)\Beckhoff\TwinCAT\3.1\Components\Plc\Build_4026.*\" `
"Common\RepTool.exe" -Resolve
)
That works on the day you pack the nupkg, on a machine that looks like the one you tested. Years later, or on a CI image with a different TwinCAT layout, a different drive, or a Build folder the wildcard no longer matches, the script fails or does nothing. The .library inside the package is still fine. The package as a whole is useless, because the install logic aged out of the environment. Every custom library you publish that way versions not only the binary, but a frozen assumption about where TwinCAT lives on every machine that will ever install it.
TcPkg also does not solve the release-line problem from the branching post. Machine setup can install a named Beckhoff package. It does not give your product libraries a first-class branch: release/1.x with version: null meaning latest on that line, restored from a file in Git.
So the split is simple: TcPkg for the agent. Twinpack for the project.
Twinpack’s answer: declare once, restore every build
Twinpack is our open-source, project-scoped package manager for TwinCAT libraries. The IDE side is the catalog under the References node. The CI side is the same dependency file that catalog writes.
Dependencies live in .Zeugwerk/config.json in the repository. Each package entry carries at least a name, a version, and a branch:
{
"name": "MyConveyorLib",
"version": null,
"branch": "release/1.x"
}
version: null means latest, but latest on that branch. The Twinpack server filters candidates by branch first, then picks the highest version among them. A machine project on release/1.x keeps receiving v1 maintenance builds and never gets handed 2.0.0. An exact four-part version still works when you need a bit-for-bit pin.
That is the model. The pipeline still has to run it on every agent, every time, before compile. Twinpack itself exposes that as twinpack restore --headed: resolve from the config, download the .library into .Zeugwerk/libraries, install into TwinCAT through the Automation Interface on that machine at restore time. The install path is not a PowerShell script shipped inside last year’s package. Fresh checkout, empty agent, same libraries as the developer who last touched the project.
Headed vs headless. By default twinpack restore runs headless and only manipulates files. That is enough to download and stage packages; it is not enough to install them into the live TwinCAT library repository on the agent. For that you need --headed (Automation Interface) or a separate Beckhoff RepTool step after download. In practice we use headed. The Automation Interface is already on the build machine because TwinCAT is already on the build machine, and headed restore is the path that matches how compile itself works.
Sources are configurable. The same restore works against:
- the Twinpack server (public open-source packages; enterprise customers get private hosting and feature branches),
- your own NuGet server for on-premises libraries that must not leave the network,
- the Beckhoff Library Repository on TwinCAT 3.1.4026+.
So the pattern is not “upload everything to Zeugwerk.” It is “one declared dependency set, restored from whichever sources your security policy allows.”
What you actually put in the pipeline
You could wire twinpack restore --headed as its own step and then call your compile tool. That is honest and it works. In the setups we run and ship, restore is not a separate line in the pipeline. It is the first thing zkmake does when you build with snapshot updates:
build:
tags: [windows]
script:
- scoop update zkmake zkdoc
- zkmake build --update-snapshots --kill-all
--update-snapshots is the switch that tells zkmake to restore packages through Twinpack (download, then install via the Automation Interface) before it compiles and runs tests. The pipeline author sees one command. Underneath, Twinpack still owns resolution: same config.json, same branch-aware “latest”, same cache under .Zeugwerk/libraries.
That split is deliberate, and it matches what this series is for. Twinpack is the open piece that makes dependency identity portable across developers, agents, and release lines. zkmake is the build CLI that turns that identity into a green or red pipeline without asking every team to assemble restore, install, compile, and test by hand. The Scoop post already showed this job shape for tool distribution; this post is why the restore half of that command exists.
A few details that still matter either way:
- Commit the config, not the binaries.
.Zeugwerk/config.jsonbelongs in Git. The downloaded cache under.Zeugwerk/librariesusually does not. - Pin when you mean pin. Exact versions for released machine lines;
nullplus an explicitbranchfor “track this release line.” - Do not leave TwinCAT
*references as your source of truth. Twinpack can restore what the config says. It cannot rescue a project that still asks TwinCAT for “whatever is newest on this agent.” - Restore on every build, not once when the agent is provisioned. Agents get wiped, patched, and replaced. The config is the durable record.
Why this only works once the rest of the series is in place
Restore is not a substitute for the earlier decisions. It is what makes them operational.
Without branching, branch: "release/1.x" in the config has nowhere honest to point. The branching post is where that line becomes real.
Without versioned tooling on the node, you are back to “whichever Twinpack or zkmake binary someone copied onto the agent.” Scoop is how those CLIs stay pinned and authenticated across the farm.
Without exclusive locking, two restores and two compiles on the same Windows machine will eventually step on each other’s TwinCAT library repository. The locking post is why that machine is yours for the duration of the build.
Put differently: the layer table from the locking post is not a menu. Dependency resolution is the row that fails last and hurts most, because everything above it can look green while the wrong library quietly wins.
Where this leaves you (for now)
With restore in place, a TwinCAT CI pipeline has the pieces that decide whether a green build is honest: a branching model that matches machines in the field, agents that do not tramp on each other, tools that arrive at a known version, and libraries that resolve the same way on every node. What it does not yet decide is when that green build becomes a package other people may take. That is the publish topology: build, staging, deploy. Separate post, same series.
Twinpack is free and open source. The product page and the docs cover IDE setup, sources, and the full CLI. zkmake and the rest of DevTools sit on top when you want restore and build as one pipeline step. If you want either wired into a real farm rather than a laptop demo, that is the kind of thing we set up with teams.
