Migrating from Legacy Systems

Why LabVIEW Version Control Is So Painful

Learn why Git can store a LabVIEW VI but not diff, merge or blame it, what NI's tooling fixes and where it stops, and what a text station changes.

JJulien Buteau
beginner7 min readSeptember 23, 2026

LabVIEW version control is painful because a VI is a binary file. Git stores it, versions it and reverts it, but it can't show you a line diff, can't merge two people's edits to the same VI, and can't tell you who changed a limit. NI's tooling narrows that gap, one licensed Professional seat at a time.

What Git Gives a Text File and Not a VI

Git was built for text. Every feature you expect from source control on a Python file follows from Git being able to read lines, and a .vi gives it none to read.

Capabilityphases/power_rails.pyPower Rails.vi
Store and revert a versionYesYes
Line diff (git diff)YesNo, Git prints "Binary files differ"
Blame (git blame) on one limitYesNo
Three-way merge of two editsYes, automatic when the edits don't overlapNo, Git stops with a conflict
Review in a pull request on the webYesNo, the reviewer downloads both versions and opens them
Works on any machineYesOnly with the same LabVIEW version installed
Licence needed to reviewNoneA LabVIEW seat, Professional for LVCompare

The right column isn't a Git setting you can change. Git can't merge what it can't parse, and a block diagram is a graph serialised in a format only LabVIEW reads. The same holds for .ctl, .lvclass and .lvlib contents, and for a TestStand .seq (binary by default, and the XML save option diffs a little better but still isn't something you'd merge by hand).

The Four Sources of Pain

Most "LabVIEW and Git" problems reduce to four mechanisms. Each has its own guide in this cluster. This section is the map.

Binary VIs

A commit message is the only readable record of what changed in a VI. If the message says "fixed limits", that's all anyone will ever know about that commit, including you in six months. LVCompare and LVMerge show the diagram diff, on a machine that has them installed and licensed.

One Person per VI at a Time

Because Git can't merge two edits to the same VI, two engineers who touch the same file on two branches produce a conflict Git can't resolve on its own. The usual answer is a convention: you say which VI you're in, and nobody else opens it until you push. It holds on a team of two. Fix LabVIEW merge conflicts on binary VIs covers what to do when it slips.

Compiled Code and Mass Compile

By default a VI file holds its source and its compiled code together. Recompile a caller because a callee's connector pane changed, and the caller's file changes on disk even though you edited nothing in it. Separating compiled code from the source file (a setting since LabVIEW 2010) moves the compiled part to a local object cache and removes most of that noise.

It doesn't remove all of it. A mass compile, which you run after any LabVIEW version or bitness change, rewrites every VI it touches, so the whole repository shows as modified in one commit and the history on either side of it no longer diffs cleanly. Separate compiled code in LabVIEW, explained covers the setting and its limits.

A Run-Time Engine per Station

A built executable runs only on the LabVIEW Run-Time Engine of the same version and bitness. A station on RTE 2019 32-bit won't start a build from LabVIEW 2024 64-bit until someone installs that RTE, and the DAQmx and VISA drivers on that station have their own supported version ranges. So "which version of the test runs on station 3" has two answers: the commit the build came from, and the RTE and drivers that build needs. Which LabVIEW version runs on each station is the inventory.

What NI Gives You

NI knows all this and has built real tooling around it. It's worth stating fairly, conditions included.

ToolWhat it doesCondition
LVCompareGraphical diff of two VIs, front panel and block diagram, node by nodeProfessional Development System, every dependency resolvable on that machine
LVMergeThree-way merge (Base, Theirs, Yours) into a merged VISame as LVCompare, plus both branches must load
Separate compiled codeKeeps compiled code out of the source file, so callers don't show as modified when a callee changesPer-VI or per-project setting, still not a diff, and a mass compile still touches files
Project Explorer source controlCheck out and commit from inside LabVIEW through a configured providerDepends on the provider, and the diff still needs LVCompare

The pattern is the same each time: the tool works, and it needs the same licensed LabVIEW installed on the machine where it runs. Reviews happen on a web page, and no NI tool runs there. NI's own page on software configuration management and LabVIEW is the honest summary of what they recommend, and the doc on separating compiled code from VIs covers the setting.

LabVIEW is a capable tool with a shrinking pool of people who read it. The versioning pain is specific to binary files, and it doesn't go away with a better convention. It goes away with text.

What a Text Station Looks Like

A TofuPilot Framework station is a folder: procedure.yaml, phases/*.py, plugs/*.py. All of it is text. Here's a limit change on a 3.3 V rail, exactly as Git shows it in a pull request, with no plugin and no licence, on any machine.

git-diff.txt
21 lines
# git diff of one limit change, as any reviewer sees itdiff --git a/procedure.yaml b/procedure.yamlindex 3f2a1c7..8b9e0d4 100644--- a/procedure.yaml+++ b/procedure.yaml@@ -2,7 +2,7 @@ name: Controller PCBA FCT-version: 2.4.0+version: 2.4.1  unit:   serial_number:     default_value: SN-000000@@ -24,7 +24,7 @@ main:         unit: V         validators:           - operator: ">="-            expected_value: 3.2+            expected_value: 3.15           - operator: "<="             expected_value: 3.4

Two lines changed, and the reviewer can see the old value, the new value and the author without opening anything. git blame procedure.yaml names who set 3.15 and when. Two engineers editing different phases on two branches merge automatically.

The phase behind that limit is a plain function. The limit isn't in it, which is the point: the code reads the instrument, and the procedure holds the number.

phases/power_rails.py
# The 3.3 V rail phase. The limit lives in procedure.yaml, not here.def power_rails(measurements, dmm):    measurements.rail_3v3 = dmm.read_voltage()

The same property carries through to the floor. A Git push builds an immutable deployment with an id like dep_abc123, stations fetch it with tofupilot pull, and every run uploaded from that station carries the deployment_id. In TofuPilot, "which units ran 2.4.0 and which ran 2.4.1" is a filter on the run list, and rolling every station back to the previous deployment is one action with no rebuild. Trace a test change to the units it shipped walks through it, and how to version control hardware tests with Git and TofuPilot covers the repository layout.

Start With One Station

Pick the station only one person can open, or the one with the highest volume. Rebuild it as a procedure.yaml with its phases and plugs in Python with the TofuPilot Framework, keep the same limits, and run it side by side with the LabVIEW version on the same units for two weeks. The first pull request on that station is the first reviewable limit change your plant has had.

install-and-run.sh
# Install the CLI, run the station locally (no account needed), or run with upload.curl -fsSL https://www.tofupilot.app/install | shtofupilot run ./procedure.yamltofupilot run ./procedure.yaml --upload

The step-by-step rebuild is in how to migrate from LabVIEW to Python for manufacturing tests with TofuPilot, and the framework is at tofupilot.com/products/framework. The Lab tier is free, and tofupilot run works without an account.

More Guides

Put this guide into practice