fontist 3.0.35 → 3.0.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1ade396e3811d8a2df5ae15cce0af89df56bf723bc9c915f42f72b487073de39
4
- data.tar.gz: ab738ce85235d7cdf811c0cf51ec23b28488637d09f7c9bc77ca9b91c8afb2e6
3
+ metadata.gz: 85b1409d941968bcf69c2058ab3e3ffac4138279bf6c262d670206590dd8e557
4
+ data.tar.gz: f025af9d9214504168b1612f7f42f3142afdadb9112e9283022fe92378665000
5
5
  SHA512:
6
- metadata.gz: 69e7e9ef3fd5200939a9c2820cd58e19af2cef6890ee4f91b317c227c5e33b46d5ae71046663e565c4883a89bda147a39f9c723a51383ab03203c8e59ac73fcf
7
- data.tar.gz: 3e08dc9b127f6e313ddf2f6fcae9fa174832d57c0ae0bc4bb7730094f2fbc05a0171966ce1d793f1af87ef08808b88b0771630acf374347210dcb6ac8bc10f41
6
+ metadata.gz: 6c13e6cca540bd6f5e2ed3bd203bad3b4dad7db56824d34e36b511331b6a9bd5b5178ecf7fc6717318d54981950c33536ae3dfc6eafc82e4ceaf771f3b08fbdb
7
+ data.tar.gz: f1a3542fcda412f62e763cb38d9484028ddc65585eba3d6c9cb982eea8ef80656672769fc898a5b4266ef7419adadcf7e592200f4d7c8c9845505ddf13ec4f58
@@ -28,3 +28,13 @@ jobs:
28
28
  next_version: ${{ github.event.inputs.next_version }}
29
29
  secrets:
30
30
  rubygems-api-key: ${{ secrets.FONTIST_CI_RUBYGEMS_API_KEY }}
31
+
32
+ # Self-contained executables and .deb/.rpm packages for the new version,
33
+ # attached to its GitHub Release (fontist/fontist#463).
34
+ packages:
35
+ needs: release
36
+ permissions:
37
+ contents: write
38
+ uses: ./.github/workflows/tebako-pack.yml
39
+ with:
40
+ publish: true
@@ -1,65 +1,239 @@
1
1
  name: tebako-pack
2
2
 
3
+ # Packs fontist into one self-contained executable per platform with
4
+ # tebako v2, wraps the Linux ones into .deb/.rpm packages, checks both on
5
+ # machines with no Ruby, and attaches them to the GitHub Release when given
6
+ # a tag.
7
+
3
8
  on:
4
- repository_dispatch:
5
- types: [ do-release ]
9
+ # Called by release.yml once the gem is out. Its releases are created with
10
+ # GITHUB_TOKEN, and GitHub starts no workflow from such an event, so a
11
+ # `release: published` trigger here would never fire.
12
+ workflow_call:
13
+ inputs:
14
+ publish:
15
+ type: boolean
16
+ default: true
6
17
  workflow_dispatch:
18
+ inputs:
19
+ tag:
20
+ description: Release tag to build and attach to (empty = build only)
21
+ required: false
22
+ default: ''
23
+ pull_request:
24
+ paths:
25
+ - .github/workflows/tebako-pack.yml
26
+ - script/tebako/**
27
+ - packaging/**
7
28
 
8
29
  permissions:
9
30
  contents: read
10
- packages: write
11
31
 
12
32
  concurrency:
13
- group: '${{ github.workflow }}-${{ github.job }}-${{ github.head_ref || github.ref_name }}'
33
+ group: '${{ github.workflow }}-${{ github.head_ref || github.ref_name }}'
14
34
  cancel-in-progress: true
15
35
 
16
36
  env:
17
- # This is Ruby version that will be as a base for tebako package
18
- # I.e.: fontist binary will be executed by this version of Ruby
19
- PACKAGE_RUBY_VER: 3.2.3
20
- # This is Ruby version that will be used to execute tebako itself
21
- # It can be the same as PACKAGE_RUBY_VER but for whatever reason 3.1.4
22
- # has better performance
23
- TEBAKO_RUBY_VER: 3.1.4
37
+ # Ruby the packed fontist runs on: a version tebako ships a runtime for
38
+ # (tebako press rejects the rest).
39
+ PACKAGE_RUBY_VERSION: 3.3.12
40
+ TEBAKO_VERSION: 2.8.68
41
+ NFPM_VERSION: 2.48.0
24
42
 
25
43
  jobs:
44
+ resolve:
45
+ name: resolve tag
46
+ runs-on: ubuntu-24.04
47
+ outputs:
48
+ tag: ${{ steps.tag.outputs.tag }}
49
+ steps:
50
+ # After release.yml the default branch carries the released version.
51
+ - uses: actions/checkout@v4
52
+ with:
53
+ ref: ${{ github.event.repository.default_branch }}
54
+
55
+ - id: tag
56
+ env:
57
+ GH_TOKEN: ${{ github.token }}
58
+ CALLED: ${{ github.event_name != 'workflow_dispatch' && github.event_name != 'pull_request' && inputs.publish }}
59
+ INPUT_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || '' }}
60
+ run: |
61
+ if [ "$CALLED" = true ]; then
62
+ tag="v$(sed -n 's/.*VERSION = "\(.*\)".*/\1/p' lib/fontist/version.rb)"
63
+ else
64
+ tag="$INPUT_TAG"
65
+ fi
66
+ if [ -n "$tag" ]; then
67
+ # Only a version tag that is a published release of this repo.
68
+ if ! printf '%s' "$tag" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
69
+ echo "not a release tag: $tag" >&2
70
+ exit 1
71
+ fi
72
+ gh release view "$tag" --repo "$GITHUB_REPOSITORY" --json tagName >/dev/null
73
+ fi
74
+ echo "tag=$tag" >> "$GITHUB_OUTPUT"
75
+ echo "building ${tag:-$GITHUB_SHA}"
76
+
26
77
  pack:
