How software is built
Larger software systems and products are commonly assembled from many software components. Each component is a collection of programs. Components frequently re-use other components.
Each component usually corresponds to one or more packages. A package is typically an archive that contains the programs and information about the component – its name, what it does, who wrote it, and possibly the license.
A dependent component or “dependency” may be required by this component. In formal terms, a package is the set of software programs and files that are distributed or used for a given component.
A program may require one or more other programs to run (the “dependencies”). The dependencies can be recursive with potentially very deep relationships. Sometimes the dependency is only for testing or “building/compiling” the program – i.e. a development or “dev” dependency. Sometimes it is needed to run the program; other times it can be optional to provide extra features.
Some dependencies for a program may be rather generic and implied. At a high level, many programs have a dependency on the operating system, the application server, or the database server. This type of dependency is often not stated explicitly.
For example, a Java program requires a Java Virtual Machine (JVM) to run and the fact that a program is written in Java implies this. This dependency is not explicitly stated by most Java packages because most Java programs will not depend on a specific JVM version.
Dependency management is the approach practiced by software programmers to specify, provision, install, update and generally manage the set of dependent programs that their product or application relies on.
Managing dependencies is evolving with different tools available for different languages and frameworks. There are several tools and public package repositories that help programmers deal with a large volume of dependencies. Many of these dependencies are for free, libre and open source-licensed (FLOSS) components and their packages.
Each operating system / language / framework tends to have its own (eco) system for FLOSS components:
- Documenting dependencies in one or more configuration or “meta” files
- Ruby – GEMs from RubyGems,
- Python – Wheels from Pypi,
- Java – JARs from Maven,
- Linux packages such as RPM and Debian,
- and many more.
Dependencies are often weakly defined and variable: A dependency statement may include a range of possible versions for a dependent package and may state if a dependency is “required” or “optional.” Package systems compute (aka “resolve”) a list of concrete dependencies when you provision packages using these definitions.
The dependency relationship between two programs may be more or less “intimate”:
- At arm’s length, where the programs are entirely separated and may only communicate by writing or reading files, or by network interaction
- At a handshake level – where the programs call each other’s subroutines (aka “functions”) directly and exchange data directly
- Merged or combined into a single larger (executable) program
Program dependencies: programs can be compiled and combined
Programs are often compiled: the text of the programs source code is transformed in a binary code that the machine can understand and execute, i.e. C, C++, Java, Go.
But not always: some programming languages are “interpreted” from the source code when you run them and do not require pre-compilation (or are compiled on the fly when you run them).
With the compilation process, several programs can be combined into larger (binary) programs. Combining programs together this way is often called “static linking” when programs are “merged” into a larger binary program.
Free, libre and open source licensing and dependencies
Many software licenses have different requirements for source code and for binaries (BSD, MIT). Copyleft-licensed programs (using licenses such as the GPL or the LGPL) may require source code redistribution for other programs that use them depending on how programs are combined and depend on each other.
Limited Copyleft and Copyleft licenses impose some constraints on how other (proprietary or not) programs can be combined or interact with Copyleft-licensed programs. For instance, the LGPL explicitly discusses static vs. dynamic linking. These are concepts originally defined for compiled languages such as C and C++.
These concepts do not always translate well to other languages. Technical and legal interpretation is usually required to translate these C/C++ oriented concepts to other languages.
Permissive licenses typically impose few constraints on how you can combine programs. For Copyleft licenses, the interactions and dependencies between proprietary- or non-Copyleft- licensed) programs and Copyleft-licensed programs determine when and how the source code redistribution conditions of the GPL or LGPL are triggered.
Remember that the relationship between programs and their dependencies can be more or less intimate: Arm’s length, Handshake, or Merged. Copyleft licenses may impact proprietary or non-Copyleft-licensed programs based on the nature of the dependencies and interaction between programs:
- Any Handshake or Merged relationship with a GPL-licensed program implies that GPL becomes the primary license for that combined program
- Any Merged relationship with LGPL-licensed programs implies that LGPL becomes the primary license for that combined program
Arm’s length is similar to “independent process” or “command line.” Handshake is similar to “dynamic linking.” Merged is similar to “static linking.”
Relationships may flow through a dependency chain and the related license conditions may also follow that dependency chain. What if a proprietary program depends on a program that depends on a program that depends on a program that is GPL-licensed? It depends!
- What is the type of relationship at each step?
- What are the licenses of the intermediate programs?
Eventually the GPL license may flow up or down to a proprietary program for a “Handshake” or “Merged” style of relationship, unless the relevant license has an exception to stop this flow (e.g. such as the Linux User Space exception to the GPL license) or the nature of the interaction changes through this chain (such as when a dependent program is used at “Arm’s length”).
An approach to handle dependencies and license compliance
The recommended approach to properly handle dependencies — whether you write proprietary or FLOSS-licensed code:
- Evaluate the likely dependencies for a new component or package during selection and evaluation
- Confirm actual dependencies when you build for your product, application or library
- Ensure that you consider the whole chain of dependencies, both direct and indirect and at full depth
- Consider the purpose of each program or package in that chain (test, tool, compilation, runtime, etc.)
- Determine which dependent programs are used
- Only at development time, or
- Distributed with your product, application or library, or
- Installed by your user or customer
- Determine the license of each dependency in this chain
- Evaluate what license conditions flow up or down this chain and determine what actions are required to comply with the applicable license conditions
Using these elements, you can define a policy for which dependencies may be acceptable or not in your context and document your corresponding obligations for the use of FLOSS programs.