TsFile C++ Packaging and Installation Design

Status: Mailing-list discussion draft. A review PR for this document is not intended to be merged; the mailing-list thread remains the canonical decision record.

This document defines the intended installation and packaging contract for the TsFile C++ library and command-line tools. It is the design proposed in the package-manager discussion. The contract must be agreed before public binary packages are produced because downstream packages make library names, paths, and compatibility promises costly to change.

Goals

  • Provide a conventional, relocatable CMake installation.
  • Give downstream consumers stable CMake and pkg-config interfaces.
  • Define the public ABI, library version, and SONAME policy.
  • Define logical runtime, development, and tools components while allowing each package manager to follow its own conventions.
  • Generate local test packages and validate their complete lifecycle in CI.
  • Keep every published package traceable to a community-approved Apache source release.

The work proceeds in stages. First, make the shared library, headers, CMake package, pkg-config metadata, CLI, and notices install correctly without any platform package. Second, validate that installation with independent consumers. Third, build native Homebrew and Debian/Ubuntu packages from the same install rules. RPM packaging follows after that workflow is stable.

Native packages contain the shared library and use dependencies supplied by their package manager. Static libtsfile packages are not planned. A portable all-in-one archive with pinned bundled dependencies is a separate distribution profile, not a fallback for every platform without APT.

Release and distribution boundary

The ASF source release remains the official Apache release. A package in Homebrew, Debian, Fedora, or another downstream repository is a downstream distribution of that release, not a separate Apache release.

Public packages must:

  • be built from a source release approved by the project community;
  • use a version that maps unambiguously to that source release;
  • retain provenance such as the source URL and source checksum in the platform-specific packaging metadata;
  • carry LICENSE and NOTICE files that describe the exact artifact contents, including bundled third-party software when applicable; and
  • avoid presenting snapshots, nightly builds, or release candidates as a general-purpose installation channel.

CI may create packages from development revisions for validation. Those artifacts are development-only, must include a revision identifier, and must not be promoted as releases. Public testing channels and release-candidate handling require a separate community decision.

These rules follow the ASF Release Policy, the ASF third-party license policy, and the ASF trademark policy.

Installation contract

Installation uses GNUInstallDirs and respects CMAKE_INSTALL_PREFIX, DESTDIR, and platform-specific directory overrides.

ArtifactInstallation path
Shared library${CMAKE_INSTALL_LIBDIR}/libtsfile.*
Windows runtime library${CMAKE_INSTALL_BINDIR}/tsfile.dll
Windows import library${CMAKE_INSTALL_LIBDIR}/tsfile.lib
Public and required transitive headers${CMAKE_INSTALL_INCLUDEDIR}/tsfile/...
CMake package${CMAKE_INSTALL_LIBDIR}/cmake/TsFile/
pkg-config metadata${CMAKE_INSTALL_LIBDIR}/pkgconfig/libtsfile.pc
CLI${CMAKE_INSTALL_BINDIR}/tsfile-cli
License and project notices${CMAKE_INSTALL_DATADIR}/doc/tsfile/

Headers are installed below ${CMAKE_INSTALL_INCLUDEDIR}/tsfile, while the include root exported to consumers is ${CMAKE_INSTALL_INCLUDEDIR}. With those two settings, consumer code uses:

#include <tsfile/reader/tsfile_reader.h>
#include <tsfile/writer/tsfile_writer.h>

For example, the exported target may obtain its installed include root through the following rule or an equivalent INSTALL_INTERFACE declaration:

install(TARGETS tsfile
        EXPORT TsFileTargets
        LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}"
        RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}"
        ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}"
        INCLUDES DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}")

The implementation must replace the current build-tree staging directory in install(TARGETS) with the standard destinations above. It must also install headers through an explicit reviewed manifest. Copying every source header is not a declaration that every internal class is public API.

CMake consumer interface

The supported CMake interface is:

find_package(TsFile CONFIG REQUIRED)
target_link_libraries(my_application PRIVATE TsFile::tsfile)

Consumers choose where CMake searches for the installed package through CMAKE_PREFIX_PATH or TsFile_DIR. They do not manually add the TsFile header directory with target_include_directories():

cmake -S . -B build -DCMAKE_PREFIX_PATH=/opt/tsfile

The installation provides:

  • TsFileConfig.cmake;
  • TsFileConfigVersion.cmake;
  • TsFileTargets.cmake; and
  • the imported target TsFile::tsfile.

The same target name is available in the build tree as an alias. Its usage requirements include the installed header directory, the required C++ standard, platform libraries, and any feature definitions visible from public headers. Generated CMake files must be relocatable and must not contain source or build directory paths.

pkg-config consumer interface

The module name is libtsfile:

pkg-config --cflags --libs libtsfile

The name follows the installed library basename, libtsfile, and is less likely to collide with a generic module called tsfile. There is no central pkg-config registry. Before the first public package is submitted, maintainers must recheck the target Homebrew and Linux package indexes, plus commonly used public .pc module names, for conflicts. Consumer CI must use an isolated installation prefix and verify that pkg-config resolves libtsfile.pc from that prefix rather than from an unrelated preinstalled package.