27
- name: Pack fontist
78
+ name: pack ${{ matrix.name }}
79
+ needs: resolve
28
80
  runs-on: ${{ matrix.os }}
29
81
  strategy:
30
82
  fail-fast: false
31
- # macos-11 runner is deadly slow because brew is not supported on this version anymore
32
- # so it is practically impossible to use it for tests anymore
33
83
  matrix:
34
- os: [ubuntu-20.04, macos-12]
84
+ include:
85
+ # Linux is packed on the oldest runner so the result runs on older
86
+ # glibc distros too.
87
+ - { os: ubuntu-22.04, triplet: linux-gnu-x86_64, name: linux-x86_64 }
88
+ - { os: ubuntu-22.04-arm, triplet: linux-gnu-arm64, name: linux-arm64 }
89
+ - { os: macos-15, triplet: macos-arm64, name: macos-arm64 }
90
+ - { os: macos-15-intel, triplet: macos-x86_64, name: macos-x86_64 }
91
+ steps:
92
+ - uses: actions/checkout@v4
93
+ with:
94
+ fetch-depth: 0
95
+
96
+ # A release build packs the validated release tag, not the dispatching
97
+ # commit (release.yml bumps the version after this run's commit).
98
+ - name: Check out the release tag
99
+ if: needs.resolve.outputs.tag != ''
100
+ env:
101
+ TAG: ${{ needs.resolve.outputs.tag }}
102
+ run: git checkout --detach "refs/tags/$TAG"
103
+
104
+ - uses: ruby/setup-ruby@v1
105
+ with:
106
+ ruby-version: '3.3'
107
+
108
+ - name: Press
109
+ env:
110
+ GH_TOKEN: ${{ github.token }}
111
+ run: script/tebako/press.sh "${{ matrix.triplet }}" "out/fontist-${{ matrix.name }}"
112
+
113
+ # Uploaded before the smoke test so a failing binary can be fetched
114
+ # and inspected.
115
+ - uses: actions/upload-artifact@v4
116
+ with:
117
+ name: fontist-${{ matrix.name }}
118
+ path: out/fontist-${{ matrix.name }}
119
+ if-no-files-found: error
120
+
121
+ # Only the system directories and git stay on PATH, so the Ruby set up
122
+ # above is out of reach. (macOS keeps a system ruby in /usr/bin; the
123
+ # fresh distro containers in package-smoke are the no-Ruby-at-all
124
+ # check.) fontist's git gem needs git >= 2.43, newer than Apple's, so
125
+ # the runner's own git is kept.
126
+ - name: Smoke test without the build Ruby
127
+ run: |
128
+ git_dir="$(dirname "$(command -v git)")"
129
+ env -i HOME="$HOME" PATH="$git_dir:/usr/bin:/bin:/usr/sbin:/sbin" \
130
+ script/tebako/smoke.sh "out/fontist-${{ matrix.name }}"
131
+
132
+ packages:
133
+ name: deb and rpm
134
+ needs: [ resolve, pack ]
135
+ runs-on: ubuntu-24.04
35
136
  steps:
36
- - uses: actions/checkout@v4
37
- with:
38
- fetch-depth: 1
39
-
40
- - name: Setup Tebako
41
- uses: tamatebako/tebako/.github/actions/setup-tebako@main
42
- with:
43
- cache: build
44
- ruby_version: ${{ env.TEBAKO_RUBY_VER }}
45
-
46
- - name: Pack Fontist
47
- run: |
48
- tebako press -R "$PACKAGE_RUBY_VER" --root="." --entry-point="fontist" --output="fontist"
49
- if [ ${{ matrix.os }} -ne "macos-12" ]; then
50
- strip fontist
51
- fi
52
- ./fontist help || echo "fontist help exits with status 1"
53
- cp ./fontist ./fontist-${{ runner.os }}
54
-
55
- - if: github.event_name != 'workflow_dispatch'
56
- uses: softprops/action-gh-release@v1
57
- with:
58
- files: |
59
- fontist-${{ runner.os }}
60
-
61
- - if: github.event_name == 'workflow_dispatch'
62
- uses: actions/upload-artifact@v4
63
- with:
64
- name: fontist-${{ matrix.os }}
65
- path: fontist-${{ runner.os }}
137
+ - uses: actions/checkout@v4
138
+ with:
139
+ fetch-depth: 0
140
+
141
+ # A release build packs the validated release tag, not the dispatching
142
+ # commit (release.yml bumps the version after this run's commit).
143
+ - name: Check out the release tag
144
+ if: needs.resolve.outputs.tag != ''
145
+ env:
146
+ TAG: ${{ needs.resolve.outputs.tag }}
147
+ run: git checkout --detach "refs/tags/$TAG"
148
+
149
+ - uses: actions/download-artifact@v4
150
+ with:
151
+ pattern: fontist-linux-*
152
+ path: out
153
+ merge-multiple: true
154
+
155
+ - name: Install nfpm
156
+ env:
157
+ GH_TOKEN: ${{ github.token }}
158
+ run: |
159
+ gh release download "v${NFPM_VERSION}" --repo goreleaser/nfpm --dir /tmp/nfpm \
160
+ --pattern "nfpm_${NFPM_VERSION}_Linux_x86_64.tar.gz" --pattern checksums.txt
161
+ (cd /tmp/nfpm && sha256sum --ignore-missing -c checksums.txt)
162
+ tar -xzf "/tmp/nfpm/nfpm_${NFPM_VERSION}_Linux_x86_64.tar.gz" -C /tmp/nfpm nfpm
163
+ sudo install /tmp/nfpm/nfpm /usr/local/bin/nfpm
164
+
165
+ - name: Build packages
166
+ run: |
167
+ script/tebako/package.sh amd64 out/fontist-linux-x86_64 pkg
168
+ script/tebako/package.sh arm64 out/fontist-linux-arm64 pkg
169
+
170
+ - uses: actions/upload-artifact@v4
171
+ with:
172
+ name: fontist-linux-packages
173
+ path: pkg/*
174
+ if-no-files-found: error
175
+
176
+ package-smoke:
177
+ name: install on ${{ matrix.image }} ${{ matrix.arch }}
178
+ needs: [ resolve, packages ]
179
+ runs-on: ${{ matrix.runner }}
180
+ container: ${{ matrix.image }}
181
+ strategy:
182
+ fail-fast: false
183
+ matrix:
184
+ # Distros whose git meets the git gem's >= 2.43; older ones refuse
185
+ # the package on its git dependency.
186
+ image: [ 'ubuntu:24.04', 'debian:13', 'fedora:latest', 'rockylinux:9' ]
187
+ arch: [ amd64 ]
188
+ runner: [ ubuntu-24.04 ]
189
+ include:
190
+ - { image: 'ubuntu:24.04', arch: arm64, runner: ubuntu-24.04-arm }
191
+ steps:
192
+ # Only the smoke scripts are used here, so the dispatching commit is
193
+ # enough (these containers have no git to check out a tag with).
194
+ - uses: actions/checkout@v4
195
+
196
+ - uses: actions/download-artifact@v4
197
+ with:
198
+ name: fontist-linux-packages
199
+ path: pkg
200
+
201
+ - name: Keep only this architecture
202
+ run: |
203
+ if [ "${{ matrix.arch }}" = amd64 ]; then
204
+ find pkg \( -name '*arm64*' -o -name '*aarch64*' \) -delete
205
+ else
206
+ find pkg \( -name '*amd64*' -o -name '*x86_64*' \) -delete
207
+ fi
208
+ ls -l pkg
209
+
210
+ - name: Install, use, remove
211
+ run: script/tebako/package-smoke.sh pkg
212
+
213
+ publish:
214
+ name: attach to release
215
+ if: needs.resolve.outputs.tag != ''
216
+ needs: [ resolve, pack, package-smoke ]
217
+ runs-on: ubuntu-24.04
218
+ permissions:
219
+ contents: write
220
+ steps:
221
+ - uses: actions/download-artifact@v4
222
+ with:
223
+ path: dist
224
+ merge-multiple: true
225
+
226
+ - name: Name executables by version and upload
227
+ env:
228
+ GH_TOKEN: ${{ github.token }}
229
+ TAG: ${{ needs.resolve.outputs.tag }}
230
+ run: |
231
+ version="${TAG#v}"
232
+ cd dist
233
+ for name in linux-x86_64 linux-arm64 macos-arm64 macos-x86_64; do
234
+ mv "fontist-$name" "fontist-$version-$name"
235
+ done
236
+ # Executables and .rpm files are fontist-*, .deb files fontist_*.
237
+ sha256sum fontist-* fontist_* > SHA256SUMS
238
+ gh release upload "$TAG" --repo "$GITHUB_REPOSITORY" --clobber \
239
+ fontist-* fontist_* SHA256SUMS
data/README.adoc CHANGED
@@ -42,6 +42,56 @@ And then execute:
42
42
  bundle install
43
43
  ----
44
44
 
45
+ === Install without Ruby
46
+
47
+ Each release on the
48
+ https://github.com/fontist/fontist/releases[GitHub Releases] page has a
49
+ self-contained `fontist` executable. It does not need a Ruby installation.
50
+ Linux and macOS are supported.
51
+
52
+ Download the file for your system, check it, and make it executable. The
53
+ names are `fontist-<version>-linux-x86_64`, `-linux-arm64`, `-macos-arm64`
54
+ and `-macos-x86_64`.
55
+
56
+ [source,sh]
57
+ ----
58
+ curl -LO https://github.com/fontist/fontist/releases/download/v<version>/fontist-<version>-linux-x86_64
59
+ curl -LO https://github.com/fontist/fontist/releases/download/v<version>/SHA256SUMS
60
+ sha256sum --ignore-missing -c SHA256SUMS # on macOS: shasum -a 256 -c --ignore-missing SHA256SUMS
61
+ chmod +x fontist-<version>-linux-x86_64
62
+ ./fontist-<version>-linux-x86_64 version
63
+ ----
64
+
65
+ The executables are not signed. On macOS, remove the quarantine flag before
66
+ the first run:
67
+
68
+ [source,sh]
69
+ ----
70
+ xattr -d com.apple.quarantine ./fontist-<version>-macos-arm64
71
+ ----
72
+
73
+ Each run prints a notice from the packager that the executable is unsigned.
74
+ It is harmless.
75
+
76
+ fontist needs git 2.43 or newer to fetch formulas. The git that ships with
77
+ macOS is older, so install a current one first (for example
78
+ `brew install git`).
79
+
80
+ On Debian and Ubuntu, install the `.deb` package. On Fedora and RHEL, install
81
+ the `.rpm` package. Both put `fontist` in `/usr/bin` and depend on git 2.43 or
82
+ newer, which Ubuntu 24.04, Debian 13, Fedora and RHEL 9 provide.
83
+
84
+ [source,sh]
85
+ ----
86
+ sudo apt install ./fontist_<version>_amd64.deb
87
+ sudo dnf install ./fontist-<version>-1.x86_64.rpm
88
+ ----
89
+
90
+ For ARM64, use `_arm64.deb` and `.aarch64.rpm`.
91
+
92
+ This build uses the same home directory as the gem (`~/.fontist`, or
93
+ `FONTIST_PATH` if set), so the two can be used side by side.
94
+
45
95
  === Fetch formulas
46
96
 
47
97
  After installation, please fetch formulas via the `fontist` command:
@@ -363,6 +413,20 @@ Keeps only the glyphs for those characters. Options can be combined with
363
413
  fontist pack family.ttc Regular.ttf Bold.ttf
364
414
  ----
365
415
 
416
+ ==== Transform a font file you already have
417
+
418
+ `fontist convert` runs the same operations on a local file, without
419
+ installing anything. Each output is written next to the source unless
420
+ `--output` (or `--output-dir`) is given, and the source is never overwritten.
421
+
422
+ [source,sh]
423
+ ----
424
+ fontist convert format Lato-Regular.ttf woff2
425
+ fontist convert extract NotoSansCJK.ttc --index 1 --output-dir out
426
+ fontist convert instance "Roboto[wdth,wght].ttf" wght=300,wdth=90
427
+ fontist convert subset Lato-Regular.ttf --unicodes "U+0020-007E" --text "é"
428
+ ----
429
+
366
430
  ==== Ruby API
367
431
 
368
432
  [source,ruby]
@@ -204,6 +204,7 @@ export default defineConfig({
204
204
  { text: "repo", link: "/cli/repo" },
205
205
  { text: "fontconfig", link: "/cli/fontconfig" },
206
206
  { text: "import", link: "/cli/import" },
207
+ { text: "convert", link: "/cli/convert" },
207
208
  { text: "index", link: "/cli/index-cmd" },
208
209
  { text: "create-formula", link: "/cli/create-formula" },
209
210
  ],
@@ -247,7 +248,7 @@ export default defineConfig({
247
248
  ],
248
249
 
249
250
  footer: {
250
- message: `Fontist is <a href="https://open.ribose.com/">riboseopen</a>`,
251
+ message: `Fontist is <a href="https://www.ribose.com/">riboseopen</a>`,
251
252
  copyright: `Copyright &copy; 2026 Ribose Group Inc. All rights reserved.`,
252
253
  },
253
254
  },
@@ -0,0 +1,38 @@
1
+ # fontist convert
2
+
3
+ Transform a font file you already have, using [Fontisan](https://github.com/fontist/fontisan). Nothing is installed.
4
+
5
+ ## Syntax
6
+
7
+ ```sh
8
+ fontist convert format FONT_FILE FORMAT [--output FILE]
9
+ fontist convert extract COLLECTION --index N [--output-dir DIR]
10
+ fontist convert instance FONT_FILE AXES [--output FILE]
11
+ fontist convert subset FONT_FILE [--unicodes LIST] [--text TEXT] [--output FILE]
12
+ ```
13
+
14
+ ## Subcommands
15
+
16
+ | Subcommand | Description |
17
+ |------------|-------------|
18
+ | `format` | Convert to `ttf`, `otf`, `woff` or `woff2` |
19
+ | `extract` | Write one member of a TTC/OTC collection as a standalone font |
20
+ | `instance` | Write a static instance of a variable font at the given axis values |
21
+ | `subset` | Keep only the glyphs for the given characters |
22
+
23
+ By default each output is written next to the source: `Lato.woff2`, `Font-wght300.ttf`, `Lato-subset.ttf`. A command never overwrites its source font.
24
+
25
+ ## Examples
26
+
27
+ ```sh
28
+ fontist convert format Lato-Regular.ttf woff2
29
+ fontist convert extract NotoSansCJK.ttc --index 1 --output-dir out
30
+ fontist convert instance "Roboto[wdth,wght].ttf" wght=300,wdth=90
31
+ fontist convert subset Lato-Regular.ttf --unicodes "U+0020-007E" --text "é"
32
+ ```
33
+
34
+ Axis tags and values are checked against the font: an unknown axis or a value outside its range fails with an error.
35
+
36
+ ## Related Commands
37
+
38
+ - [fontist install](/cli/install) - The same operations while installing (`--format`, `--collection-index`, `--instance`, `--subset-*`)
data/docs/cli/index.md CHANGED
@@ -23,6 +23,7 @@ Complete command-line interface reference for Fontist.
23
23
  | [`fontist repo`](/cli/repo) | Custom repositories |
24
24
  | [`fontist fontconfig`](/cli/fontconfig) | Fontconfig integration |
25
25
  | [`fontist import`](/cli/import) | Import fonts |
26
+ | [`fontist convert`](/cli/convert) | Convert, extract, instance or subset a font file |
26
27
  | [`fontist index`](/cli/index-cmd) | System font index |
27
28
  | [`fontist create-formula`](/cli/create-formula) | Create formulas |
28
29
 
data/lib/fontist/cli.rb CHANGED
@@ -33,6 +33,7 @@ module Fontist
33
33
  Fontist::Errors::CollectionIndexError => [STATUS_UNKNOWN_ERROR],
34
34
  Fontist::Errors::UnsupportedTranscodeError => [STATUS_UNKNOWN_ERROR],
35
35
  Fontist::Errors::SourceNotFoundError => [STATUS_UNKNOWN_ERROR],
36
+ Fontist::Errors::InstanceError => [STATUS_UNKNOWN_ERROR],
36
37
  Fontist::Errors::SizeLimitError => [
37
38
  STATUS_SIZE_LIMIT_ERROR,
38
39
  :append,
@@ -470,6 +471,10 @@ module Fontist
470
471
  desc "config SUBCOMMAND ...ARGS", "Manage fontist config"
471
472
  subcommand "config", Fontist::ConfigCLI
472
473
 
474
+ desc "convert SUBCOMMAND ...ARGS",
475
+ "Convert, extract, instance or subset a font file"
476
+ subcommand "convert", Fontist::ConvertCLI
477
+
473
478
  desc "cache SUBCOMMAND ...ARGS", "Manage fontist cache"
474
479
  subcommand "cache", Fontist::CacheCLI
475
480
 
@@ -559,7 +564,7 @@ module Fontist
559
564
  styles.each do |style, installed|
560
565
  opts = []
561
566
  opts << "manual" if formula.manual?
562
- opts << (installed ? "installed" : "not installed")
567
+ opts << install_label(installed)
563
568
  msg = " #{style.type} (#{opts.join(', ')})"
564
569
 
565
570
  if installed
@@ -573,6 +578,14 @@ module Fontist
573
578
  end
574
579
  # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
575
580
 
581
+ def install_label(status)
582
+ case status
583
+ when true then "installed"
584
+ when false then "not installed"
585
+ else "installed (system)"
586
+ end
587
+ end
588
+
576
589
  def print_find_results(results)
577
590
  if results.empty?
578
591
  Fontist.ui.say("No fonts found matching criteria")
@@ -0,0 +1,88 @@
1
+ module Fontist
2
+ # `fontist convert ...`: Fontisan operations on font files the user
3
+ # already has, without installing anything.
4
+ class ConvertCLI < Thor
5
+ include CLI::ClassOptions
6
+
7
+ desc "format FONT_FILE FORMAT",
8
+ "Convert FONT_FILE to FORMAT (ttf, otf, woff, woff2)"
9
+ option :output, aliases: :o,
10
+ desc: "Output file (default: FONT_FILE, new extension)"
11
+ def to_format(source, target)
12
+ default = replace_ext(source, ".#{target.to_s.downcase}")
13
+ write_font do
14
+ output = output_for(source, default)
15
+ FontOperations::Convert.call(source, target, output)
16
+ end
17
+ end
18
+ map "format" => :to_format
19
+
20
+ desc "extract COLLECTION", "Write one member of a TTC/OTC collection"
21
+ option :index, type: :numeric, required: true,
22
+ desc: "Zero-based member index"
23
+ option :output_dir, desc: "Output directory (default: next to COLLECTION)"
24
+ def extract(collection)
25
+ dir = options[:output_dir] || File.dirname(collection)
26
+ write_font do
27
+ FontOperations::Extract.call(collection, index: options[:index],
28
+ output_dir: dir)
29
+ end
30
+ end
31
+
32
+ desc "instance FONT_FILE AXES",
33
+ "Write a static instance of a variable font (e.g. wght=300,wdth=90)"
34
+ option :output, aliases: :o,
35
+ desc: "Output file (default: FONT_FILE named after AXES)"
36
+ def instance(source, axes)
37
+ write_font do
38
+ coords = FontOperations::Instance.parse(axes)
39
+ name = FontOperations::Instance.file_name(File.basename(source), coords)
40
+ default = File.join(File.dirname(source), name)
41
+ FontOperations::Instance.call(source, output_for(source, default),
42
+ coordinates: coords)
43
+ end
44
+ end
45
+
46
+ desc "subset FONT_FILE", "Keep only the glyphs for the given characters"
47
+ option :unicodes, desc: "Codepoints to keep (e.g. 'U+0020-007E,U+00E9')"
48
+ option :text, desc: "Characters to keep"
49
+ option :output, aliases: :o,
50
+ desc: "Output file (default: FONT_FILE with -subset)"
51
+ def subset(source)
52
+ default = replace_ext(source, "-subset#{File.extname(source)}")
53
+ write_font do
54
+ FontOperations::Subset.call(source, output_for(source, default),
55
+ unicodes: options[:unicodes],
56
+ text: options[:text])
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def write_font
63
+ handle_class_options(options)
64
+ path = yield
65
+ Fontist.ui.success("Written to #{path}")
66
+ CLI::STATUS_SUCCESS
67
+ rescue Errors::GeneralError => e
68
+ Fontist.ui.error(e.message)
69
+ CLI::STATUS_UNKNOWN_ERROR
70
+ end
71
+
72
+ # The given --output, or +default+; never the source itself.
73
+ def output_for(source, default)
74
+ output = options[:output] || default
75
+ if File.expand_path(output) == File.expand_path(source)
76
+ raise FontOperations::Error,
77
+ "Output would overwrite #{source}. Give a different --output."
78
+ end
79
+
80
+ output
81
+ end
82
+
83
+ def replace_ext(path, suffix)
84
+ File.join(File.dirname(path),
85
+ File.basename(path, File.extname(path)) + suffix)
86
+ end
87
+ end
88
+ end
data/lib/fontist/font.rb CHANGED
@@ -446,8 +446,9 @@ module Fontist
446
446
  def list_styles(formulas)
447
447
  map_to_hash(formulas) do |formula|
448
448
  map_to_hash(requested_fonts(formula.all_fonts)) do |font|
449
+ external = external_paths_by_type(font)
449
450
  map_to_hash(font.styles) do |style|
450
- installed(style)
451
+ installed(style, external)
451
452
  end
452
453
  end
453
454
  end
@@ -465,8 +466,25 @@ module Fontist
465
466
  end
466
467
  end
467
468
 
468
- def installed(style)
469
- path(style) ? true : false
469
+ # A font found only outside the fontist directory reports its paths
470
+ # instead of true, so callers can tell the copies fontist cannot
471
+ # update or remove from the managed ones.
472
+ def installed(style, external)
473
+ return true if path(style)
474
+
475
+ external.fetch(style.type.to_s.downcase, false)
476
+ end
477
+
478
+ # One index lookup per font name, grouped by lower-cased style type.
479
+ def external_paths_by_type(font)
480
+ entries = SystemFont.find_styles(font.name) || []
481
+ entries.reject { |entry| in_fontist_dir?(entry.path) }
482
+ .group_by { |entry| entry.type.to_s.downcase }
483
+ .transform_values { |group| group.map(&:path) }
484
+ end
485
+
486
+ def in_fontist_dir?(path)
487
+ path.start_with?(Fontist.fonts_path.to_s)
470
488
  end
471
489
 
472
490
  def raise_non_supported_font
@@ -403,8 +403,11 @@ module Fontist
403
403
  end
404
404
 
405
405
  def index_changed?
406
+ # Verified first: a location that holds no fonts leaves the index
407
+ # empty after its rebuild, and treating empty as unbuilt would rescan
408
+ # the disk on every lookup. Installs reach the index via add_font.
409
+ return false if @index_check_done # already verified in this session
406
410
  return true if fonts.nil? || fonts.empty?
407
- return false if @index_check_done # Skip if already verified in this session
408
411
 
409
412
  # Quick check: if index was scanned recently, trust it
410
413
  if recently_scanned?
@@ -93,7 +93,7 @@ module Fontist
93
93
 
94
94
  # Build lookup hash for fast O(1) access by path (non-shared state)
95
95
  def to_lookup
96
- entries.index_by(&:path)
96
+ entries.to_h { |entry| [entry.path, entry] }
97
97
  end
98
98
 
99
99
  # Get validation result for a path, checking if file has changed
@@ -78,8 +78,7 @@ module Fontist
78
78
  end
79
79
 
80
80
  # Validate the font
81
- font_file = FontFile.from_path(path, validate: true,
82
- check_extension: false)
81
+ font_file = FontFile.from_path(path)
83
82
 
84
83
  time_taken = Time.now - start_time
85
84
 
@@ -1,3 +1,3 @@
1
1
  module Fontist
2
- VERSION = "3.0.35".freeze
2
+ VERSION = "3.0.37".freeze
3
3
  end
data/lib/fontist.rb CHANGED
@@ -82,6 +82,7 @@ module Fontist
82
82
  autoload :CLI, "#{__dir__}/fontist/cli"
83
83
  autoload :CacheCLI, "#{__dir__}/fontist/cache_cli"
84
84
  autoload :ConfigCLI, "#{__dir__}/fontist/config_cli"
85
+ autoload :ConvertCLI, "#{__dir__}/fontist/convert_cli"
85
86
  autoload :FontconfigCLI, "#{__dir__}/fontist/fontconfig_cli"
86
87
  autoload :ImportCLI, "#{__dir__}/fontist/import_cli"
87
88
  autoload :IndexCLI, "#{__dir__}/fontist/index_cli"
@@ -0,0 +1,33 @@
1
+ # Linux packages that wrap the tebako-packed fontist executable.
2
+ # Built by script/tebako/package.sh, which sets ARCH, VERSION and BINARY.
3
+ name: fontist
4
+ arch: ${ARCH}
5
+ platform: linux
6
+ version: ${VERSION}
7
+ section: utils
8
+ priority: optional
9
+ maintainer: Ribose Inc. <open.source@ribose.com>
10
+ description: |
11
+ Install openly-licensed fonts on Windows, Linux and Mac.
12
+ Self-contained build: no Ruby installation is needed.
13
+ vendor: Ribose Inc.
14
+ homepage: https://www.fontist.org
15
+ license: BSD-2-Clause
16
+
17
+ # fontist clones its formula repository with git, and its git gem (~> 6.0)
18
+ # refuses a host git older than 2.43. Debian and Ubuntu version git with
19
+ # epoch 1 (1:2.43.0-1), RPM distros do not, hence one dependency per packager.
20
+ overrides:
21
+ deb:
22
+ depends:
23
+ - git (>= 1:2.43)
24
+ rpm:
25
+ depends:
26
+ - git >= 2.43
27
+
28
+ contents:
29
+ - src: ${BINARY}
30
+ dst: /usr/bin/fontist
31
+ expand: true # nfpm leaves ${BINARY} in src alone without this
32
+ file_info:
33
+ mode: 0755
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Entry point of the tebako-packed fontist (copied to bin/fontist by
5
+ # script/tebako/stage.sh).
6
+ #
7
+ # tebako installs the locked dependencies as gems inside the package; only
8
+ # fontist itself sits at a path, so it goes on the load path directly.
9
+ # bundler/setup is not used: bundler chdirs into path gems, and the packed
10
+ # filesystem refuses that.
11
+ #
12
+ # Libraries loaded through FFI (yeptris, leptris, ...) are opened with the
13
+ # host's dlopen, which cannot see inside the package. Each one is copied
14
+ # once to a host cache keyed by its content, and FFI is handed that copy.
15
+
16
+ # The packed gems are the only gems this program may see. rubygems also
17
+ # searches the host's GEM_HOME, GEM_PATH and user gem dir (~/.gem/ruby/<abi>),
18
+ # so a Ruby manager such as rvm, or a `gem install --user-install`, would
19
+ # shadow the locked gems. Pin rubygems to the package's gem dir before
20
+ # anything is required.
21
+ packed_gem_dir = Gem.default_dir
22
+ ENV.delete("GEM_HOME")
23
+ ENV.delete("GEM_PATH")
24
+ Gem.use_paths(packed_gem_dir, [packed_gem_dir])
25
+
26
+ require "digest"
27
+ require "fileutils"
28
+ require "ffi"
29
+
30
+ module FontistPackedNativeLibraries
31
+ PACKAGE_ROOT = "/__tfs__/"
32
+
33
+ def self.cache_root
34
+ home = ENV["FONTIST_PATH"] || File.join(Dir.home, ".fontist")
35
+ File.join(home, "tebako-native")
36
+ end
37
+
38
+ def self.host_path(name)
39
+ return name unless name.is_a?(String) && name.start_with?(PACKAGE_ROOT)
40
+ return name unless File.file?(name)
41
+
42
+ bytes = File.binread(name)
43
+ dir = File.join(cache_root, Digest::SHA256.hexdigest(bytes)[0, 16])
44
+ target = File.join(dir, File.basename(name))
45
+ write_once(target, bytes) unless File.file?(target)
46
+ target
47
+ end
48
+
49
+ def self.write_once(target, bytes)
50
+ FileUtils.mkdir_p(File.dirname(target))
51
+ partial = "#{target}.#{Process.pid}.partial"
52
+ File.binwrite(partial, bytes)
53
+ File.chmod(0o755, partial)
54
+ File.rename(partial, target)
55
+ end
56
+
57
+ # An Array argument is a list of alternatives (the first that loads wins),
58
+ # so the nesting is kept: only the names inside are rewritten.
59
+ def self.rewrite(name)
60
+ name.is_a?(Array) ? name.map { |n| rewrite(n) } : host_path(name)
61
+ end
62
+
63
+ def ffi_lib(*names)
64
+ super(*names.map { |name| FontistPackedNativeLibraries.rewrite(name) })
65
+ end
66
+ end
67
+
68
+ FFI::Library.prepend(FontistPackedNativeLibraries)
69
+
70
+ # leptris also opens libleptris from its compiled extension, which consults
71
+ # LEPTRIS_LIB_PATH and then package paths the host cannot open. Two copies
72
+ # of the library loaded side by side crash it, so both loaders are pointed
73
+ # at the same host copy. yeptris reads YEPTRIS_LIB_PATH the same way.
74
+ {
75
+ "leptris" => %w[LEPTRIS_LIB_PATH libleptris],
76
+ "yeptris" => %w[YEPTRIS_LIB_PATH libyeptris],
77
+ }.each do |gem_name, (variable, basename)|
78
+ next if ENV[variable]
79
+
80
+ spec = Gem::Specification.find_all_by_name(gem_name).max_by(&:version)
81
+ next unless spec
82
+
83
+ library = Dir.glob(
84
+ File.join(spec.full_gem_path, "**", "#{basename}.{dylib,so,dll}"),
85
+ ).min
86
+ ENV[variable] = FontistPackedNativeLibraries.host_path(library) if library
87
+ end
88
+
89
+ $LOAD_PATH.unshift(File.expand_path("../vendor/fontist/lib", __dir__))
90
+ load File.expand_path("../vendor/fontist/exe/fontist", __dir__)
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env bash
2
+ # Install a fontist .deb or .rpm on a fresh distro, use it, then remove it.
3
+ # Runs inside a distro container (see .github/workflows/tebako-pack.yml).
4
+ #
5
+ # script/tebako/package-smoke.sh <package-dir>
6
+ set -euo pipefail
7
+
8
+ dir="${1:?usage: package-smoke.sh <package-dir>}"
9
+ # apt reads a bare relative path as a package name; give it an absolute one.
10
+ dir="$(cd "$dir" && pwd)"
11
+ here="$(cd "$(dirname "$0")" && pwd)"
12
+
13
+ if command -v apt-get >/dev/null; then
14
+ export DEBIAN_FRONTEND=noninteractive
15
+ apt-get update -qq
16
+ apt-get install -y -qq ca-certificates "$dir"/fontist_*.deb
17
+ remove() { apt-get remove -y -qq fontist; }
18
+ else
19
+ dnf install -y -q "$dir"/fontist-*.rpm
20
+ remove() { dnf remove -y -q fontist; }
21
+ fi
22
+
23
+ if command -v ruby >/dev/null; then
24
+ echo "package-smoke: this image has no business carrying ruby" >&2
25
+ exit 1
26
+ fi
27
+ "$here/smoke.sh" "$(command -v fontist)"
28
+ remove
29
+ if command -v fontist >/dev/null; then
30
+ echo "package-smoke: fontist still on PATH after removal" >&2
31
+ exit 1
32
+ fi
33
+ echo "package-smoke: ok"
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env bash
2
+ # Wrap a packed Linux fontist executable into .deb and .rpm packages.
3
+ #
4
+ # script/tebako/package.sh <amd64|arm64> <fontist-executable> <out-dir>
5
+ set -euo pipefail
6
+
7
+ usage="usage: package.sh <amd64|arm64> <fontist-executable> <out-dir>"
8
+ arch="${1:?$usage}"
9
+ binary="${2:?$usage}"
10
+ out="${3:?$usage}"
11
+
12
+ root="$(cd "$(dirname "$0")/../.." && pwd)"
13
+ version="$(sed -n 's/.*VERSION = "\(.*\)".*/\1/p' "$root/lib/fontist/version.rb")"
14
+
15
+ mkdir -p "$out"
16
+ BINARY="$(cd "$(dirname "$binary")" && pwd)/$(basename "$binary")"
17
+ export ARCH="$arch" VERSION="$version" BINARY
18
+
19
+ for packager in deb rpm; do
20
+ nfpm package --config "$root/packaging/nfpm.yaml" \
21
+ --packager "$packager" --target "$out"
22
+ done
23
+ ls -l "$out"
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env bash
2
+ # Press fontist into one self-contained executable with tebako v2.
3
+ #
4
+ # script/tebako/press.sh <tebako-triplet> <output-path>
5
+ #
6
+ # <tebako-triplet> is the tebako release asset suffix: linux-gnu-x86_64,
7
+ # linux-gnu-arm64, macos-arm64 or macos-x86_64. Needs gh, ruby and bundler.
8
+ # GH_TOKEN (or GITHUB_TOKEN) keeps tebako's runtime lookups clear of the
9
+ # anonymous GitHub API limit.
10
+ set -euo pipefail
11
+
12
+ triplet="${1:?usage: press.sh <tebako-triplet> <output-path>}"
13
+ output="${2:?usage: press.sh <tebako-triplet> <output-path>}"
14
+
15
+ tebako_version="${TEBAKO_VERSION:-2.8.68}"
16
+ ruby_version="${PACKAGE_RUBY_VERSION:-3.3.12}"
17
+ here="$(cd "$(dirname "$0")" && pwd)"
18
+ work="$(mktemp -d)"
19
+ trap 'rm -rf "$work"' EXIT
20
+
21
+ export GITHUB_TOKEN="${GITHUB_TOKEN:-${GH_TOKEN:-}}"
22
+
23
+ asset="tebako-${tebako_version}-${triplet}"
24
+ gh release download "v${tebako_version}" --repo tamatebako/tebako \
25
+ --pattern "${asset}" --pattern "${asset}.sha256" --dir "$work"
26
+ (cd "$work" && shasum -a 256 -c "${asset}.sha256")
27
+ chmod +x "$work/$asset"
28
+
29
+ stage="$("$here/stage.sh" "$work/stage")"
30
+
31
+ mkdir -p "$(dirname "$output")"
32
+ output="$(cd "$(dirname "$output")" && pwd)/$(basename "$output")"
33
+
34
+ # tebako appends a trailer to the executable: never strip the result.
35
+ "$work/$asset" press --root "$stage" --entry-point bin/fontist \
36
+ --output "$output" --mode self-contained -R "$ruby_version"
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env bash
2
+ # Check that a packed (or installed) fontist works on its own.
3
+ #
4
+ # script/tebako/smoke.sh <fontist-executable>
5
+ #
6
+ # Uses a throwaway FONTIST_PATH so it never touches a real ~/.fontist.
7
+ set -euo pipefail
8
+
9
+ fontist="${1:?usage: smoke.sh <fontist-executable>}"
10
+ font="${SMOKE_FONT:-Overpass}"
11
+
12
+ home="$(mktemp -d)"
13
+ trap 'rm -rf "$home"' EXIT
14
+ export FONTIST_PATH="$home"
15
+
16
+ "$fontist" version
17
+ "$fontist" install --accept-all-licenses "$font"
18
+
19
+ if ! find "$home/fonts" -type f | grep -q .; then
20
+ echo "smoke: no font file landed under $home/fonts" >&2
21
+ exit 1
22
+ fi
23
+ "$fontist" list "$font" | tee "$home/list.txt"
24
+ grep -qi "installed" "$home/list.txt"
25
+ echo "smoke: ok"
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env bash
2
+ # Build a Gemfile-rooted app around the fontist gem, the layout tebako v2
3
+ # presses (it refuses a root that is itself a gem source tree).
4
+ #
5
+ # script/tebako/stage.sh <stage-dir>
6
+ #
7
+ # The gem is built from this checkout, unpacked with a static gemspec (the
8
+ # repository gemspec lists files with `git ls-files`, which needs .git), and
9
+ # referenced by path, so the packed fontist is exactly this checkout's code.
10
+ set -euo pipefail
11
+
12
+ stage="${1:?usage: stage.sh <stage-dir>}"
13
+ root="$(cd "$(dirname "$0")/../.." && pwd)"
14
+
15
+ rm -rf "$stage"
16
+ mkdir -p "$stage/vendor" "$stage/bin"
17
+ stage="$(cd "$stage" && pwd)"
18
+
19
+ (cd "$root" && gem build fontist.gemspec --output "$stage/fontist.gem" >/dev/null)
20
+ # Unpacked directory naming differs across rubygems versions; take the one
21
+ # child of a fresh directory instead of guessing it.
22
+ unpacked="$(mktemp -d)"
23
+ gem unpack "$stage/fontist.gem" --target "$unpacked" >/dev/null
24
+ mv "$unpacked"/*/ "$stage/vendor/fontist"
25
+ rmdir "$unpacked"
26
+ gem spec "$stage/fontist.gem" --ruby > "$stage/vendor/fontist/fontist.gemspec"
27
+ rm "$stage/fontist.gem"
28
+
29
+ cat > "$stage/Gemfile" <<'GEMFILE'
30
+ source "https://rubygems.org"
31
+
32
+ gem "fontist", path: "vendor/fontist"
33
+ GEMFILE
34
+
35
+ cp "$root/script/tebako/entry.rb" "$stage/bin/fontist"
36
+ chmod +x "$stage/bin/fontist"
37
+
38
+ (cd "$stage" && bundle lock >/dev/null)
39
+ echo "$stage"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fontist
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.35
4
+ version: 3.0.37
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-11-05 00:00:00.000000000 Z
11
+ date: 2026-11-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: down
@@ -277,6 +277,7 @@ files:
277
277
  - docs/api/manifest.md
278
278
  - docs/cli/cache.md
279
279
  - docs/cli/config.md
280
+ - docs/cli/convert.md
280
281
  - docs/cli/create-formula.md
281
282
  - docs/cli/exit-codes.md
282
283
  - docs/cli/fontconfig.md
@@ -343,6 +344,7 @@ files:
343
344
  - lib/fontist/collection_file.rb
344
345
  - lib/fontist/config.rb
345
346
  - lib/fontist/config_cli.rb
347
+ - lib/fontist/convert_cli.rb
346
348
  - lib/fontist/css.rb
347
349
  - lib/fontist/css/google_family.rb
348
350
  - lib/fontist/css/local.rb
@@ -515,9 +517,16 @@ files:
515
517
  - lib/fontist/yaml/yeptris_adapter.rb
516
518
  - lib/fontist/yaml_backend.rb
517
519
  - lib/tasks/system_fonts.rake
520
+ - packaging/nfpm.yaml
518
521
  - script/capture_system_fonts
519
522
  - script/download_system_indexes
520
523
  - script/generate_windows_formulas.rb
524
+ - script/tebako/entry.rb
525
+ - script/tebako/package-smoke.sh
526
+ - script/tebako/package.sh
527
+ - script/tebako/press.sh
528
+ - script/tebako/smoke.sh
529
+ - script/tebako/stage.sh
521
530
  - script/validate_windows_fod_ci.rb
522
531
  homepage: https://github.com/fontist/fontist
523
532
  licenses: