Latex Package Development: Guidelines For Maintaining Backwards Compatibility
Maintaining Backwards Compatibility in LaTeX Packages
When developing a LaTeX package, maintaining backwards compatibility with existing user documents is crucial. Any changes that break the usage or behavior that users rely on can cause documents to stop compiling or produce different output. This results in a bad user experience and should be avoided. There are several key reasons why breaking backwards compatibility can be problematic:
- Users expect consistency and reliability. If an update suddenly breaks their workflow, it will frustrate users and damage trust in the package.
- Fixing breakages creates extra work for users. They will have to diagnose issues and then fix or workaround problems in many documents.
- Some users cannot update easily. For example, publishers may have thousands of documents relying on certain behavior. It is very hard for them to update across the board.
Here are two examples of backwards-incompatible changes that caused significant issues:
- Removing or renaming an internal command that some users relied on in their custom templates. This broke all documents using the command.
- Changing the default sorting order of some elements. While the new order was an improvement, existing documents now output differently.
In both cases, user outcry was swift and the changes had to be reverted post-haste. This guides the first principle: avoid breaking existing usage at (almost) all costs.
Guidelines for Package Authors
As a package developer, here are some key guidelines to follow so changes do not break compatibility:
Clearly Documenting Public Interfaces
The first step is understanding and explicitly documenting what constitutes the public interfaces and behavior of your package. This includes:
- All user-facing commands, environments, options etc.
- The inputs, outputs and side effects of public functions.
- Expected behavior, output and formatting from a user perspective.
With detailed documentation, both developers and users can understand the boundaries between public and private package functionality. This public contract sets user expectations.
Semantic Versioning of Releases
Use semantic version numbers like 1.2.3 to label releases. Incrementing the major version indicates breaking changes, incrementing minor version indicates new features that are backwards compatible, and incrementing the patch version is for compatible bug fixes.
This signals to users the likelihood of breakage so they can test changes appropriately before updating live documents.
Deprecating Before Removing Functionality
Sometimes certain functionality does need to change or be removed to improve the package. In that case, first denote it as deprecated and keep it functioning. Only remove it in a future major release. This gives users time to transition.
Clearly document when the removal will occur and what users need to differently. Offer any helper functions needed to smooth the transition process.
Handling Optional Parameters
New optional parameters are very common when extending packages. There are two approaches to ensure these do not break backwards compatibility:
Default Values for New Parameters
When introducing a new optional parameter, provide a default value so existing usage will continue functioning without changes. Power users can then opt into the new functionality by setting the parameter explicitly.
Conditional Code Paths
Check if a new parameter is set and branch execution between old vs new code paths. If the parameter is unset, execute legacy logic for backwards compatibility.
\ifsetnewparam % New param set, execute updated logic \else % Execute legacy logic for compatibility \fi
This isolates new behavior while retaining old behavior for compatibility.
Managing Internal Changes
Sometimes changes are needed to internal functions, variables, or structure of the package code itself. These can often break seemingly unrelated user functionality that implicitly relied on internal details. Here are ways to avoid internal breakage:
Using Internal Functions Rather than Global Variables
Avoid exposing configurable state via global variables that users may access. Encapsulate within internal functions instead to avoid users depending on specifics of implementation.
Abstracting Away Low-Level Details
Hide structural changes like data layouts or class hierarchies under abstract interfaces so users are isolated from the underlying shifts.
For example, switch from storing data in TeX macros to Lua tables, but expose same logical getter functions.
Advertising Changes to Users
Communication about changes helps avoid surprise breakages. Ensure visibility through:
Release Notes List All Updates
Carefully detail every change, update, addition and removal in the changelog. This allows users to assess impact.
Call special attention to any deprecations scheduled for future removal.
Encouraging User Reports on Compatibility Issues
Ask users to file issues if they encounter any backwards compatibility problems. Consider creating an explicit compatibility testing guide listing key usage examples for users to try out on updates.
Any reported breakage can then be quickly fixed or added back via the major version / deprecation process outlined earlier.
An Example LaTeX Package
To illustrate recommended practices, consider an example LaTeX package called examplepkg:
\ProvidesPackage{examplepkg} \newcommand{\examplecmd}[1]{Example: #1} \newcounter{examplecount} \setcounter{examplecount}{0} \newenvironment{exampleenv} {\stepcounter{examplecount}} {\theexamplecount\ examples shown}
It provides a basic \examplecmd command, an examplecount counter tracking usage, and an exampleenv environment to encapsulate content.
Here is how the author can implement changes while maintaining compatibility per guidelines:
Deprecating \examplecmd
\examplecmd\ is meant for internal use so should not be public API. To remove:
- In v2.0.0 release notes, denote \examplecmd deprecated.
- Add \deprecexamplecmd with warning to use \newexamplecmd instead.
- In v3.0.0, remove \examplecmd completely.
Adding Option to Reset Counter
To allow resetting examplecount mid-document:
- In v2.1.0, introduce boolean \resetexamplecount option with false default.
- Wrap counter logic in check if \resetexamplecount true.
\newif\ifresetexamplecount \resetexamplecountfalse % Set default value \ifresetexamplecount \setcounter{examplecount}{0} % Reset logic \fi
This extends functionality in a compatible way.
Testing for Regressions
Whenever changes are made, thorough testing helps catch any accidental backwards breakages early. Two testing approaches to consider:
Continuous Integration Systems
Setup automated build pipelines that compile the test suite and sample user documents after every commit. Reject any changes that introduce errors or warnings.
User Compatibility Testing
Maintain a corpus of real life user documents using diverse functionality. Use virtual machines or containers with old package versions to simulate production environments.
Continuously upgrade the packages and confirm existing documents still compile properly without changes. Fix any regressions immediately.
Balancing Innovation with Stability
The responsibility of package developers is two-fold:
- Drive innovation and introduce new functionality for users.
- Maintain compatibility so users can safely upgrade and rely on stability.
Change is necessary to improve, but moving too aggressively risks breakages that erode user trust and force rollbacks. Balance ambition with discipline by carefully assessing the impact of changes before releasing them into production environments.
While limiting, maintaining backwards compatibility ultimately enables much faster and sustainable progress. The effort to upgrade and adapt remains reasonable for users of each release.
Keep the upgrade burden low, communicate clearly, test rigorously for regressions, and most importantly - respect user productivity above all else!