libtsfile.pc contains the same public include and link requirements as the CMake target. Dependencies used only inside the shared library are omitted. A dependency whose types appear in public headers belongs in Requires; it must not be hidden in Requires.private.

Its include declaration is conceptually:

includedir=${prefix}/include
Cflags: -I${includedir}

The generated Cflags keeps ${includedir} as its root and does not append /tsfile. CMake and pkg-config consumers therefore obtain the same #include <tsfile/...> form from the package metadata.

Public API and ABI policy

The first release containing the completed installation metadata establishes the packaged ABI baseline. The public API consists of:

  • headers listed in the installation manifest as public entry points;
  • public declarations reachable from those headers and required to use them;
  • the C wrapper declarations installed for external consumers; and
  • the TsFile::tsfile CMake target and libtsfile pkg-config module.

An implementation header may need to be installed because a public header includes it. That does not make undocumented declarations in the implementation header a supported API. The implementation work should add a small set of documented umbrella or entry-point headers and reduce exposed implementation details over time without breaking those entry points.

The compatibility rules are:

  • Adding API without changing existing binary layouts is compatible.
  • Removing or changing an exported function, symbol, class layout, virtual interface, enum value, or public data-member type is incompatible.
  • Changing compiler flags or feature macros in a way that changes a public type layout is incompatible.
  • An incompatible change requires a new ABI epoch and therefore a new SONAME. Source deprecation alone does not permit an ABI break within an epoch.
  • Platform toolchain ABI boundaries still apply. Portable C++ archives must identify their operating system, architecture, compiler/runtime baseline, and minimum supported operating-system version.

The C wrapper remains part of libtsfile for the first milestone; it does not become a second runtime library. Splitting it later would require its own ABI and SONAME decision.

Library version and SONAME

The release version and ABI epoch are separate values:

set(TSFILE_VERSION <approved-source-release-version>)
set(TSFILE_ABI_VERSION <initial-abi-epoch>)

set_target_properties(tsfile PROPERTIES
    VERSION   "${TSFILE_VERSION}"
    SOVERSION "${TSFILE_ABI_VERSION}")

TSFILE_VERSION identifies the source release. TSFILE_ABI_VERSION is a positive integer changed only for an incompatible ABI change. Before selecting the initial value, implementation work must inspect the SONAME or install name produced by recent releases, identify known binary consumers, and establish a baseline from the installed public headers and exported symbols. If no stable binary compatibility promise exists, ABI epoch 1 is the recommended initial value. If an existing binary contract must be preserved, the initial value and transition plan must reflect it rather than silently relabeling the library.

For example, if epoch 1 is selected, a 2.4.0 build installs libtsfile.so.2.4.0 with SONAME libtsfile.so.1 on ELF platforms. Development suffixes such as .dev must never appear in a SONAME.

This replaces the current behavior where the complete project version is also used as SOVERSION. Keeping the ABI epoch independent avoids changing the runtime package name for every feature or patch release.

Before the baseline is declared, CI must compare builds with supported feature profiles and remove configuration-dependent layouts from public headers where possible. In particular, build-only and test-only definitions must not alter the installed ABI. Required feature state should be expressed through an installed generated configuration header and propagated by the consumer metadata.

Shared-library scope

The packaging contract supports one shared target, tsfile. Public binary packages must use a fixed, documented feature profile so that packages with the same SONAME have the same public ABI.

There is no plan to publish a static libtsfile. Native packages and the portable all-in-one archive both expose a shared library. A future request for a public static library requires a separate community proposal covering a concrete use case, dependency availability, security updates, symbol visibility, and license aggregation.

Dependency policy

Native Homebrew, Debian/Ubuntu, Fedora, and EPEL packages should use system dependencies where the platform provides a supported version. This allows the platform to deliver security updates and avoids shipping duplicate libraries. The build must therefore gain an explicit provider mode rather than selecting bundled code implicitly:

TSFILE_DEPENDENCY_PROVIDER=system|bundled

system is required for every native downstream package, including Homebrew, DEB, and RPM. A dependency unavailable on a target platform may be disabled or handled in that platform's packaging review; it must not silently fall back to a bundled copy.

bundled is intended for reproducible local builds and the portable all-in-one profile. Its dependency versions and complete licensing material must be pinned and audited. The two provider modes produce distinct artifacts and must not be mixed in one package build.

The package configuration must expose only dependencies required by consumers. Private compression and parser implementations linked into the shared library must not leak source-tree include paths or unnecessary link flags into TsFile::tsfile or libtsfile.pc.

Native package components

CPack and CI use three logical components:

ComponentContentsExample native package names
runtimeVersioned shared library and required noticesDebian: libtsfile1; RPM: tsfile-libs
developmentHeaders, unversioned linker name/import library, CMake config, pkg-config fileDebian: libtsfile-dev; RPM: tsfile-devel
toolstsfile-cli and required noticestsfile-tools

The numeric Debian runtime suffix follows the ABI epoch, not the source release major version. Exact names remain platform packaging decisions.

Homebrew should use one formula containing the runtime, development files, and CLI unless Homebrew review establishes a reason to split them. Official distribution repositories use their native packaging files. CPack-generated DEB or RPM output is a local validation aid and is not submitted in place of a formula, Debian packaging, or an RPM spec. The CPack TGZ profile described below has a different purpose: it produces the portable all-in-one archive.

Portable all-in-one archive

An all-in-one archive serves systems where a suitable native package is not available or installing development dependencies is impractical. It is not selected merely because a platform does not use APT. DNF/YUM, Homebrew, and other capable native package managers still use the system dependency profile.

A portable archive contains:

apache-tsfile-cpp-<version>-<os>-<arch>/
├── bin/tsfile-cli
├── lib/libtsfile.<shared-library-suffix>
├── include/tsfile/...
├── lib/cmake/TsFile/...
├── lib/pkgconfig/libtsfile.pc
├── LICENSE
└── NOTICE

Pinned third-party dependencies may be linked privately into the shared libtsfile or placed in a private library directory. Third-party symbols must not leak into the public ABI, and any private shared libraries must use origin-relative runtime lookup paths. The archive must not bundle foundational system runtimes such as glibc.

Each archive is specific to an operating system, architecture, compiler/C++ runtime baseline, and minimum operating-system version. It must be relocatable, run without development packages installed, and include exact third-party LICENSE and NOTICE content. A dependency security update requires rebuilding and republishing the complete archive.

The all-in-one archive is a convenience binary built from an approved source release. It does not replace the ASF source release and does not contain a public static libtsfile.

CPack scope

CPack uses separate packaging profiles:

  • DEB uses TSFILE_DEPENDENCY_PROVIDER=system and the runtime, development, and tools components.
  • TGZ produces one portable all-in-one archive with TSFILE_DEPENDENCY_PROVIDER=bundled after its relocation, symbol, and license checks are available.
  • RPM uses TSFILE_DEPENDENCY_PROVIDER=system and is enabled after RPM CI and package conventions are ready.

Package metadata includes the source release version, project URL, license identifier, architecture, component dependencies where applicable, and package maintainer contact agreed by the community.

CPack configuration must be owned by the TsFile top-level project. The bundled ANTLR build currently calls include(CPack), which can produce a top-level configuration containing ANTLR metadata. Dependency CPack configuration must be disabled or isolated before TsFile packages are generated.

CI acceptance criteria

Packaging changes are complete only when CI verifies:

  1. A clean release build with tests and examples excluded from installed artifacts.
  2. Installation to a temporary, non-default prefix using DESTDIR, with both the minimum supported CMake version and cmake --install on newer CMake versions.
  3. An independent CMake consumer that calls find_package(TsFile CONFIG REQUIRED), links TsFile::tsfile without manually adding an include directory, verifies that the imported include root is <prefix>/include rather than <prefix>/include/tsfile, compiles with the resulting <tsfile/...> include form, and reads and writes a small TsFile.
  4. An independent pkg-config consumer that verifies the same include root, uses the same #include <tsfile/...> spelling, and compiles and runs the same smoke operation.
  5. Each public entry-point header compiles in an isolated translation unit using only the usage requirements provided by the installed package.
  6. CLI execution, including tsfile-cli --version.
  7. Package creation, installation on a clean image, upgrade from the previous compatible release when one exists, removal, and a check for unexpected remaining files.
  8. Runtime/development/tools component dependencies and file ownership in native packages.
  9. Native packages link to package-manager dependencies and do not install private copies of them.
  10. The portable archive runs in a clean environment without development dependencies, remains relocatable, and resolves any bundled libraries only from inside the archive.
  11. Absence of source/build paths, leaked third-party symbols, and unaccounted LICENSE or NOTICE content.
  12. SONAME, symlink, exported-symbol, and ABI checks against the previous release in the same ABI epoch.

The initial native-package matrix covers macOS with Homebrew dependencies and a supported Debian/Ubuntu release. A Linux all-in-one job is added only after the bundled-dependency and license checks are ready. Fedora/RPM jobs are added before RPM artifacts or repositories are advertised.

Incremental implementation

The implementation should be split into reviewable changes:

  1. Agree on this installation, naming, ABI, and component contract.
  2. Add standard install destinations, the tsfile/-namespaced public-header layout and manifest, versioned shared-library rules, CMake export files, and pkg-config metadata.
  3. Add independent CMake and pkg-config consumers plus install/uninstall and ABI checks.
  4. Add explicit, isolated system/bundled dependency provider selection.
  5. Add native DEB packaging and a Homebrew test formula using system dependencies, plus their lifecycle CI.
  6. Add the portable all-in-one TGZ profile using pinned bundled dependencies, plus relocation, symbol, license, and clean-runtime checks.
  7. Add native RPM packaging with system dependencies when maintainers and user demand justify it.
  8. Discuss ownership, credentials, retention, and release-candidate policy before creating any public testing repository.