Compare commits

..

2 Commits

Author SHA1 Message Date
David Adam
08cd59727b CI: use build_tools/check.sh in Cirrus CI
08b03a733a removed CMake from the Docker images used for the
Cirrus builds.

It might be better to use fish_run_tests.sh in the Docker image, but
that requires some context which I'm not sure is set up properly in
Cirrus.
2025-10-05 23:25:04 +08:00
David Adam
8e5046061d build_tools/check.sh: add support for FISH_TEST_MAX_CONCURRENCY 2025-10-05 23:00:11 +08:00
794 changed files with 38153 additions and 137269 deletions

View File

@@ -1,6 +1,7 @@
image: alpine/edge
packages:
- cargo
- clang17-libclang
- cmake
- ninja
- pcre2-dev
@@ -23,4 +24,4 @@ tasks:
ninja
- test: |
cd fish-shell/build
ninja fish_run_tests
ninja test

View File

@@ -20,4 +20,4 @@ tasks:
ninja
- test: |
cd fish/build
ninja fish_run_tests
ninja test

View File

@@ -5,10 +5,10 @@ packages:
- gettext
- gmake
- llvm
- terminfo-db
- ninja
- pcre2
- py311-pexpect
- py311-sphinx
- python
- rust
- tmux
@@ -27,4 +27,4 @@ tasks:
ninja
- test: |
cd fish-shell/build
ninja fish_run_tests
ninja test

View File

@@ -1,30 +0,0 @@
image: openbsd/latest
packages:
- cmake
- gcc
- gettext
- gmake
- llvm
- ninja
- pcre2
- py3-pexpect
- py3-sphinx
- python
- rust
- tmux
sources:
- https://github.com/fish-shell/fish-shell
tasks:
- build: |
cd fish-shell
mkdir build
cd build
cmake -GNinja .. \
-DCMAKE_INSTALL_PREFIX=/usr \
-DCMAKE_INSTALL_DATADIR=share \
-DCMAKE_INSTALL_DOCDIR=share/doc/fish \
-DCMAKE_INSTALL_SYSCONFDIR=/etc
ninja
- test: |
cd fish-shell/build
ninja fish_run_tests

View File

@@ -1,2 +1,8 @@
[alias]
xtask = "run --package xtask --"
# This file is _not_ included in the tarballs for now
# Binary builds on Linux packaging infrastructure need to overwrite it to make `cargo vendor` work
# Releases and development builds made using OBS/Launchpad will _not_ reflect the contents of this
# file
[resolver]
# Make cargo 1.84+ respect MSRV (rust-version in Cargo.toml)
incompatible-rust-versions = "fallback"

View File

@@ -8,28 +8,49 @@ linux_task:
container: &step
image: ghcr.io/krobelus/fish-ci/alpine:latest
memory: 4GB
- name: ubuntu-oldest-supported
- name: jammy
container:
<<: *step
image: ghcr.io/krobelus/fish-ci/ubuntu-oldest-supported:latest
image: ghcr.io/krobelus/fish-ci/jammy:latest
# - name: jammy-asan
# container:
# <<: *step
# image: ghcr.io/krobelus/fish-ci/jammy-asan:latest
# - name: focal-32bit
# container:
# <<: *step
# image: ghcr.io/krobelus/fish-ci/focal-32bit:latest
tests_script:
# cirrus at times gives us 32 procs and 2 GB of RAM
# Unrestriced parallelism results in OOM
- lscpu || true
- (cat /proc/meminfo | grep MemTotal) || true
- mkdir build && cd build
- FISH_TEST_MAX_CONCURRENCY=6 cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug ..
- ninja -j 6 fish
- ninja fish_run_tests
- FISH_TEST_MAX_CONCURRENCY=6 build_tools/check.sh
only_if: $CIRRUS_REPO_OWNER == 'fish-shell'
linux_arm_task:
matrix:
- name: focal-arm64
arm_container:
image: ghcr.io/fish-shell/fish-ci/focal-arm64
- name: jammy-armv7-32bit
arm_container:
image: ghcr.io/fish-shell/fish-ci/jammy-armv7-32bit
tests_script:
# cirrus at times gives us 32 procs and 2 GB of RAM
# Unrestriced parallelism results in OOM
- lscpu || true
- (cat /proc/meminfo | grep MemTotal) || true
- FISH_TEST_MAX_CONCURRENCY=6 build_tools/check.sh
# CI task disabled during RIIR transition
only_if: false && $CIRRUS_REPO_OWNER == 'fish-shell'
freebsd_task:
matrix:
- name: FreeBSD Stable
- name: FreeBSD 14
freebsd_instance:
image: freebsd-15-0-release-amd64-ufs # updatecli.d/cirrus-freebsd.yml
image: freebsd-14-3-release-amd64-ufs
tests_script:
- pkg update
- pkg install -y cmake-core devel/pcre2 devel/ninja gettext git-lite lang/rust misc/py-pexpect
# libclang.so is a required build dependency for rust-c++ ffi bridge
- pkg install -y llvm
@@ -40,10 +61,7 @@ freebsd_task:
- pw user add -n fish-user -s /bin/csh -d /home/fish-user
- mkdir -p /home/fish-user
- chown -R fish-user /home/fish-user
- mkdir build && cd build
- chown -R fish-user ..
- chown -R fish-user .
- sudo -u fish-user -s whoami
- sudo -u fish-user -s FISH_TEST_MAX_CONCURRENCY=1 cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug ..
- sudo -u fish-user -s ninja -j 6 fish
- sudo -u fish-user -s ninja fish_run_tests
- sudo -u fish-user -s FISH_TEST_MAX_CONCURRENCY=1 build_tools/check.sh
only_if: $CIRRUS_REPO_OWNER == 'fish-shell'

View File

@@ -9,11 +9,12 @@ trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 100
[{Makefile,{BSD,GNU}makefile}]
[{Makefile,*.in}]
indent_style = tab
[*.{md,rst}]
max_line_length = unset
trim_trailing_whitespace = false
[*.sh]
indent_size = 4
@@ -21,7 +22,7 @@ indent_size = 4
[build_tools/release.sh]
max_line_length = 72
[{Dockerfile,Vagrantfile}]
[Dockerfile]
indent_size = 2
[share/{completions,functions}/**.fish]
@@ -29,6 +30,3 @@ max_line_length = unset
[{COMMIT_EDITMSG,git-revise-todo,*.jjdescription}]
max_line_length = 72
[*.yml]
indent_size = 2

5
.gitattributes vendored
View File

@@ -14,10 +14,15 @@
.gitattributes export-ignore
.gitignore export-ignore
/build_tools/make_tarball.sh export-ignore
/debian export-ignore
/debian/* export-ignore
/.github export-ignore
/.github/* export-ignore
/.builds export-ignore
/.builds/* export-ignore
# to make cargo vendor work correctly
/.cargo/ export-ignore
/.cargo/config.toml export-ignore
# for linguist, which drives GitHub's language statistics
alpine.js linguist-vendored

2
.github/FUNDING.yml vendored
View File

@@ -1,2 +0,0 @@
github:
- krobelus

View File

@@ -1,8 +1,11 @@
## Description
Talk about your changes here.
Fixes issue #
## TODOs:
<!-- Check off what what has been done so far. -->
- [ ] If addressing an issue, a commit message mentions `Fixes issue #<issue-number>`
<!-- Just check off what what we know been done so far. We can help you with this stuff. -->
- [ ] Changes to fish usage are reflected in user documentation/manpages.
- [ ] Tests have been added for regressions fixed
- [ ] User-visible changes noted in CHANGELOG.rst <!-- Usually skipped for changes to completions -->
- [ ] User-visible changes noted in CHANGELOG.rst <!-- Don't document changes for completions inside CHANGELOG.rst, there are lot of such edits -->

View File

@@ -1,41 +0,0 @@
name: Install dependencies for system tests
inputs:
include_sphinx:
description: Whether to install Sphinx
required: true
default: false
include_pcre:
description: Whether to install the PCRE library
required: false
default: true
permissions:
contents: read
runs:
using: "composite"
steps:
- shell: bash
env:
include_pcre: ${{ inputs.include_pcre }}
run: |
set -x
: "optional dependencies"
sudo apt install \
gettext \
$(if $include_pcre; then echo libpcre2-dev; fi) \
;
: "system test dependencies"
sudo apt install \
diffutils $(: "for diff") \
git \
gettext \
less \
$(if ${{ inputs.include_pcre }}; then echo libpcre2-dev; fi) \
python3-pexpect \
tmux \
wget \
;
- uses: ./.github/actions/install-sphinx
if: ${{ inputs.include_sphinx == 'true' }}

View File

@@ -0,0 +1,14 @@
name: Install sphinx-markdown-builder
permissions:
contents: read
runs:
using: "composite"
steps:
- shell: bash
run: |
set -x
commit=b259de1dc97573a71470a1d71c3d83535934136b
pip install git+https://github.com/krobelus/sphinx-markdown-builder@"$commit"
python -c 'import sphinx_markdown_builder'

View File

@@ -1,23 +0,0 @@
name: Install sphinx
permissions:
contents: read
runs:
using: "composite"
steps:
- shell: bash
run: |
set -x
sudo pip install uv --break-system-packages
command -v uv
command -v uvx
# Check that pyproject.toml and the lock file are in sync.
# TODO Use "uv" to install Python as well.
: 'Note that --no-managed-python below would be implied but be explicit'
uv='env UV_PYTHON=python uv --no-managed-python'
$uv lock --check --exclude-newer="$(awk -F'"' <uv.lock '/^exclude-newer[[:space:]]*=/ {print $2}')"
# Install globally.
sudo $uv pip install --group=dev --system --break-system-packages
# Smoke test.
python -c 'import sphinx; import sphinx_markdown_builder'

View File

@@ -1,41 +0,0 @@
name: Rust Toolchain
inputs:
toolchain_channel:
description: Either "stable" or "msrv"
required: true
targets:
description: Comma-separated list of target triples to install for this toolchain
required: false
components:
description: Comma-separated list of components to be additionally installed
required: false
permissions:
contents: read
runs:
using: "composite"
steps:
- name: Set toolchain
env:
toolchain_channel: ${{ inputs.toolchain_channel }}
shell: bash
run: |
set -x
toolchain=$(
case "$toolchain_channel" in
(stable) echo 1.93 ;; # updatecli.d/rust.yml
(msrv) echo 1.85 ;; # updatecli.d/rust.yml
(*)
printf >&2 "error: unsupported toolchain channel %s" "$toolchain_channel"
exit 1
;;
esac
)
printf 'TOOLCHAIN=%s\n' "$toolchain" >>"$GITHUB_ENV"
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ env.TOOLCHAIN }}
targets: ${{ inputs.targets }}
components: ${{ inputs.components }}

View File

@@ -14,8 +14,7 @@ permissions:
runs:
using: "composite"
steps:
- uses: ./.github/actions/rust-toolchain
- uses: dtolnay/rust-toolchain@1.70
with:
toolchain_channel: "msrv"
targets: ${{ inputs.targets }}
components: ${{ inputs.components }}
components: ${{ inputs.components}}

View File

@@ -14,8 +14,7 @@ permissions:
runs:
using: "composite"
steps:
- uses: ./.github/actions/rust-toolchain
- uses: dtolnay/rust-toolchain@1.90
with:
toolchain_channel: "stable"
targets: ${{ inputs.targets }}
components: ${{ inputs.components }}

View File

@@ -10,7 +10,7 @@ jobs:
steps:
- name: Set label and milestone
id: set-label-milestone
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8, build_tools/update-dependencies.sh
uses: actions/github-script@v7
with:
script: |
const completionsLabel = 'completions';

View File

@@ -1,64 +0,0 @@
name: Build Docker test images
on:
push:
branches:
- master
paths:
- 'docker/**'
workflow_dispatch:
concurrency:
group: docker-builds
env:
REGISTRY: ghcr.io
NAMESPACE: fish-ci
jobs:
docker-build:
if: github.repository_owner == 'fish-shell'
permissions:
contents: read
packages: write
attestations: write
id-token: write
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: alpine
- os: ubuntu-latest
target: ubuntu-oldest-supported
runs-on: ${{ matrix.os }}
steps:
-
name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2, build_tools/update-dependencies.sh
-
name: Login to Container registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0, build_tools/update-dependencies.sh
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
-
name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0, build_tools/update-dependencies.sh
with:
images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.NAMESPACE }}/${{ matrix.target }}
flavor: |
latest=true
-
name: Build and push
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0, build_tools/update-dependencies.sh
with:
context: docker/context
push: true
file: docker/${{ matrix.target }}.Dockerfile
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -1,24 +0,0 @@
name: Lint Dependencies
on:
push:
paths:
- '.github/workflows/lint-dependencies.yml'
- 'Cargo.lock'
- '**/Cargo.toml'
- 'deny.toml'
pull_request:
paths:
- '.github/workflows/lint-dependencies.yml'
- 'Cargo.lock'
- '**/Cargo.toml'
- 'deny.toml'
jobs:
cargo-deny:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2, build_tools/update-dependencies.sh
- uses: EmbarkStudios/cargo-deny-action@44db170f6a7d12a6e90340e9e0fca1f650d34b14 # v2.0.15, build_tools/update-dependencies.sh
with:
command: check licenses
arguments: --all-features --locked --exclude-dev
rust-version: 1.93 # updatecli.d/rust.yml

View File

@@ -1,66 +0,0 @@
name: Lint
on: [push, pull_request]
permissions:
contents: read
jobs:
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2, build_tools/update-dependencies.sh
- uses: ./.github/actions/rust-toolchain@stable
with:
components: rustfmt
- name: install dependencies
run: pip install ruff
- name: build fish
run: cargo build
- name: check format
run: PATH="target/debug:$PATH" build_tools/style.fish --all --check
- name: check rustfmt
run: find build.rs crates src -type f -name '*.rs' | xargs rustfmt --check
clippy:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- rust_version: "stable"
features: ""
- rust_version: "stable"
features: "--no-default-features"
- rust_version: "msrv"
features: ""
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2, build_tools/update-dependencies.sh
- uses: ./.github/actions/rust-toolchain
with:
toolchain_channel: ${{ matrix.rust_version }}
components: clippy
- name: Update package database
run: sudo apt-get update
- name: Install deps
run: |
sudo apt install gettext
- name: cargo clippy
run: cargo clippy --workspace --all-targets ${{ matrix.features }} -- --deny=warnings
rustdoc:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2, build_tools/update-dependencies.sh
- uses: ./.github/actions/rust-toolchain@stable
- name: Update package database
run: sudo apt-get update
- name: Install deps
run: |
sudo apt install gettext
- name: cargo doc
run: |
RUSTDOCFLAGS='-D warnings' cargo doc --workspace
- name: cargo doctest
run: |
cargo test --doc --workspace

View File

@@ -12,13 +12,12 @@ permissions:
jobs:
lock:
if: github.repository_owner == 'fish-shell'
permissions:
issues: write # for dessant/lock-threads to lock issues
pull-requests: write # for dessant/lock-threads to lock PRs
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@f5f995c727ac99a91dec92781a8e34e7c839a65e # v6.0.0, build_tools/update-dependencies.sh
- uses: dessant/lock-threads@v4
with:
github-token: ${{ github.token }}
issue-inactive-days: '365'

View File

@@ -1,4 +1,4 @@
name: Test
name: make fish_run_tests
on: [push, pull_request]
@@ -11,17 +11,18 @@ permissions:
jobs:
ubuntu:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2, build_tools/update-dependencies.sh
- uses: actions/checkout@v4
- uses: ./.github/actions/rust-toolchain@oldest-supported
- name: Install deps
uses: ./.github/actions/install-dependencies
with:
include_sphinx: true
- name: Generate a locale that uses a comma as decimal separator.
run: |
sudo apt install gettext libpcre2-dev python3-pexpect python3-sphinx tmux
# Generate a locale that uses a comma as decimal separator.
sudo locale-gen fr_FR.UTF-8
- uses: ./.github/actions/install-sphinx-markdown-builder
- name: cmake
run: |
mkdir build && cd build
@@ -42,21 +43,18 @@ jobs:
git --no-pager diff --exit-code || { echo 'There are uncommitted changes after regenerating the gettext PO files. Make sure to update them via `build_tools/update_translations.fish` after changing source files.'; exit 1; }
ubuntu-32bit-static-pcre2:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2, build_tools/update-dependencies.sh
- uses: actions/checkout@v4
- uses: ./.github/actions/rust-toolchain@oldest-supported
with:
targets: "i586-unknown-linux-gnu"
- name: Update package database
run: sudo apt-get update
targets: "i586-unknown-linux-gnu" # rust-toolchain wants this comma-separated
- name: Install deps
uses: ./.github/actions/install-dependencies
with:
include_pcre: false
include_sphinx: false
- name: Install g++-multilib
run: sudo apt install g++-multilib
run: |
sudo apt update
sudo apt install gettext python3-pexpect g++-multilib tmux
- name: cmake
env:
CFLAGS: "-m32"
@@ -71,6 +69,7 @@ jobs:
make -C build VERBOSE=1 fish_run_tests
ubuntu-asan:
runs-on: ubuntu-latest
env:
# Rust has two different memory sanitizers of interest; they can't be used at the same time:
@@ -80,22 +79,18 @@ jobs:
#
RUSTFLAGS: "-Zsanitizer=address"
# RUSTFLAGS: "-Zsanitizer=memory -Zsanitizer-memory-track-origins"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2, build_tools/update-dependencies.sh
- uses: actions/checkout@v4
# All -Z options require running nightly
- uses: dtolnay/rust-toolchain@nightly
with:
# ASAN uses `cargo build -Zbuild-std` which requires the rust-src component
# this is comma-separated
components: rust-src
- name: Update package database
run: sudo apt-get update
- name: Install deps
uses: ./.github/actions/install-dependencies
with:
include_sphinx: false
- name: Install llvm
run: |
sudo apt install gettext libpcre2-dev python3-pexpect tmux
sudo apt install llvm # for llvm-symbolizer
- name: cmake
env:
@@ -124,14 +119,44 @@ jobs:
export LSAN_OPTIONS="$LSAN_OPTIONS:suppressions=$PWD/build_tools/lsan_suppressions.txt"
make -C build VERBOSE=1 fish_run_tests
# Our clang++ tsan builds are not recognizing safe rust patterns (such as the fact that Drop
# cannot be called while a thread is using the object in question). Rust has its own way of
# running TSAN, but for the duration of the port from C++ to Rust, we'll keep this disabled.
# ubuntu-threadsan:
#
# runs-on: ubuntu-latest
#
# steps:
# - uses: actions/checkout@v4
# - uses: ./.github/actions/rust-toolchain@oldest-supported
# - name: Install deps
# run: |
# sudo apt install gettext libpcre2-dev python3-pexpect tmux
# - name: cmake
# env:
# FISH_CI_SAN: 1
# CC: clang
# run: |
# mkdir build && cd build
# cmake ..
# - name: make
# run: |
# make
# - name: make fish_run_tests
# run: |
# make -C build fish_run_tests
macos:
runs-on: macos-latest
env:
# macOS runners keep having issues loading Cargo.toml dependencies from git (GitHub) instead
# of crates.io, so give this a try. It's also sometimes significantly faster on all platforms.
CARGO_NET_GIT_FETCH_WITH_CLI: true
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2, build_tools/update-dependencies.sh
- uses: actions/checkout@v4
- uses: ./.github/actions/rust-toolchain@oldest-supported
- name: Install deps
run: |
@@ -139,41 +164,13 @@ jobs:
# this is CI so we don't actually care.
sudo pip3 install --break-system-packages pexpect
brew install gettext tmux
- uses: ./.github/actions/install-sphinx
- name: cmake
run: |
mkdir build && cd build
FISH_TEST_MAX_CONCURRENCY=1 \
cmake -DCMAKE_BUILD_TYPE=Debug ..
cmake -DCMAKE_BUILD_TYPE=Debug ..
- name: make
run: |
make -C build VERBOSE=1
- name: make fish_run_tests
run: |
make -C build VERBOSE=1 fish_run_tests
windows:
runs-on: windows-latest
defaults:
run:
shell: msys2 {0}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2, build_tools/update-dependencies.sh
- uses: msys2/setup-msys2@4f806de0a5a7294ffabaff804b38a9b435a73bda # v2.30.0, build_tools/update-dependencies.sh
with:
update: true
msystem: MSYS
- name: Install deps
# Not using setup-msys2 `install` option to make it easier to copy/paste
run: |
pacman --noconfirm -S --needed git rust
- name: cargo build
run: |
cargo build
- name: smoketest
# We can't run `build_tools/check.sh` yet, there are just too many failures
# so this is just a quick check to make sure that fish can swim
run: |
set -x
[ "$(target/debug/fish.exe -c 'echo (math 1 + 1)')" = 2 ]
cargo test

View File

@@ -16,7 +16,7 @@ jobs:
name: Pre-release checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2, build_tools/update-dependencies.sh
- uses: actions/checkout@v4
with:
# Workaround for https://github.com/actions/checkout/issues/882
ref: ${{ inputs.version }}
@@ -36,15 +36,13 @@ jobs:
version: ${{ steps.version.outputs.version }}
tarball-name: ${{ steps.version.outputs.tarball-name }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2, build_tools/update-dependencies.sh
- uses: actions/checkout@v4
with:
# Workaround for https://github.com/actions/checkout/issues/882
ref: ${{ inputs.version }}
- name: Update package database
run: sudo apt-get update
- name: Install dependencies
run: sudo apt install cmake gettext ninja-build python3-pip
- uses: ./.github/actions/install-sphinx
run: sudo apt install cmake gettext ninja-build python3-pip python3-sphinx
- uses: ./.github/actions/install-sphinx-markdown-builder
- name: Create tarball
run: |
set -x
@@ -54,16 +52,13 @@ jobs:
# Need history since the last release (i.e. tag) for stats.
git fetch --tags
git fetch --unshallow
gpg_public_key_url=https://github.com/${{ github.actor }}.gpg
curl -sS "$gpg_public_key_url" | grep 'PGP PUBLIC KEY BLOCK' -A5
FISH_GPG_PUBLIC_KEY_URL=$gpg_public_key_url \
sh -x ./build_tools/release-notes.sh >"$relnotes"
sh -x ./build_tools/release-notes.sh >"$relnotes"
# Delete title
sed -n 1p "$relnotes" | grep -q "^## fish .*"
sed -n 2p "$relnotes" | grep -q '^$'
sed -i 1,2d "$relnotes"
- name: Upload tarball artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0, build_tools/update-dependencies.sh
uses: actions/upload-artifact@v4
with:
name: source-tarball
path: |
@@ -73,10 +68,10 @@ jobs:
packages-for-linux:
needs: [is-release-tag]
name: Build single-file fish for Linux
name: Build single-file fish for Linux (experimental)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2, build_tools/update-dependencies.sh
- uses: actions/checkout@v4
with:
# Workaround for https://github.com/actions/checkout/issues/882
ref: ${{ inputs.version }}
@@ -84,11 +79,8 @@ jobs:
uses: ./.github/actions/rust-toolchain@stable
with:
targets: x86_64-unknown-linux-musl,aarch64-unknown-linux-musl
- name: Update package database
run: sudo apt-get update
- name: Install dependencies
run: sudo apt install crossbuild-essential-arm64 gettext musl-tools
- uses: ./.github/actions/install-sphinx
run: sudo apt install crossbuild-essential-arm64 gettext musl-tools python3-sphinx
- name: Build statically-linked executables
run: |
set -x
@@ -104,7 +96,7 @@ jobs:
tar -cazf fish-$(git describe)-linux-$arch.tar.xz \
-C target/$arch-unknown-linux-musl/release fish
done
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0, build_tools/update-dependencies.sh
- uses: actions/upload-artifact@v4
with:
name: Static builds for Linux
path: fish-${{ inputs.version }}-linux-*.tar.xz
@@ -118,19 +110,19 @@ jobs:
name: Create release draft
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2, build_tools/update-dependencies.sh
- uses: actions/checkout@v4
with:
# Workaround for https://github.com/actions/checkout/issues/882
ref: ${{ inputs.version }}
- name: Download all artifacts
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0, build_tools/update-dependencies.sh
uses: actions/download-artifact@v4
with:
merge-multiple: true
path: /tmp/artifacts
- name: List artifacts
run: find /tmp/artifacts -type f
- name: Create draft release
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0, build_tools/update-dependencies.sh
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ inputs.version }}
name: fish ${{ inputs.version }}
@@ -146,17 +138,20 @@ jobs:
runs-on: macos-latest
environment: macos-codesign
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2, build_tools/update-dependencies.sh
- uses: actions/checkout@v4
with:
# Workaround for https://github.com/actions/checkout/issues/882
ref: ${{ inputs.version }}
- name: Install Rust
uses: ./.github/actions/rust-toolchain@oldest-supported
with:
targets: x86_64-apple-darwin
- name: Install Rust Stable
uses: ./.github/actions/rust-toolchain@stable
with:
targets: aarch64-apple-darwin,x86_64-apple-darwin
targets: aarch64-apple-darwin
- name: Install dependencies
run: brew install gettext
- uses: ./.github/actions/install-sphinx
- name: Build and codesign
run: |
die() { echo >&2 "$*"; exit 1; }
@@ -176,7 +171,7 @@ jobs:
echo "$MACOS_NOTARIZE_JSON" >/tmp/notarize.json
./build_tools/make_macos_pkg.sh -s -f /tmp/app.p12 \
-i /tmp/installer.p12 -p "$MAC_CODESIGN_PASSWORD" \
-n -j /tmp/notarize.json -- -c "-DWITH_DOCS=ON"
-n -j /tmp/notarize.json
version=$(git describe)
[ -f "${FISH_ARTEFACT_PATH}/fish-$version.app.zip" ]
[ -f "${FISH_ARTEFACT_PATH}/fish-$version.pkg" ]

61
.github/workflows/rust_checks.yml vendored Normal file
View File

@@ -0,0 +1,61 @@
name: Rust checks
on: [push, pull_request]
permissions:
contents: read
jobs:
rustfmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/rust-toolchain@stable
with:
components: rustfmt
- name: cargo fmt
run: cargo fmt --check
clippy-stable:
runs-on: ubuntu-latest
strategy:
matrix:
features: ["", "--no-default-features"]
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/rust-toolchain@stable
with:
components: clippy
- name: Install deps
run: |
sudo apt install gettext
- name: cargo clippy
run: cargo clippy --workspace --all-targets ${{ matrix.features }} -- --deny=warnings
clippy-msrv:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/rust-toolchain@oldest-supported
with:
components: clippy
- name: Install deps
run: |
sudo apt install gettext
- name: cargo clippy
run: cargo clippy --workspace --all-targets -- --deny=warnings
rustdoc:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/rust-toolchain@stable
- name: Install deps
run: |
sudo apt install gettext
- name: cargo doc
run: |
RUSTDOCFLAGS='-D warnings' cargo doc --workspace
- name: cargo doctest
run: |
cargo test --doc --workspace

4
.gitignore vendored
View File

@@ -7,6 +7,7 @@
*.DS_Store
*.a
*.app
*.d
*.dll
*.dylib
*.exe
@@ -104,6 +105,3 @@ target/
# JetBrains editors.
.idea/
# AI slop
.claude/

View File

@@ -1 +0,0 @@
edition = "2024"

View File

@@ -1,3 +0,0 @@
# Coding style
- Use comments sparingly. Don't explain what the code is doing, rather explain why.

View File

@@ -1,249 +1,16 @@
fish 4.5.0 (released February 17, 2026)
=======================================
fish ?.?.? (released ???)
=========================
This is mostly a patch release for Vi mode regressions in 4.4.0 but other minor behavior changes are included as well.
Interactive improvements
------------------------
- :kbd:`ctrl-l` no longer cancels history search (:issue:`12436`).
- History search cursor positioning now works correctly with characters of arbitrary width.
Deprecations and removed features
---------------------------------
- fish no longer reads the terminfo database to alter behaviour based on the :envvar:`TERM` environment variable, and does not depend on ncurses or terminfo. The ``ignore-terminfo`` feature flag, introduced and enabled by default in fish 4.1, is now permanently enabled. fish may no longer work correctly on Data General Dasher D220 and Wyse WY-350 terminals, but should continue to work on all known terminal emulators released in the 21st century.
Regression fixes:
-----------------
- (from 4.4.0) Vi mode ``d,f`` key binding did not work (:issue:`12417`).
- (from 4.4.0) Vi mode ``c,w`` key binding wrongly deleted trailing spaces (:issue:`12443`).
- (from 4.4.0) Vi mode crash on ``c,i,w`` after accepting autosuggestion (:issue:`12430`).
- (from 4.4.0) ``fish_vi_key_bindings`` called with a mode argument produced an error (:issue:`12413`).
- (from 4.0.0) Build on Illumos (:issue:`12410`).
fish 4.4.0 (released February 03, 2026)
=======================================
Deprecations and removed features
---------------------------------
- The default fossil prompt has been disabled (:issue:`12342`).
Interactive improvements
------------------------
- The ``bind`` builtin lists mappings from all modes if ``--mode`` is not provided (:issue:`12214`).
- Line-wise autosuggestions that don't start a command are no longer shown (739b82c34db, 58e7a50de8a).
- Builtin ``history`` now assumes that :envvar:`PAGER` supports ANSI color sequences.
- fish now clears the terminal's ``FLUSHO`` flag when acquiring control of the terminal, to fix an issue caused by pressing :kbd:`ctrl-o` on macOS (:issue:`12304`).
New or improved bindings
------------------------
- Vi mode word movements (``w``, ``W``, ``e``, and ``E``) are now largely in line with Vim. The only exception is that underscores are treated as word separators (:issue:`12269`).
- New special input functions to support these movements: ``forward-word-vi``, ``kill-word-vi``, ``forward-bigword-vi``, ``kill-bigword-vi``, ``forward-word-end``, ``backward-word-end``, ``forward-bigword-end``, ``backward-bigword-end``, ``kill-a-word``, ``kill-inner-word``, ``kill-a-bigword``, and ``kill-inner-bigword``.
- Vi mode key bindings now support counts for movement and deletion commands (e.g. `d3w` or `3l`), via a new operator mode (:issue:`2192`).
- New ``catpuccin-*`` color themes.
Improved terminal support
-------------------------
- ``set_color`` learned the strikethrough (``--strikethrough`` or ``-s``) modifier.
For distributors and developers
-------------------------------
- The CMake option ``WITH_GETTEXT`` has been renamed to ``WITH_MESSAGE_LOCALIZATION``, to reflect that it toggles localization independently of the backend used in the implementation.
- New ``cargo xtask`` commands can replace some CMake workflows.
Regression fixes:
-----------------
- (from 4.1.0) Crash when autosuggesting Unicode characters with nontrivial lowercase mapping (:issue:`12326`, 78f4541116e).
- (from 4.3.0) Glitch on ``read --prompt-str ""`` (:issue:`12296`).
fish 4.3.3 (released January 07, 2026)
======================================
This release fixes the following problems identified in fish 4.3.0:
- Selecting a completion could insert only part of the token (:issue:`12249`).
- Glitch with soft-wrapped autosuggestions and :doc:`fish_right_prompt <cmds/fish_right_prompt>` (:issue:`12255`).
- Spurious echo in tmux when typing a command really fast (:issue:`12261`).
- ``tomorrow`` theme always using the light variant (:issue:`12266`).
- ``fish_config theme choose`` sometimes not shadowing themes set by e.g. webconfig (:issue:`12278`).
- The sample prompts and themes are correctly installed (:issue:`12241`).
- Last line of command output could be hidden when missing newline (:issue:`12246`).
Other improvements include:
- The ``abbr``, ``bind``, ``complete``, ``functions``, ``history`` and ``type`` commands now support a ``--color`` option to control syntax highlighting in their output. Valid values are ``auto`` (default), ``always``, or ``never``.
- Existing file paths in redirection targets such as ``> file.txt`` are now highlighted using :envvar:`fish_color_valid_path`, indicating that ``file.txt`` will be clobbered (:issue:`12260`).
fish 4.3.2 (released December 30, 2025)
=======================================
This release fixes the following problems identified in 4.3.0:
- Pre-built macOS packages failed to start due to a ``Malformed Mach-O file`` error (:issue:`12224`).
- ``extra_functionsdir`` (usually ``vendor_functions.d``) and friends were not used (:issue:`12226`).
- Sample config file ``~/.config/fish/config.fish/`` and config directories ``~/.config/fish/conf.d/``, ``~/.config/fish/completions/`` and ``~/.config/fish/functions/`` were recreated on every startup instead of only the first time fish runs on a system (:issue:`12230`).
- Spurious echo of ``^[[I`` in some scenarios (:issue:`12232`).
- Infinite prompt redraw loop on some prompts (:issue:`12233`).
- The removal of pre-built HTML docs from tarballs revealed that cross compilation is broken because we use ``${CMAKE_BINARY_DIR}/fish_indent`` for building HTML docs.
As a workaround, the new CMake build option ``FISH_INDENT_FOR_BUILDING_DOCS`` can be set to the path of a runnable ``fish_indent`` binary.
fish 4.3.1 (released December 28, 2025)
=======================================
This release fixes the following problem identified in 4.3.0:
- Possible crash after expanding an abbreviation (:issue:`12223`).
fish 4.3.0 (released December 28, 2025)
=======================================
Deprecations and removed features
---------------------------------
- fish no longer sets user-facing :ref:`universal variables <variables-universal>` by default, making the configuration easier to understand.
Specifically, the ``fish_color_*``, ``fish_pager_color_*`` and ``fish_key_bindings`` variables are now set in the global scope by default.
After upgrading to 4.3.0, fish will (once and never again) migrate these universals to globals set at startup in the
``~/.config/fish/conf.d/fish_frozen_theme.fish`` and
``~/.config/fish/conf.d/fish_frozen_key_bindings.fish`` files.
We suggest that you delete those files and :ref:`set your theme <syntax-highlighting>` in ``~/.config/fish/config.fish``.
- You can still configure fish to propagate theme changes instantly; see :ref:`here <syntax-highlighting-instant-update>` for an example.
- You can still opt into storing color variables in the universal scope
via ``fish_config theme save`` though unlike ``fish_config theme choose``,
it does not support dynamic theme switching based on the terminal's color theme (see below).
- In addition to setting the variables which are explicitly defined in the given theme,
``fish_config theme choose`` now clears only color variables that were set by earlier invocations of a ``fish_config theme choose`` command
(which is how fish's default theme is set).
Scripting improvements
----------------------
- New :ref:`status language <status-language>` command allows showing and modifying language settings for fish messages without having to modify environment variables.
- When using a noninteractive fish instance to compute completions, ``commandline --cursor`` works as expected instead of throwing an error (:issue:`11993`).
- :envvar:`fish_trace` can now be set to ``all`` to also trace execution of key bindings, event handlers as well as prompt and title functions.
Interactive improvements
------------------------
- When typing immediately after starting fish, the first prompt is now rendered correctly.
- Completion accuracy was improved for file paths containing ``=`` or ``:`` (:issue:`5363`).
- Prefix-matching completions are now shown even if they don't match the case typed by the user (:issue:`7944`).
- On Cygwin/MSYS, command name completion will favor the non-exe name (``foo``) unless the user started typing the extension.
- When using the exe name (``foo.exe``), fish will use the description and completions for ``foo`` if there are none for ``foo.exe``.
- Autosuggestions now also show soft-wrapped portions (:issue:`12045`).
New or improved bindings
------------------------
- :kbd:`ctrl-w` (``backward-kill-path-component``) also deletes escaped spaces (:issue:`2016`).
- New special input functions ``backward-path-component``, ``forward-path-component`` and ``kill-path-component`` (:issue:`12127`).
Improved terminal support
-------------------------
- Themes can now be made color-theme-aware by including both ``[light]`` and ``[dark]`` sections in the :ref:`theme file <fish-config-theme-files>`.
Some default themes have been made color-theme-aware, meaning they dynamically adjust as your terminal's background color switches between light and dark colors (:issue:`11580`).
- The working directory is now reported on every fresh prompt (via OSC 7), fixing scenarios where a child process (like ``ssh``) left behind a stale working directory (:issue:`12191`).
- OSC 133 prompt markers now also mark the prompt end, which improves shell integration with terminals like iTerm2 (:issue:`11837`).
- Operating-system-specific key bindings are now decided based on the :ref:`terminal's host OS <status-terminal-os>`.
- New :ref:`feature flag <featureflags>` ``omit-term-workarounds`` can be turned on to prevent fish from trying to work around some incompatible terminals.
For distributors and developers
-------------------------------
- Tarballs no longer contain prebuilt documentation,
so building and installing documentation requires Sphinx.
To avoid users accidentally losing docs, the ``BUILD_DOCS`` and ``INSTALL_DOCS`` configuration options have been replaced with a new ``WITH_DOCS`` option.
- ``fish_key_reader`` and ``fish_indent`` are now installed as hardlinks to ``fish``, to save some space.
Regression fixes:
-----------------
- (from 4.1.0) Crash on incorrectly-set color variables (:issue:`12078`).
- (from 4.1.0) Crash when autosuggesting Unicode characters with nontrivial lowercase mapping.
- (from 4.2.0) Incorrect emoji width computation on macOS.
- (from 4.2.0) Mouse clicks and :kbd:`ctrl-l` edge cases in multiline command lines (:issue:`12121`).
- (from 4.2.0) Completions for Git remote names on some non-glibc systems.
- (from 4.2.0) Expansion of ``~$USER``.
fish 4.2.1 (released November 13, 2025)
=======================================
This release fixes the following problems identified in 4.2.0:
- When building from a tarball without Sphinx (that is, with ``-DBUILD_DOCS=OFF`` or when ``sphinx-build`` is not found),
builtin man pages and help files were missing, which has been fixed (:issue:`12052`).
- ``fish_config``'s theme selector (the "colors" tab) was broken, which has been fixed (:issue:`12053`).
fish 4.2.0 (released November 10, 2025)
=======================================
Notable improvements and fixes
------------------------------
- History-based autosuggestions now include multi-line commands.
- A :ref:`transient prompt <transient-prompt>` containing more lines than the final prompt will now be cleared properly (:issue:`11875`).
- Taiwanese Chinese translations have been added.
- French translations have been supplemented (:issue:`11842`).
Deprecations and removed features
---------------------------------
- fish now assumes UTF-8 for character encoding even if the system does not have a UTF-8 locale.
Input bytes which are not valid UTF-8 are still round-tripped correctly.
For example, file paths using legacy encodings can still be used,
but may be rendered differently on the command line.
- On systems where no multi-byte locale is available,
fish will no longer fall back to using ASCII replacements for :ref:`Unicode characters <term-compat-unicode-codepoints>` such as "…".
Interactive improvements
------------------------
- The title of the terminal tab can now be set separately from the window title by defining the :doc:`fish_tab_title <cmds/fish_tab_title>` function (:issue:`2692`).
- fish now hides the portion of a multiline prompt that is scrolled out of view due to a huge command line. This prevents duplicate lines after repainting with partially visible prompt (:issue:`11911`).
- :doc:`fish_config prompt <cmds/fish_config>`'s ``choose`` and ``save`` subcommands have been taught to reset :doc:`fish_mode_prompt <cmds/fish_mode_prompt>` in addition to the other prompt functions (:issue:`11937`).
- fish no longer force-disables mouse capture (DECSET/DECRST 1000),
so you can use those commands
to let mouse clicks move the cursor or select completions items (:issue:`4918`).
- The :kbd:`alt-p` binding no longer adds a redundant space to the command line.
- When run as a login shell on macOS, fish now sets :envvar:`MANPATH` correctly when that variable was already present in the environment (:issue:`10684`).
- A Windows-specific case of the :doc:`web-based config <cmds/fish_config>` failing to launch has been fixed (:issue:`11805`).
- A MSYS2-specific workaround for Konsole and WezTerm has been added,
to prevent them from using the wrong working directory when opening new tabs (:issue:`11981`).
For distributors and developers
-------------------------------
- Release tags and source code tarballs are GPG-signed again (:issue:`11996`).
- Documentation in release tarballs is now built with the latest version of Sphinx,
which means that pre-built man pages include :ref:`OSC 8 hyperlinks <term-compat-osc-8>`.
- The Sphinx dependency is now specified in ``pyproject.toml``,
which allows you to use `uv <https://github.com/astral-sh/uv>`__ to provide Sphinx for building documentation (e.g. ``uv run cargo install --path .``).
- The minimum supported Rust version (MSRV) has been updated to 1.85.
- The standalone build mode has been made the default.
This means that the files in ``$CMAKE_INSTALL_PREFIX/share/fish`` will not be used anymore, except for HTML docs.
As a result, future upgrades will no longer break running shells
if one of fish's internal helper functions has been changed in the updated version.
For now, the data files are still installed redundantly,
to prevent upgrades from breaking already-running shells (:issue:`11921`).
To reverse this change (which should not be necessary),
patch out the ``embed-data`` feature from ``cmake/Rust.cmake``.
This option will be removed in future.
- OpenBSD 7.8 revealed an issue with fish's approach for displaying builtin man pages, which has been fixed.
Regression fixes:
-----------------
- (from 4.1.0) Fix the :doc:`web-based config <cmds/fish_config>` for Python 3.9 and older (:issue:`12039`).
- (from 4.1.0) Correct wrong terminal modes set by ``fish -c 'read; cat`` (:issue:`12024`).
- (from 4.1.0) On VTE-based terminals, stop redrawing the prompt on resize again, to avoid glitches.
- (from 4.1.0) On MSYS2, fix saving/loading of universal variables (:issue:`11948`).
- (from 4.1.0) Fix error using ``man`` for the commands ``!`` ``.`` ``:`` ``[`` ``{`` (:issue:`11955`).
- (from 4.1.0) Fix build issues on illumos systems (:issue:`11982`).
- (from 4.1.0) Fix crash on invalid :doc:`function <cmds/function>` command (:issue:`11912`).
- (from 4.0.0) Fix build on SPARC and MIPS Linux by disabling ``SIGSTKFLT``.
- (from 4.0.0) Fix crash when passing negative PIDs to builtin :doc:`wait <cmds/wait>` (:issue:`11929`).
- (from 4.0.0) On Linux, fix :doc:`status fish-path <cmds/status>` output when fish has been reinstalled since it was started.
fish 4.1.2 (released October 7, 2025)
=====================================
fish 4.1.2 (released ???)
=========================
This release fixes the following regressions identified in 4.1.0:
- Fixed spurious error output when completing remote file paths for ``scp`` (:issue:`11860`).
- Fixed the :kbd:`alt-l` binding not formatting ``ls`` output correctly (one entry per line, no colors) (:issue:`11888`).
- Fixed an issue where focus events (currently only enabled in ``tmux``) would cause multiline prompts to be redrawn in the wrong line (:issue:`11870`).
- Stopped printing output that would cause a glitch on old versions of Midnight Commander (:issue:`11869`).
- Added a fix for some configurations of Zellij where :kbd:`escape` key processing was delayed (:issue:`11868`).
- Added a workaround for old versions of Zellij where :kbd:`escape` processing was delayed (:issue:`11868`).
- Fixed a case where the :doc:`web-based configuration tool <cmds/fish_config>` would generate invalid configuration (:issue:`11861`).
- Fixed a case where pasting into ``fish -c read`` would fail with a noisy error (:issue:`11836`).
- Fixed a case where upgrading fish would break old versions of fish that were still running.
In general, fish still needs to be restarted after it is upgraded,
@@ -262,13 +29,15 @@ This release fixes the following regressions identified in 4.1.0:
This will not affect fish's child processes unless ``LC_MESSAGES`` was already exported.
- Some :doc:`fish_config <cmds/fish_config>` subcommands for showing prompts and themes had been broken in standalone Linux builds (those using the ``embed-data`` cargo feature), which has been fixed (:issue:`11832`).
- On Windows Terminal, we observed an issue where fish would fail to read the terminal's response to our new startup queries, causing noticeable lags and a misleading error message. A workaround has been added (:issue:`11841`).
- On Windows Terminal, we observed an issue where fish would fail to read the terminal's response to our new startup queries, causing noticeable lags and a misleading error message. A workaround has been added (:issue:`11841`).
- A WezTerm `issue breaking shifted key input <https://github.com/wezterm/wezterm/issues/6087>`__ has resurfaced on some versions of WezTerm; our workaround has been extended to cover all versions for now (:issue:`11204`).
- Fixed a crash in :doc:`the web-based configuration tool <cmds/fish_config>` when using the new underline styles (:issue:`11840`).
fish 4.1.0 (released September 27, 2025)
========================================
.. ignore for 4.1: 10929 10940 10948 10955 10965 10975 10989 10990 10998 11028 11052 11055 11069 11071 11079 11092 11098 11104 11106 11110 11140 11146 11148 11150 11214 11218 11259 11288 11299 11328 11350 11373 11395 11417 11419
Notable improvements and fixes
------------------------------
- Compound commands (``begin; echo 1; echo 2; end``) can now be written using braces (``{ echo1; echo 2 }``), like in other shells.
@@ -1090,7 +859,7 @@ Notable improvements and fixes
which expands ``!!`` to the last history item, anywhere on the command line, mimicking other shells' history expansion.
See :doc:`the documentation <cmds/abbr>` for more.
See :ref:`the documentation <cmd-abbr>` for more.
- ``path`` gained a new ``mtime`` subcommand to print the modification time stamp for files. For example, this can be used to handle cache file ages (:issue:`9057`)::
> touch foo
@@ -1515,7 +1284,7 @@ Scripting improvements
two
'blue '
- ``$fish_user_paths`` is now automatically deduplicated to fix a common user error of appending to it in config.fish when it is universal (:issue:`8117`). :doc:`fish_add_path <cmds/fish_add_path>` remains the recommended way to add to $PATH.
- ``$fish_user_paths`` is now automatically deduplicated to fix a common user error of appending to it in config.fish when it is universal (:issue:`8117`). :ref:`fish_add_path <cmd-fish_add_path>` remains the recommended way to add to $PATH.
- ``return`` can now be used outside functions. In scripts, it does the same thing as ``exit``. In interactive mode,it sets ``$status`` without exiting (:issue:`8148`).
- An oversight prevented all syntax checks from running on commands given to ``fish -c`` (:issue:`8171`). This includes checks such as ``exec`` not being allowed in a pipeline, and ``$$`` not being a valid variable. Generally, another error was generated anyway.
- ``fish_indent`` now correctly reformats tokens that end with a backslash followed by a newline (:issue:`8197`).

View File

@@ -24,26 +24,9 @@ include(cmake/Rust.cmake)
# Work around issue where archive-built libs go in the wrong place.
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
find_program(SPHINX_EXECUTABLE NAMES sphinx-build
HINTS
$ENV{SPHINX_DIR}
PATH_SUFFIXES bin
DOC "Sphinx documentation generator")
# Tell Cargo where our build directory is so it can find Cargo.toml.
set(VARS_FOR_CARGO
"FISH_CMAKE_BINARY_DIR=${CMAKE_BINARY_DIR}"
"PREFIX=${CMAKE_INSTALL_PREFIX}"
"DOCDIR=${CMAKE_INSTALL_FULL_DOCDIR}"
"DATADIR=${CMAKE_INSTALL_FULL_DATADIR}"
"SYSCONFDIR=${CMAKE_INSTALL_FULL_SYSCONFDIR}"
"BINDIR=${CMAKE_INSTALL_FULL_BINDIR}"
"CARGO_TARGET_DIR=${FISH_RUST_BUILD_DIR}"
"CARGO_BUILD_RUSTC=${Rust_COMPILER}"
"${FISH_PCRE2_BUILDFLAG}"
"FISH_SPHINX=${SPHINX_EXECUTABLE}"
)
# Set up the machinery around FISH-BUILD-VERSION-FILE
# This defines the FBVF variable.
include(Version)
# Let fish pick up when we're running out of the build directory without installing
get_filename_component(REAL_CMAKE_BINARY_DIR "${CMAKE_BINARY_DIR}" REALPATH)
@@ -56,40 +39,37 @@ if(NOT "${CMAKE_BUILD_TYPE}" IN_LIST build_types)
message(WARNING "Unsupported build type ${CMAKE_BUILD_TYPE}. If this doesn't build, try one of Release, RelWithDebInfo or Debug")
endif()
add_custom_target(
fish ALL
COMMAND
"${CMAKE_COMMAND}" -E
env ${VARS_FOR_CARGO}
${Rust_CARGO}
build --bin fish
$<$<CONFIG:Release>:--release>
$<$<CONFIG:RelWithDebInfo>:--profile=release-with-debug>
--target ${Rust_CARGO_TARGET}
--no-default-features
--features=${FISH_CARGO_FEATURES}
${CARGO_FLAGS}
&&
"${CMAKE_COMMAND}" -E
copy "${rust_target_dir}/${rust_profile}/fish" "${CMAKE_CURRENT_BINARY_DIR}"
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
USES_TERMINAL
)
function(CREATE_LINK target)
# Define a function to build and link dependencies.
function(CREATE_TARGET target)
add_custom_target(
${target} ALL
DEPENDS fish
COMMAND ln -f fish ${target}
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
COMMAND
"${CMAKE_COMMAND}" -E
env ${VARS_FOR_CARGO}
${Rust_CARGO}
build --bin ${target}
$<$<CONFIG:Release>:--release>
$<$<CONFIG:RelWithDebInfo>:--profile=release-with-debug>
--target ${Rust_CARGO_TARGET}
--no-default-features
--features=${FISH_CARGO_FEATURES}
${CARGO_FLAGS}
&&
"${CMAKE_COMMAND}" -E
copy "${rust_target_dir}/${rust_profile}/${target}" "${CMAKE_CURRENT_BINARY_DIR}"
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
USES_TERMINAL
)
endfunction(CREATE_LINK)
endfunction(CREATE_TARGET)
# Define fish.
create_target(fish)
# Define fish_indent.
create_link(fish_indent)
create_target(fish_indent)
# Define fish_key_reader.
create_link(fish_key_reader)
create_target(fish_key_reader)
# Set up the docs.
include(cmake/Docs.cmake)

View File

@@ -21,10 +21,10 @@ Archives are available at https://lists.sr.ht/~krobelus/fish-shell/.
GitHub
======
Fish is available on GitHub, at https://github.com/fish-shell/fish-shell.
Fish is available on Github, at https://github.com/fish-shell/fish-shell.
First, you'll need an account there, and you'll need a git clone of fish.
Fork it on GitHub and then run::
Fork it on Github and then run::
git clone https://github.com/<USERNAME>/fish-shell.git
@@ -50,24 +50,7 @@ Guidelines
In short:
- Be conservative in what you need (keep to the agreed minimum supported Rust version, limit new dependencies)
- Use automated tools to help you (``cargo xtask check``)
Commit History
==============
We use a linear, `recipe-style <https://www.bitsnbites.eu/git-history-work-log-vs-recipe/>`__ history.
Every commit should pass our checks.
We do not want "fixup" commits in our history.
If you notice an issue with a commit in a pull request, or get feedback suggesting changes,
you should rewrite the commit history and fix the relevant commits directly,
instead of adding new "fixup" commits.
When a pull request is ready, we rebase it on top of the current master branch,
so don't be shy about rewriting the history of commits which are not on master yet.
Rebasing (not merging) your pull request on the latest version of master is also welcome, especially if it resolves conflicts.
If you're using Git, consider using `jj <https://www.jj-vcs.dev/>`__ to make this easier.
If a commit should close an issue, add a ``Fixes #<issue-number>`` line at the end of the commit description.
- Use automated tools to help you (``build_tools/check.sh``)
Contributing completions
========================
@@ -88,7 +71,7 @@ Completion scripts should
1. Use as few dependencies as possible - try to use fish's builtins like ``string`` instead of ``grep`` and ``awk``,
use ``python`` to read json instead of ``jq`` (because it's already a soft dependency for fish's tools)
2. If it uses a common unix tool, use POSIX-compatible invocations - ideally it would work on GNU/Linux, macOS, the BSDs and other systems
2. If it uses a common unix tool, use posix-compatible invocations - ideally it would work on GNU/Linux, macOS, the BSDs and other systems
3. Option and argument descriptions should be kept short.
The shorter the description, the more likely it is that fish can use more columns.
4. Function names should start with ``__fish``, and functions should be kept in the completion file unless they're used elsewhere.
@@ -105,40 +88,45 @@ Contributing documentation
==========================
The documentation is stored in ``doc_src/``, and written in ReStructured Text and built with Sphinx.
The builtins and various functions shipped with fish are documented in ``doc_src/cmds/``.
To build an HTML version of the docs locally, run::
To build it locally, run from the main fish-shell directory::
cargo xtask html-docs
sphinx-build -j 8 -b html -n doc_src/ /tmp/fish-doc/
will output to ``target/fish-docs/html`` or, if you use CMake::
cmake --build build -t sphinx-docs
will output to ``build/cargo/fish-docs/html/``. You can also run ``sphinx-build`` directly, which allows choosing the output directory::
sphinx-build -j auto -b html doc_src/ /tmp/fish-doc/
will output HTML docs to ``/tmp/fish-doc``.
After building them, you can open the HTML docs in a browser and see that it looks okay.
which will build the docs as html in /tmp/fish-doc. You can open it in a browser and see that it looks okay.
The builtins and various functions shipped with fish are documented in doc_src/cmds/.
Code Style
==========
For formatting, we use:
To ensure your changes conform to the style rules run
::
build_tools/style.fish
before committing your change. That will run our autoformatters:
- ``rustfmt`` for Rust
- ``fish_indent`` (shipped with fish) for fish script
- ``ruff format`` for Python
- ``black`` for python
To reformat files, there is a script
If youve already committed your changes thats okay since it will then
check the files in the most recent commit. This can be useful after
youve merged another persons change and want to check that its style
is acceptable. However, in that case it will run ``clang-format`` to
ensure the entire file, not just the lines modified by the commit,
conform to the style.
If you want to check the style of the entire code base run
::
build_tools/style.fish --all
build_tools/style.fish somefile.rs some.fish
That command will refuse to restyle any files if you have uncommitted
changes.
Fish Script Style Guide
-----------------------
@@ -190,10 +178,10 @@ made to run fish_indent via e.g.
(add-hook 'fish-mode-hook (lambda ()
(add-hook 'before-save-hook 'fish_indent-before-save)))
Minimum Supported Rust Version (MSRV) Policy
--------------------------------------------
Rust Style Guide
----------------
We support at least the version of ``rustc`` available in Debian Stable.
Use ``cargo fmt`` and ``cargo clippy``. Clippy warnings can be turned off if there's a good reason to.
Testing
=======
@@ -202,53 +190,84 @@ The source code for fish includes a large collection of tests. If you
are making any changes to fish, running these tests is a good way to make
sure the behaviour remains consistent and regressions are not
introduced. Even if you dont run the tests on your machine, they will
still be run via GitHub Actions.
still be run via Github Actions.
You are strongly encouraged to add tests when changing the functionality
of fish, especially if you are fixing a bug to help ensure there are no
regressions in the future (i.e., we dont reintroduce the bug).
Unit tests live next to the implementation in Rust source files, in inline submodules (``mod tests {}``).
The tests can be found in three places:
System tests live in ``tests/``:
- src/tests for unit tests.
- tests/checks for script tests, run by `littlecheck <https://github.com/ridiculousfish/littlecheck>`__
- tests/pexpects for interactive tests using `pexpect <https://pexpect.readthedocs.io/en/stable/>`__
- ``tests/checks`` are run by `littlecheck <https://github.com/ridiculousfish/littlecheck>`__
and test noninteractive (script) behavior,
except for ``tests/checks/tmux-*`` which test interactive scenarios.
- ``tests/pexpects`` tests interactive scenarios using `pexpect <https://pexpect.readthedocs.io/en/stable/>`__
When in doubt, the bulk of the tests should be added as a littlecheck test in tests/checks, as they are the easiest to modify and run, and much faster and more dependable than pexpect tests. The syntax is fairly self-explanatory. It's a fish script with the expected output in ``# CHECK:`` or ``# CHECKERR:`` (for stderr) comments.
If your littlecheck test has a specific dependency, use ``# REQUIRE: ...`` with a posix sh script.
When in doubt, the bulk of the tests should be added as a littlecheck test in tests/checks, as they are the easiest to modify and run, and much faster and more dependable than pexpect tests.
The syntax is fairly self-explanatory.
It's a fish script with the expected output in ``# CHECK:`` or ``# CHECKERR:`` (for stderr) comments.
If your littlecheck test has a specific dependency, use ``# REQUIRE: ...`` with a POSIX sh script.
The pexpects are written in python and can simulate input and output to/from a terminal, so they are needed for anything that needs actual interactivity. The runner is in tests/pexpect_helper.py, in case you need to modify something there.
The pexpect tests are written in Python and can simulate input and output to/from a terminal, so they are needed for anything that needs actual interactivity.
The runner is in tests/pexpect_helper.py, in case you need to modify something there.
These tests can be run via the tests/test_driver.py Python script, which will set up the environment.
These tests can be run via the tests/test_driver.py python script, which will set up the environment.
It sets up a temporary $HOME and also uses it as the current directory, so you do not need to create a temporary directory in them.
If you need a command to do something weird to test something, maybe add it to the ``fish_test_helper`` binary (in ``tests/fish_test_helper.c``).
If you need a command to do something weird to test something, maybe add it to the ``fish_test_helper`` binary (in tests/fish_test_helper.c), or see if it can already do it.
Local testing
-------------
The tests can be run on your local system::
The tests can be run on your local computer on all operating systems.
cargo build
# Run unit tests
cargo test
# Run system tests
tests/test_driver.py target/debug
# Run a specific system test.
tests/test_driver.py target/debug tests/checks/abbr.fish
::
cmake path/to/fish-shell
make fish_run_tests
Or you can run them on a fish, without involving cmake::
cargo build
cargo test # for the unit tests
tests/test_driver.py target/debug # for the script and interactive tests
Here, the first argument to test_driver.py refers to a directory with ``fish``, ``fish_indent`` and ``fish_key_reader`` in it.
In this example we're in the root of the workspace and have run ``cargo build`` without ``--release``, so it's a debug build.
In this example we're in the root of the git repo and have run ``cargo build`` without ``--release``, so it's a debug build.
To run all tests and linters, use::
Git hooks
---------
cargo xtask check
Since developers sometimes forget to run the tests, it can be helpful to
use git hooks (see githooks(5)) to automate it.
One possibility is a pre-push hook script like this one:
.. code:: sh
#!/bin/sh
#### A pre-push hook for the fish-shell project
# This will run the tests when a push to master is detected, and will stop that if the tests fail
# Save this as .git/hooks/pre-push and make it executable
protected_branch='master'
# Git gives us lines like "refs/heads/frombranch SOMESHA1 refs/heads/tobranch SOMESHA1"
# We're only interested in the branches
isprotected=false
while read from _ to _; do
if [ "$to" = "refs/heads/$protected_branch" ]; then
isprotected=true
fi
done
if "$isprotected"; then
echo "Running checks before push to master"
build_tools/check.sh
fi
This will check if the push is to the master branch and, if it is, only
allow the push if running ``build_tools/check.sh`` succeeds. In some circumstances
it may be advisable to circumvent this check with
``git push --no-verify``, but usually that isnt necessary.
To install the hook, place the code in a new file
``.git/hooks/pre-push`` and make it executable.
Contributing Translations
=========================
@@ -260,8 +279,9 @@ It also means that some features are not supported, such as message context and
We also expect all files to be UTF-8-encoded.
In practice, this should not matter much for contributing translations.
Translation sources are stored in the ``localization/po`` directory and named ``ll_CC.po``,
where ``ll`` is the two (or possibly three) letter ISO 639-1 language code of the target language
Translation sources are
stored in the ``po`` directory, named ``ll_CC.po``, where ``ll`` is the
two (or possibly three) letter ISO 639-1 language code of the target language
(e.g. ``pt`` for Portuguese). ``CC`` is an ISO 3166 country/territory code,
(e.g. ``BR`` for Brazil).
An example for a valid name is ``pt_BR.po``, indicating Brazilian Portuguese.
@@ -275,7 +295,7 @@ More specifically, you will need ``msguniq`` and ``msgmerge`` for creating trans
language.
To create a new translation, run::
build_tools/update_translations.fish localization/po/ll_CC.po
build_tools/update_translations.fish po/ll_CC.po
This will create a new PO file containing all messages available for translation.
If the file already exists, it will be updated.
@@ -321,9 +341,9 @@ Editing PO files
Many tools are available for editing translation files, including
command-line and graphical user interface programs. For simple use, you can use your text editor.
Open up the PO file, for example ``localization/po/sv.po``, and you'll see something like::
Open up the PO file, for example ``po/sv.po``, and you'll see something like::
msgid "%s: No suitable job\n"
msgid "%ls: No suitable job\n"
msgstr ""
The ``msgid`` here is the "name" of the string to translate, typically the English string to translate.
@@ -331,10 +351,10 @@ The second line (``msgstr``) is where your translation goes.
For example::
msgid "%s: No suitable job\n"
msgstr "%s: Inget passande jobb\n"
msgid "%ls: No suitable job\n"
msgstr "%ls: Inget passande jobb\n"
Any ``%s`` or ``%d`` are placeholders that fish will use for formatting at runtime. It is important that they match - the translated string should have the same placeholders in the same order.
Any ``%s`` / ``%ls`` or ``%d`` are placeholders that fish will use for formatting at runtime. It is important that they match - the translated string should have the same placeholders in the same order.
Also any escaped characters, like that ``\n`` newline at the end, should be kept so the translation has the same behavior.
@@ -349,7 +369,7 @@ Modifications to strings in source files
If a string changes in the sources, the old translations will no longer work.
They will be preserved in the PO files, but commented-out (starting with ``#~``).
If you add/remove/change a translatable strings in a source file,
run ``build_tools/update_translations.fish`` to propagate this to all translation files (``localization/po/*.po``).
run ``build_tools/update_translations.fish`` to propagate this to all translation files (``po/*.po``).
This is only relevant for developers modifying the source files of fish or fish scripts.
Setting Code Up For Translations
@@ -361,7 +381,7 @@ macros:
::
streams.out.append(wgettext_fmt!("%s: There are no jobs\n", argv[0]));
streams.out.append(wgettext_fmt!("%ls: There are no jobs\n", argv[0]));
All messages in fish script must be enclosed in single or double quote
characters for our message extraction script to find them.
@@ -384,12 +404,6 @@ You can use either single or double quotes to enclose the
message to be translated. You can also optionally include spaces after
the opening parentheses or before the closing parentheses.
Updating Dependencies
=====================
To update dependencies, run ``build_tools/update-dependencies.sh``.
This currently requires `updatecli <https://github.com/updatecli/updatecli>`__ and a few other tools.
Versioning
==========

847
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,82 +1,58 @@
[workspace]
resolver = "2"
members = ["crates/*"]
[workspace.package]
# To build revisions that use Corrosion (those before 2024-01), use CMake 3.19, Rustc 1.78 and Rustup 1.27.
rust-version = "1.85"
edition = "2024"
rust-version = "1.70"
edition = "2021"
repository = "https://github.com/fish-shell/fish-shell"
# see doc_src/license.rst for details
# don't forget to update COPYING and debian/copyright too
license = "GPL-2.0-only AND LGPL-2.0-or-later AND MIT AND PSF-2.0"
[workspace.dependencies]
assert_matches = "1.5.0"
bitflags = "2.5.0"
cc = "1.0.94"
cfg-if = "1.0.3"
clap = { version = "4.5.54", features = ["derive"] }
errno = "0.3.0"
fish-build-helper = { path = "crates/build-helper" }
fish-build-man-pages = { path = "crates/build-man-pages" }
fish-color = { path = "crates/color" }
fish-common = { path = "crates/common" }
fish-fallback = { path = "crates/fallback" }
fish-gettext = { path = "crates/gettext" }
fish-gettext-extraction = { path = "crates/gettext-extraction" }
fish-gettext-maps = { path = "crates/gettext-maps" }
fish-gettext-mo-file-parser = { path = "crates/gettext-mo-file-parser" }
fish-printf = { path = "crates/printf", features = ["widestring"] }
fish-tempfile = { path = "crates/tempfile" }
fish-util = { path = "crates/util" }
fish-wcstringutil = { path = "crates/wcstringutil" }
fish-widecharwidth = { path = "crates/widecharwidth" }
fish-widestring = { path = "crates/widestring" }
fish-wgetopt = { path = "crates/wgetopt" }
itertools = "0.14.0"
libc = "0.2.177"
libc = "0.2.155"
# lru pulls in hashbrown by default, which uses a faster (though less DoS resistant) hashing algo.
# disabling default features uses the stdlib instead, but it doubles the time to rewrite the history
# files as of 22 April 2024.
lru = "0.16.2"
nix = { version = "0.31.1", default-features = false, features = [
lru = "0.13.0"
nix = { version = "0.30.1", default-features = false, features = [
"event",
"fs",
"inotify",
"hostname",
"resource",
"process",
"signal",
"term",
"user",
"fs",
] }
num-traits = "0.2.19"
once_cell = "1.19.0"
pcre2 = { git = "https://github.com/fish-shell/rust-pcre2", tag = "0.2.9-utf32", default-features = false, features = [
"utf32",
] }
phf = { version = "0.13", default-features = false }
phf_codegen = "0.13"
phf = { version = "0.12", default-features = false }
phf_codegen = { version = "0.12" }
portable-atomic = { version = "1", default-features = false, features = [
"fallback",
] }
proc-macro2 = "1.0"
rand = { version = "0.9.2", default-features = false, features = [
"small_rng",
"thread_rng",
] }
rsconf = "0.3.0"
rust-embed = { version = "8.11.0", features = [
"deterministic-timestamps",
"include-exclude",
"interpolate-folder-path",
] }
# Don't use the "getrandom" feature as it requires "getentropy" which was not
# available on macOS < 10.12. We can enable "getrandom" when we raise the
# minimum supported version to 10.12.
rand = { version = "0.8.5", default-features = false, features = ["small_rng"] }
rsconf = "0.2.2"
rust-embed = { version = "8.7.2", features = ["deterministic-timestamps"] }
serial_test = { version = "3", default-features = false }
# We need 0.9.0 specifically for some crash fixes.
terminfo = "0.9.0"
widestring = "1.2.0"
unicode-segmentation = "1.12.0"
unicode-width = "0.2.0"
unix_path = "1.0.1"
xterm-color = "1.0.1"
[profile.release]
overflow-checks = true
@@ -88,34 +64,24 @@ debug = true
[package]
name = "fish"
version = "4.5.0"
version = "4.1.0-snapshot"
edition.workspace = true
rust-version.workspace = true
default-run = "fish"
license.workspace = true
# see doc_src/license.rst for details
# don't forget to update COPYING and debian/copyright too
license = "GPL-2.0-only AND LGPL-2.0-or-later AND MIT AND PSF-2.0"
homepage = "https://fishshell.com"
readme = "README.rst"
[dependencies]
assert_matches.workspace = true
bitflags.workspace = true
cfg-if.workspace = true
errno.workspace = true
fish-build-helper.workspace = true
fish-build-man-pages = { workspace = true, optional = true }
fish-color.workspace = true
fish-common.workspace = true
fish-fallback.workspace = true
fish-gettext = { workspace = true, optional = true }
fish-gettext-extraction = { workspace = true, optional = true }
fish-gettext-maps = { workspace = true, optional = true }
fish-printf.workspace = true
fish-tempfile.workspace = true
fish-util.workspace = true
fish-wcstringutil.workspace = true
fish-wgetopt.workspace = true
fish-widecharwidth.workspace = true
fish-widestring.workspace = true
itertools.workspace = true
libc.workspace = true
lru.workspace = true
macro_rules_attribute = "0.2.2"
@@ -123,26 +89,18 @@ nix.workspace = true
num-traits.workspace = true
once_cell.workspace = true
pcre2.workspace = true
phf = { workspace = true, optional = true }
rand.workspace = true
unicode-width.workspace = true
xterm-color.workspace = true
terminfo.workspace = true
widestring.workspace = true
[target.'cfg(not(target_has_atomic = "64"))'.dependencies]
portable-atomic.workspace = true
[target.'cfg(windows)'.dependencies]
rust-embed = { workspace = true, features = [
"deterministic-timestamps",
"debug-embed",
"include-exclude",
"interpolate-folder-path",
] }
rust-embed = { workspace = true, optional = true, features = ["deterministic-timestamps", "debug-embed"] }
[target.'cfg(not(windows))'.dependencies]
rust-embed = { workspace = true, features = [
"deterministic-timestamps",
"include-exclude",
"interpolate-folder-path",
] }
rust-embed = { workspace = true, optional = true, features = ["deterministic-timestamps"] }
[dev-dependencies]
serial_test.workspace = true
@@ -174,19 +132,20 @@ name = "fish_key_reader"
path = "src/bin/fish_key_reader.rs"
[features]
default = ["embed-manpages", "localize-messages"]
default = ["embed-data", "localize-messages"]
benchmark = []
embed-manpages = ["dep:fish-build-man-pages"]
embed-data = ["dep:rust-embed", "dep:fish-build-man-pages"]
# Enable gettext localization at runtime. Requires the `msgfmt` tool to generate catalog data at
# build time.
localize-messages = ["dep:fish-gettext"]
localize-messages = ["dep:phf", "dep:fish-gettext-maps"]
# This feature is used to enable extracting messages from the source code for localization.
# It only needs to be enabled if updating these messages (and the corresponding PO files) is
# desired. This happens when running tests via `cargo xtask check` and when calling
# desired. This happens when running tests via `build_tools/check.sh` and when calling
# `build_tools/update_translations.fish`, so there should not be a need to enable it manually.
gettext-extract = ["dep:fish-gettext-extraction"]
# The following features are auto-detected by the build-script and should not be enabled manually.
asan = []
tsan = []
[workspace.lints]
@@ -194,36 +153,15 @@ rust.non_camel_case_types = "allow"
rust.non_upper_case_globals = "allow"
rust.unknown_lints = "allow"
rust.unstable_name_collisions = "allow"
rustdoc.private_intra_doc_links = "allow"
[workspace.lints.clippy]
assigning_clones = "warn"
cloned_instead_of_copied = "warn"
explicit_into_iter_loop = "warn"
format_push_string = "warn"
implicit_clone = "warn"
len_without_is_empty = "allow" # we're not a library crate
let_and_return = "allow"
manual_assert = "warn"
manual_range_contains = "allow"
map_unwrap_or = "warn"
mut_mut = "warn"
needless_lifetimes = "allow"
new_without_default = "allow"
option_map_unit_fn = "allow"
ptr_offset_by_literal = "warn"
ref_option = "warn"
semicolon_if_nothing_returned = "warn"
stable_sort_primitive = "warn"
str_to_string = "warn"
unnecessary_semicolon = "warn"
unused_trait_names = "warn"
clippy.manual_range_contains = "allow"
clippy.needless_return = "allow"
clippy.needless_lifetimes = "allow"
# We do not want to use the e?print(ln)?! macros.
# These lints flag their use.
# In the future, they might change to flag other methods of printing.
print_stdout = "deny"
print_stderr = "deny"
clippy.print_stdout = "deny"
clippy.print_stderr = "deny"
[lints]
workspace = true

18
Dockerfile Normal file
View File

@@ -0,0 +1,18 @@
FROM centos:latest
# Build dependency
RUN yum update -y &&\
yum install -y epel-release &&\
yum install -y clang cmake3 gcc-c++ make &&\
yum clean all
# Test dependency
RUN yum install -y expect vim-common
ADD . /src
WORKDIR /src
# Build fish
RUN cmake3 . &&\
make &&\
make install

View File

@@ -7,7 +7,7 @@
CMAKE ?= cmake
GENERATOR ?= $(shell (which ninja > /dev/null 2> /dev/null && echo Ninja) || \
echo 'Unix Makefiles')
echo 'Unix Makefiles')
prefix ?= /usr/local
PREFIX ?= $(prefix)
@@ -34,7 +34,7 @@ all: .begin build/fish
.PHONY: .begin
.begin:
@which $(CMAKE) > /dev/null 2> /dev/null || \
(echo 'Please install CMake and then re-run the `make` command!' 1>&2 && false)
(echo 'Please install CMake and then re-run the `make` command!' 1>&2 && false)
.PHONY: build/fish
build/fish: build/$(BUILDFILE)

View File

@@ -37,7 +37,7 @@ fish can be installed:
- using the `installer from fishshell.com <https://fishshell.com/>`__
- as a `standalone app from fishshell.com <https://fishshell.com/>`__
Note: The minimum supported macOS version is 10.12.
Note: The minimum supported macOS version is 10.10 "Yosemite".
Packages for Linux
~~~~~~~~~~~~~~~~~~
@@ -90,7 +90,7 @@ Running fish requires:
- some common \*nix system utilities (currently ``mktemp``), in
addition to the basic POSIX utilities (``cat``, ``cut``, ``dirname``,
``ls``, ``mkdir``, ``mkfifo``, ``rm``, ``sh``, ``sort``, ``tee``, ``tr``,
``file``, ``ls``, ``mkdir``, ``mkfifo``, ``rm``, ``sh``, ``sort``, ``tee``, ``tr``,
``uname`` and ``sed`` at least, but the full coreutils plus ``find`` and
``awk`` is preferred)
@@ -100,7 +100,6 @@ The following optional features also have specific requirements:
messages require ``man`` for display
- automated completion generation from manual pages requires Python 3.5+
- the ``fish_config`` web configuration tool requires Python 3.5+ and a web browser
- the :ref:`alt-o <shared-binds-alt-o>` binding requires the ``file`` program.
- system clipboard integration (with the default Ctrl-V and Ctrl-X
bindings) require either the ``xsel``, ``xclip``,
``wl-copy``/``wl-paste`` or ``pbcopy``/``pbpaste`` utilities
@@ -112,12 +111,14 @@ The following optional features also have specific requirements:
Building
--------
.. _dependencies-1:
Dependencies
~~~~~~~~~~~~
Compiling fish requires:
- Rust (version 1.85 or later), including cargo
- Rust (version 1.70 or later)
- CMake (version 3.15 or later)
- a C compiler (for system feature detection and the test helper binary)
- PCRE2 (headers and libraries) - optional, this will be downloaded if missing
@@ -127,7 +128,7 @@ Compiling fish requires:
Sphinx is also optionally required to build the documentation from a
cloned git repository.
Additionally, running the full test suite requires diff, git, Python 3.5+, pexpect, less, tmux and wget.
Additionally, running the full test suite requires Python 3.5+, tmux, and the pexpect package.
Building from source with CMake
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -139,7 +140,7 @@ links above, and up-to-date `development builds of fish are available for many p
To install into ``/usr/local``, run:
.. code:: shell
.. code:: bash
mkdir build; cd build
cmake ..
@@ -157,52 +158,41 @@ In addition to the normal CMake build options (like ``CMAKE_INSTALL_PREFIX``), f
- Rust_COMPILER=path - the path to rustc. If not set, cmake will check $PATH and ~/.cargo/bin
- Rust_CARGO=path - the path to cargo. If not set, cmake will check $PATH and ~/.cargo/bin
- Rust_CARGO_TARGET=target - the target to pass to cargo. Set this for cross-compilation.
- WITH_DOCS=ON|OFF - whether to build the documentation. By default, this is ON when Sphinx is installed.
- FISH_INDENT_FOR_BUILDING_DOCS - useful for cross-compilation.
Set this to the path to the ``fish_indent`` executable to use for building HTML docs.
By default, ``${CMAKE_BINARY_DIR}/fish_indent`` will be used.
If that's not runnable on the compile host,
you can build a native one with ``cargo build --bin fish_indent`` and set this to ``$PWD/target/debug/fish_indent``.
- BUILD_DOCS=ON|OFF - whether to build the documentation. This is automatically set to OFF when Sphinx isn't installed.
- INSTALL_DOCS=ON|OFF - whether to install the docs. This is automatically set to on when BUILD_DOCS is or prebuilt documentation is available (like when building in-tree from a tarball).
- FISH_USE_SYSTEM_PCRE2=ON|OFF - whether to use an installed pcre2. This is normally autodetected.
- WITH_MESSAGE_LOCALIZATION=ON|OFF - whether to include translations.
- MAC_CODESIGN_ID=String|OFF - the codesign ID to use on Mac, or "OFF" to disable codesigning.
- WITH_GETTEXT=ON|OFF - whether to include translations.
- extra_functionsdir, extra_completionsdir and extra_confdir - to compile in an additional directory to be searched for functions, completions and configuration snippets
Building fish with Cargo
~~~~~~~~~~~~~~~~~~~~~~~~
Building fish with embedded data (experimental)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can also build fish with Cargo.
This example uses `uv <https://github.com/astral-sh/uv>`__ to install Sphinx (which is used for man-pages and ``--help`` options).
You can also install Sphinx another way and drop the ``uv run --no-managed-python`` prefix.
You can also build fish with the data files embedded.
.. code:: shell
This will include all the datafiles like the included functions or web configuration tool in the main ``fish`` binary.
git clone https://github.com/fish-shell/fish-shell
cd fish-shell
Fish will then read these right from its own binary, and print them out when needed. Some files, like the webconfig tool and the manpage completion generator, will be extracted to a temporary directory on-demand. You can list the files with ``status list-files`` and print one with ``status get-file path/to/file`` (e.g. ``status get-file functions/fish_prompt.fish`` to get the default prompt).
# Optional: check out a specific version rather than building the latest
# development version.
git checkout "$(git for-each-ref refs/tags/ | awk '$2 == "tag" { print $3 }' | tail -1)"
To install fish with embedded files, just use ``cargo``, like::
uv run --no-managed-python \
cargo install --path .
cargo install --path /path/to/fish # if you have a git clone
cargo install --git https://github.com/fish-shell/fish-shell --tag "$(curl -s https://api.github.com/repos/fish-shell/fish-shell/releases/latest | jq -r .tag_name)" # to build the latest release
cargo install --git https://github.com/fish-shell/fish-shell # to build the latest development snapshot
This will place standalone binaries in ``~/.cargo/bin/``, but you can move them wherever you want.
This will place the standalone binaries in ``~/.cargo/bin/``, but you can place them wherever you want.
To disable translations, disable the ``localize-messages`` feature by passing ``--no-default-features --features=embed-manpages`` to cargo.
This build won't have the HTML docs (``help`` will open the online version).
It will try to build the man pages with sphinx-build. If that is not available and you would like to include man pages, you need to install it and retrigger the build script, e.g. by setting FISH_BUILD_DOCS=1::
FISH_BUILD_DOCS=1 cargo install --path .
Setting it to "0" disables the inclusion of man pages.
To disable translations, disable the ``localize-messages`` feature by passing ``--no-default-features --features=embed-data`` to cargo.
You can also link this build statically (but not against glibc) and move it to other computers.
Here are the remaining advantages of a full installation, as currently done by CMake:
- Man pages like ``fish(1)`` installed in standard locations, easily accessible from outside fish.
- Separate files for builtins (e.g. ``$PREFIX/share/fish/man/man1/abbr.1``).
- A local copy of the HTML documentation, typically accessed via the ``help`` fish function.
In Cargo builds, ``help`` will redirect to `<https://fishshell.com/docs/current/>`__
- Ability to use our CMake options extra_functionsdir, extra_completionsdir and extra_confdir,
(also recorded in ``$PREFIX/share/pkgconfig/fish.pc``)
which are used by some package managers to house third-party completions.
Regardless of build system, fish uses ``$XDG_DATA_DIRS/{vendor_completion.d,vendor_conf.d,vendor_functions.d}``.
Contributing Changes to the Code
--------------------------------

300
build.rs
View File

@@ -1,10 +1,14 @@
use fish_build_helper::{
env_var, fish_build_dir, target_os, target_os_is_apple, target_os_is_bsd, target_os_is_cygwin,
workspace_root,
};
#![allow(clippy::uninlined_format_args)]
use fish_build_helper::{fish_build_dir, workspace_root};
use rsconf::Target;
use std::env;
use std::path::{Path, PathBuf};
fn canonicalize<P: AsRef<Path>>(path: P) -> PathBuf {
std::fs::canonicalize(path).unwrap()
}
fn main() {
setup_paths();
@@ -12,29 +16,39 @@ fn main() {
// language server.
rsconf::set_env_value(
"FISH_RESOLVED_BUILD_DIR",
"FISH_BUILD_DIR",
// If set by CMake, this might include symlinks. Since we want to compare this to the
// dir fish is executed in we need to canonicalize it.
fish_build_dir().canonicalize().unwrap().to_str().unwrap(),
canonicalize(fish_build_dir()).to_str().unwrap(),
);
// We need to canonicalize (i.e. realpath) the manifest dir because we want to be able to
// compare it directly as a string at runtime.
rsconf::set_env_value(
"CARGO_MANIFEST_DIR",
workspace_root().canonicalize().unwrap().to_str().unwrap(),
canonicalize(workspace_root()).to_str().unwrap(),
);
// Some build info
rsconf::set_env_value("BUILD_TARGET_TRIPLE", &env_var("TARGET").unwrap());
rsconf::set_env_value("BUILD_HOST_TRIPLE", &env_var("HOST").unwrap());
rsconf::set_env_value("BUILD_PROFILE", &env_var("PROFILE").unwrap());
rsconf::set_env_value("BUILD_TARGET_TRIPLE", &env::var("TARGET").unwrap());
rsconf::set_env_value("BUILD_HOST_TRIPLE", &env::var("HOST").unwrap());
rsconf::set_env_value("BUILD_PROFILE", &env::var("PROFILE").unwrap());
let version = &get_version(&env::current_dir().unwrap());
// Per https://doc.rust-lang.org/cargo/reference/build-scripts.html#inputs-to-the-build-script,
// the source directory is the current working directory of the build script
rsconf::set_env_value("FISH_BUILD_VERSION", &get_version());
rsconf::set_env_value("FISH_BUILD_VERSION", version);
fish_build_helper::rebuild_if_embedded_path_changed("share");
std::env::set_var("FISH_BUILD_VERSION", version);
// These are necessary if built with embedded functions,
// but only in release builds (because rust-embed in debug builds reads from the filesystem).
#[cfg(feature = "embed-data")]
#[cfg(any(windows, not(debug_assertions)))]
rsconf::rebuild_if_path_changed("share");
#[cfg(feature = "gettext-extract")]
rsconf::rebuild_if_env_changed("FISH_GETTEXT_EXTRACTION_FILE");
let build = cc::Build::new();
let mut target = Target::new_from(build).unwrap();
@@ -43,9 +57,7 @@ fn main() {
detect_cfgs(&mut target);
#[cfg(all(target_env = "gnu", target_feature = "crt-static"))]
compile_error!(
"Statically linking against glibc has unavoidable crashes and is unsupported. Use dynamic linking or link statically against musl."
);
compile_error!("Statically linking against glibc has unavoidable crashes and is unsupported. Use dynamic linking or link statically against musl.");
}
/// Check target system support for certain functionality dynamically when the build is invoked,
@@ -60,48 +72,76 @@ fn main() {
/// `Cargo.toml`) behind a feature we just enabled.
///
/// [0]: https://github.com/rust-lang/cargo/issues/5499
#[rustfmt::skip]
fn detect_cfgs(target: &mut Target) {
for (name, handler) in [
// Ignore the first entry, it just sets up the type inference.
// Ignore the first entry, it just sets up the type inference. Model new entries after the
// second line.
("", &(|_: &Target| false) as &dyn Fn(&Target) -> bool),
("apple", &(|_| target_os_is_apple())),
("bsd", &(|_| target_os_is_bsd())),
("cygwin", &(|_| target_os_is_cygwin())),
("have_eventfd", &|target| {
// FIXME: NetBSD 10 has eventfd, but the libc crate does not expose it.
if target_os() == "netbsd" {
false
} else {
target.has_header("sys/eventfd.h")
}
}),
("have_localeconv_l", &|target| {
("apple", &detect_apple),
("bsd", &detect_bsd),
("cygwin", &detect_cygwin),
("small_main_stack", &has_small_stack),
// See if libc supports the thread-safe localeconv_l(3) alternative to localeconv(3).
("localeconv_l", &|target| {
target.has_symbol("localeconv_l")
}),
("have_pipe2", &|target| target.has_symbol("pipe2")),
("have_posix_spawn", &|target| {
if matches!(target_os().as_str(), "openbsd" | "android") {
// OpenBSD's posix_spawn returns status 127 instead of erroring with ENOEXEC when faced with a
// shebang-less script. Disable posix_spawn on OpenBSD.
//
// Android is broken for unclear reasons
false
} else {
target.has_header("spawn.h")
("FISH_USE_POSIX_SPAWN", &|target| {
target.has_header("spawn.h")
}),
("HAVE_PIPE2", &|target| {
target.has_symbol("pipe2")
}),
("HAVE_EVENTFD", &|target| {
// FIXME: NetBSD 10 has eventfd, but the libc crate does not expose it.
if cfg!(target_os = "netbsd") {
false
} else {
target.has_header("sys/eventfd.h")
}
}),
("small_main_stack", &has_small_stack),
("using_cmake", &|_| {
option_env!("FISH_CMAKE_BINARY_DIR").is_some()
}),
("waitstatus_signal_ret", &|target| {
("HAVE_WAITSTATUS_SIGNAL_RET", &|target| {
target.r#if("WEXITSTATUS(0x007f) == 0x7f", &["sys/wait.h"])
}),
] {
rsconf::declare_cfg(name, handler(target));
rsconf::declare_cfg(name, handler(target))
}
}
fn detect_apple(_: &Target) -> bool {
cfg!(any(target_os = "ios", target_os = "macos"))
}
fn detect_cygwin(_: &Target) -> bool {
// Cygwin target is usually cross-compiled.
std::env::var("CARGO_CFG_TARGET_OS").unwrap() == "cygwin"
}
/// Detect if we're being compiled for a BSD-derived OS, allowing targeting code conditionally with
/// `#[cfg(bsd)]`.
///
/// Rust offers fine-grained conditional compilation per-os for the popular operating systems, but
/// doesn't necessarily include less-popular forks nor does it group them into families more
/// specific than "windows" vs "unix" so we can conditionally compile code for BSD systems.
fn detect_bsd(_: &Target) -> bool {
// Instead of using `uname`, we can inspect the TARGET env variable set by Cargo. This lets us
// support cross-compilation scenarios.
let mut target = std::env::var("TARGET").unwrap();
if !target.chars().all(|c| c.is_ascii_lowercase()) {
target = target.to_ascii_lowercase();
}
#[allow(clippy::let_and_return)] // for old clippy
let is_bsd = target.ends_with("bsd") || target.ends_with("dragonfly");
#[cfg(any(
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
))]
assert!(is_bsd, "Target incorrectly detected as not BSD!");
is_bsd
}
/// Rust sets the stack size of newly created threads to a sane value, but is at at the mercy of the
/// OS when it comes to the size of the main stack. Some platforms we support default to a tiny
/// 0.5 MiB main stack, which is insufficient for fish's MAX_EVAL_DEPTH/MAX_STACK_DEPTH values.
@@ -120,9 +160,9 @@ fn has_small_stack(_: &Target) -> bool {
{
use core::ffi;
unsafe extern "C" {
unsafe fn pthread_get_stacksize_np(thread: *const ffi::c_void) -> usize;
unsafe fn pthread_self() -> *const ffi::c_void;
extern "C" {
fn pthread_get_stacksize_np(thread: *const ffi::c_void) -> usize;
fn pthread_self() -> *const ffi::c_void;
}
// build.rs is executed on the main thread, so we are getting the main thread's stack size.
@@ -137,65 +177,127 @@ fn setup_paths() {
#[cfg(windows)]
use unix_path::{Path, PathBuf};
fn overridable_path(
env_var_name: &str,
f: impl FnOnce(Option<String>) -> Option<PathBuf>,
) -> Option<PathBuf> {
rsconf::rebuild_if_env_changed(env_var_name);
let maybe_path = f(env_var(env_var_name));
if let Some(path) = maybe_path.as_ref() {
rsconf::set_env_value(env_var_name, path.to_str().unwrap());
fn get_path(name: &str, default: &str, onvar: &Path) -> PathBuf {
let mut var = PathBuf::from(env::var(name).unwrap_or(default.to_string()));
if var.is_relative() {
var = onvar.join(var);
}
maybe_path
var
}
fn join_if_relative(parent_if_relative: &Path, path: String) -> PathBuf {
let path = PathBuf::from(path);
if path.is_relative() {
parent_if_relative.join(path)
let prefix = PathBuf::from(env::var("PREFIX").unwrap_or("/usr/local".to_string()));
rsconf::rebuild_if_env_changed("PREFIX");
rsconf::set_env_value("PREFIX", prefix.to_str().unwrap());
let datadir = get_path("DATADIR", "share/", &prefix);
let sysconfdir = get_path(
"SYSCONFDIR",
// Embedded builds use "/etc," not "./share/etc".
if cfg!(feature = "embed-data") {
"/etc/"
} else {
path
"etc/"
},
&datadir,
);
rsconf::set_env_value("SYSCONFDIR", sysconfdir.to_str().unwrap());
rsconf::rebuild_if_env_changed("SYSCONFDIR");
#[cfg(not(feature = "embed-data"))]
{
rsconf::set_env_value("DATADIR", datadir.to_str().unwrap());
rsconf::rebuild_if_env_changed("DATADIR");
let bindir = get_path("BINDIR", "bin/", &prefix);
rsconf::set_env_value("BINDIR", bindir.to_str().unwrap());
rsconf::rebuild_if_env_changed("BINDIR");
let localedir = get_path("LOCALEDIR", "locale/", &datadir);
let localedir = localedir.to_str().unwrap();
assert!(!localedir.is_empty(), "empty LOCALEDIR is not supported");
rsconf::set_env_value("LOCALEDIR", localedir);
rsconf::rebuild_if_env_changed("LOCALEDIR");
let docdir = get_path("DOCDIR", "doc/fish", &datadir);
rsconf::set_env_value("DOCDIR", docdir.to_str().unwrap());
rsconf::rebuild_if_env_changed("DOCDIR");
}
}
fn get_version(src_dir: &Path) -> String {
use std::fs::read_to_string;
use std::process::Command;
if let Ok(var) = std::env::var("FISH_BUILD_VERSION") {
return var;
}
let path = src_dir.join("version");
if let Ok(strver) = read_to_string(path) {
return strver;
}
let args = &["describe", "--always", "--dirty=-dirty"];
if let Ok(output) = Command::new("git").args(args).output() {
let rev = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !rev.is_empty() {
// If it contains a ".", we have a proper version like "3.7",
// or "23.2.1-1234-gfab1234"
if rev.contains('.') {
return rev;
}
// If it doesn't, we probably got *just* the commit SHA,
// like "f1242abcdef".
// So we prepend the crate version so it at least looks like
// "3.8-gf1242abcdef"
// This lacks the commit *distance*, but that can't be helped without
// tags.
let version = env!("CARGO_PKG_VERSION").to_owned();
return version + "-g" + &rev;
}
}
let prefix = overridable_path("PREFIX", |env_prefix| {
Some(PathBuf::from(env_prefix.unwrap_or("/usr/local".to_owned())))
})
.unwrap();
// git did not tell us a SHA either because it isn't installed,
// or because it refused (safe.directory applies to `git describe`!)
// So we read the SHA ourselves.
fn get_git_hash() -> Result<String, Box<dyn std::error::Error>> {
let workspace_root = workspace_root();
let gitdir = workspace_root.join(".git");
let jjdir = workspace_root.join(".jj");
let commit_id = if gitdir.exists() {
// .git/HEAD contains ref: refs/heads/branch
let headpath = gitdir.join("HEAD");
let headstr = read_to_string(headpath)?;
let headref = headstr.split(' ').nth(1).unwrap().trim();
overridable_path("SYSCONFDIR", |env_sysconfdir| {
Some(join_if_relative(
&prefix,
env_sysconfdir.unwrap_or("/etc/".to_owned()),
))
});
// .git/refs/heads/branch contains the SHA
let refpath = gitdir.join(headref);
// Shorten to 9 characters (what git describe does currently)
read_to_string(refpath)?
} else if jjdir.exists() {
let output = Command::new("jj")
.args([
"log",
"--revisions",
"@",
"--no-graph",
"--ignore-working-copy",
"--template",
"commit_id",
])
.output()
.unwrap();
String::from_utf8_lossy(&output.stdout).to_string()
} else {
return Err("did not find either of .git or .jj".into());
};
let refstr = &commit_id[0..9];
let refstr = refstr.trim();
let datadir = overridable_path("DATADIR", |env_datadir| {
env_datadir.map(|p| join_if_relative(&prefix, p))
});
overridable_path("BINDIR", |env_bindir| {
env_bindir.map(|p| join_if_relative(&prefix, p))
});
overridable_path("DOCDIR", |env_docdir| {
env_docdir.map(|p| {
join_if_relative(
&datadir
.expect("Setting DOCDIR without setting DATADIR is not currently supported"),
p,
)
})
});
}
fn get_version() -> String {
use std::process::Command;
String::from_utf8(
Command::new("build_tools/git_version_gen.sh")
.output()
.unwrap()
.stdout,
)
.unwrap()
.trim_ascii_end()
.to_owned()
let version = env!("CARGO_PKG_VERSION").to_owned();
Ok(version + "-g" + refstr)
}
get_git_hash().expect("Could not get a version. Either set $FISH_BUILD_VERSION or install git.")
}

View File

@@ -8,22 +8,6 @@ if [ "$FISH_CHECK_LINT" = false ]; then
lint=false
fi
check_dependency_versions=false
if [ "${FISH_CHECK_DEPENDENCY_VERSIONS:-false}" != false ]; then
check_dependency_versions=true
fi
if $check_dependency_versions; then
command -v curl
command -v jq
command -v rustup
command -v uv
sort --version-sort </dev/null
# To match existing behavior, only check Rust/dockerfiles for now.
# TODO: remove this from this script.
updatecli diff --config=updatecli.d/docker.yml --config=updatecli.d/rust.yml
fi
cargo_args=$FISH_CHECK_CARGO_ARGS
target_triple=$FISH_CHECK_TARGET_TRIPLE
if [ -n "$target_triple" ]; then
@@ -33,18 +17,13 @@ fi
cargo() {
subcmd=$1
shift
if [ -n "$FISH_CHECK_RUST_TOOLCHAIN" ]; then
# shellcheck disable=2086
command cargo "+$FISH_CHECK_RUST_TOOLCHAIN" "$subcmd" $cargo_args "$@"
else
# shellcheck disable=2086
command cargo "$subcmd" $cargo_args "$@"
fi
# shellcheck disable=2086
command cargo "$subcmd" $cargo_args "$@"
}
cleanup () {
if [ -n "$gettext_template_dir" ] && [ -e "$gettext_template_dir" ]; then
rm -r "$gettext_template_dir"
if [ -n "$template_file" ] && [ -e "$template_file" ]; then
rm "$template_file"
fi
}
@@ -69,15 +48,9 @@ if [ -n "$FISH_TEST_MAX_CONCURRENCY" ]; then
export CARGO_BUILD_JOBS="$FISH_TEST_MAX_CONCURRENCY"
fi
gettext_template_dir=$(mktemp -d)
(
export FISH_GETTEXT_EXTRACTION_DIR="$gettext_template_dir"
cargo build --workspace --all-targets --features=gettext-extract
)
template_file=$(mktemp)
FISH_GETTEXT_EXTRACTION_FILE=$template_file cargo build --workspace --all-targets --features=gettext-extract
if $lint; then
if command -v cargo-deny >/dev/null; then
cargo deny --all-features --locked --exclude-dev check licenses
fi
PATH="$build_dir:$PATH" "$workspace_root/build_tools/style.fish" --all --check
for features in "" --no-default-features; do
cargo clippy --workspace --all-targets $features
@@ -86,9 +59,9 @@ fi
cargo test --no-default-features --workspace --all-targets
cargo test --doc --workspace
if $lint; then
cargo doc --workspace --no-deps
cargo doc --workspace
fi
FISH_GETTEXT_EXTRACTION_DIR=$gettext_template_dir "$workspace_root/tests/test_driver.py" "$build_dir"
FISH_GETTEXT_EXTRACTION_FILE=$template_file "$workspace_root/tests/test_driver.py" "$build_dir"
exit
}

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env fish
# Build a list of all sections in the html sphinx docs, separately by page,
# so it can be added to share/functions/help.fish
# Use like
# fish extract_help_sections.fish user_doc/html/{fish_for_bash_users.html,faq.html,interactive.html,language.html,tutorial.html}
# TODO: Currently `help` uses variable names we can't generate, so it needs to be touched up manually.
# Also this could easily be broken by changes in sphinx, ideally we'd have a way to let it print the section titles.
#
for file in $argv
set -l varname (string replace -r '.*/(.*).html' '$1' -- $file | string escape --style=var)pages
# Technically we can use any id in the document as an anchor, but listing them all is probably too much.
# Sphinx stores section titles (in a slug-ized form) in the id,
# and stores explicit section links in a `span` tag like
# `<span id="identifiers"></span>`
# We extract both separately.
set -l sections (string replace -rf '.*class="headerlink" href="#([^"]*)".*' '$1' <$file)
# Sections titled "id5" and such are internal cruft and shouldn't be offered.
set -a sections (string replace -rf '.*span id="([^"]*)".*' '$1' <$file | string match -rv 'id\d+')
set sections (printf '%s\n' $sections | sort -u)
echo set -l $varname $sections
end

View File

@@ -21,13 +21,13 @@ begin
set -g workspace_root (path resolve (status dirname)/..)
set -l rust_extraction_dir
set -l rust_extraction_file
if set -l --query _flag_use_existing_template
set rust_extraction_dir $_flag_use_existing_template
set rust_extraction_file $_flag_use_existing_template
else
set rust_extraction_dir (mktemp -d)
set rust_extraction_file (mktemp)
# We need to build to ensure that the proc macro for extracting strings runs.
FISH_GETTEXT_EXTRACTION_DIR=$rust_extraction_dir cargo check --features=gettext-extract
FISH_GETTEXT_EXTRACTION_FILE=$rust_extraction_file cargo check --no-default-features --features=gettext-extract
or exit 1
end
@@ -41,11 +41,11 @@ begin
mark_section tier1-from-rust
# Get rid of duplicates and sort.
find $rust_extraction_dir -type f -exec cat {} + | msguniq --no-wrap --sort-output
msguniq --no-wrap --sort-output $rust_extraction_file
or exit 1
if not set -l --query _flag_use_existing_template
rm -r $rust_extraction_dir
rm $rust_extraction_file
end
function extract_fish_script_messages_impl

View File

@@ -1,22 +1,70 @@
#!/bin/sh
# Originally from the git sources (GIT-VERSION-GEN)
# Presumably (C) Junio C Hamano <junkio@cox.net>
# Reused under GPL v2.0
# Modified for fish by David Adam <zanchey@ucc.gu.uwa.edu.au>
set -e
# Find the fish directory as two levels up from script directory.
FISH_BASE_DIR="$( cd "$( dirname "$( dirname "$0" )" )" && pwd )"
DEF_VER=unknown
git_permission_failed=0
version=$(
awk <"$FISH_BASE_DIR/Cargo.toml" -F'"' '$1 == "version = " { print $2 }'
)
if git_version=$(
GIT_CEILING_DIRECTORIES=$FISH_BASE_DIR/.. \
git -C "$FISH_BASE_DIR" describe --always --dirty 2>/dev/null); then
if [ "$git_version" = "${git_version#"$version"}" ]; then
version=$version-g$git_version
# First see if there is a version file (included in release tarballs),
# then try git-describe, then default.
if test -f version
then
VN=$(cat version) || VN="$DEF_VER"
else
if VN=$(git -C "$FISH_BASE_DIR" describe --always --dirty 2>/dev/null); then
:
else
version=$git_version
if test $? = 128; then
# Current git versions return status 128
# when run in a repo owned by another user.
# Even for describe and everything.
# This occurs for `sudo make install`.
git_permission_failed=1
fi
VN="$DEF_VER"
fi
fi
echo "$version"
# If the first param is --stdout, then output to stdout and exit.
if test "$1" = '--stdout'
then
echo $VN
exit 0
fi
# Set the output directory as either the first param or cwd.
test -n "$1" && OUTPUT_DIR=$1/ || OUTPUT_DIR=
FBVF="${OUTPUT_DIR}FISH-BUILD-VERSION-FILE"
if test "$VN" = unknown && test -r "$FBVF" && test "$git_permission_failed" = 1
then
# HACK: Git failed, so we keep the current version file.
# This helps in case you built fish as a normal user
# and then try to `sudo make install` it.
date +%s > "${OUTPUT_DIR}"fish-build-version-witness.txt
exit 0
fi
if test -r "$FBVF"
then
VC=$(cat "$FBVF")
else
VC="unset"
fi
# Maybe output the FBVF
# It looks like "2.7.1-621-ga2f065e6"
test "$VN" = "$VC" || {
echo >&2 "$VN"
echo "$VN" >"$FBVF"
}
# Output the fish-build-version-witness.txt
# See https://cmake.org/cmake/help/v3.4/policy/CMP0058.html
date +%s > "${OUTPUT_DIR}"fish-build-version-witness.txt

View File

@@ -0,0 +1,3 @@
# LSAN can detect leaks tracing back to __asan::AsanThread::ThreadStart (probably caused by our
# threads not exiting before their TLS dtors are called). Just ignore it.
leak:AsanThread

View File

@@ -24,12 +24,16 @@ SIGN=
NOTARIZE=
ARM64_DEPLOY_TARGET='MACOSX_DEPLOYMENT_TARGET=11.0'
X86_64_DEPLOY_TARGET='MACOSX_DEPLOYMENT_TARGET=10.12'
cmake_args=()
X86_64_DEPLOY_TARGET='MACOSX_DEPLOYMENT_TARGET=10.9'
while getopts "c:sf:i:p:e:nj:" opt; do
# As of this writing, the most recent Rust release supports macOS back to 10.12.
# The first supported version of macOS on arm64 is 10.15, so any Rust is fine for arm64.
# We wish to support back to 10.9 on x86-64; the last version of Rust to support that is
# version 1.73.0.
RUST_VERSION_X86_64=1.70.0
while getopts "sf:i:p:e:nj:" opt; do
case $opt in
c) cmake_args+=("$OPTARG");;
s) SIGN=1;;
f) P12_APP_FILE=$(realpath "$OPTARG");;
i) P12_INSTALL_FILE=$(realpath "$OPTARG");;
@@ -49,7 +53,7 @@ if [ -n "$NOTARIZE" ] && [ -z "$API_KEY_FILE" ]; then
usage
fi
VERSION=$(build_tools/git_version_gen.sh)
VERSION=$(build_tools/git_version_gen.sh --stdout 2>/dev/null)
echo "Version is $VERSION"
@@ -61,37 +65,42 @@ OUTPUT_PATH=${FISH_ARTEFACT_PATH:-~/fish_built}
mkdir -p "$PKGDIR/build_x86_64" "$PKGDIR/build_arm64" "$PKGDIR/root" "$PKGDIR/intermediates" "$PKGDIR/dst"
do_cmake() {
cmake \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_EXE_LINKER_FLAGS="-Wl,-ld_classic" \
-DCMAKE_OSX_ARCHITECTURES='arm64;x86_64' \
-DFISH_USE_SYSTEM_PCRE2=OFF \
"${cmake_args[@]}" \
"$@" \
"$SRC_DIR"
}
# Build and install for arm64.
# Pass FISH_USE_SYSTEM_PCRE2=OFF because a system PCRE2 on macOS will not be signed by fish,
# and will probably not be built universal, so the package will fail to validate/run on other systems.
# Note CMAKE_OSX_ARCHITECTURES is still relevant for the Mac app.
{ cd "$PKGDIR/build_arm64" \
&& do_cmake -DRust_CARGO_TARGET=aarch64-apple-darwin \
&& cmake \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_EXE_LINKER_FLAGS="-Wl,-ld_classic" \
-DRust_CARGO_TARGET=aarch64-apple-darwin \
-DCMAKE_OSX_ARCHITECTURES='arm64;x86_64' \
-DFISH_USE_SYSTEM_PCRE2=OFF \
"$SRC_DIR" \
&& env $ARM64_DEPLOY_TARGET make VERBOSE=1 -j 12 \
&& env DESTDIR="$PKGDIR/root/" $ARM64_DEPLOY_TARGET make install;
}
# Build for x86-64 but do not install; instead we will make a fat binary inside the root.
# Build for x86-64 but do not install; instead we will make some fat binaries inside the root.
# Set RUST_VERSION_X86_64 to the last version of Rust that supports macOS 10.9.
{ cd "$PKGDIR/build_x86_64" \
&& do_cmake -DRust_CARGO_TARGET=x86_64-apple-darwin \
&& cmake \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_EXE_LINKER_FLAGS="-Wl,-ld_classic" \
-DRust_TOOLCHAIN="$RUST_VERSION_X86_64" \
-DRust_CARGO_TARGET=x86_64-apple-darwin \
-DRust_COMPILER="$(rustup +$RUST_VERSION_X86_64 which rustc)" \
-DRust_CARGO="$(rustup +$RUST_VERSION_X86_64 which cargo)" \
-DCMAKE_OSX_ARCHITECTURES='arm64;x86_64' \
-DFISH_USE_SYSTEM_PCRE2=OFF "$SRC_DIR" \
&& env $X86_64_DEPLOY_TARGET make VERBOSE=1 -j 12; }
# Fatten it up.
FILE=$PKGDIR/root/usr/local/bin/fish
X86_FILE=$PKGDIR/build_x86_64/$(basename "$FILE")
rcodesign macho-universal-create --output "$FILE" "$FILE" "$X86_FILE"
chmod 755 "$FILE"
# Fatten them up.
for FILE in "$PKGDIR"/root/usr/local/bin/*; do
X86_FILE="$PKGDIR/build_x86_64/$(basename "$FILE")"
rcodesign macho-universal-create --output "$FILE" "$FILE" "$X86_FILE"
chmod 755 "$FILE"
done
if test -n "$SIGN"; then
echo "Signing executables"
@@ -104,7 +113,9 @@ if test -n "$SIGN"; then
if [ -n "$ENTITLEMENTS_FILE" ]; then
ARGS+=(--entitlements-xml-file "$ENTITLEMENTS_FILE")
fi
(set +x; rcodesign sign "${ARGS[@]}" "$PKGDIR"/root/usr/local/bin/fish)
for FILE in "$PKGDIR"/root/usr/local/bin/*; do
(set +x; rcodesign sign "${ARGS[@]}" "$FILE")
done
fi
pkgbuild --scripts "$SRC_DIR/build_tools/osx_package_scripts" --root "$PKGDIR/root/" --identifier 'com.ridiculousfish.fish-shell-pkg' --version "$VERSION" "$PKGDIR/intermediates/fish.pkg"
@@ -125,13 +136,15 @@ fi
(cd "$PKGDIR/build_arm64" && env $ARM64_DEPLOY_TARGET make -j 12 fish_macapp)
(cd "$PKGDIR/build_x86_64" && env $X86_64_DEPLOY_TARGET make -j 12 fish_macapp)
# Make the app's /usr/local/bin/fish binary universal. Note fish.app/Contents/MacOS/fish already is, courtesy of CMake.
# Make the app's /usr/local/bin binaries universal. Note fish.app/Contents/MacOS/fish already is, courtesy of CMake.
cd "$PKGDIR/build_arm64"
FILE=fish.app/Contents/Resources/base/usr/local/bin/fish
X86_FILE=$PKGDIR/build_x86_64/fish.app/Contents/Resources/base/usr/local/bin/$(basename "$FILE")
rcodesign macho-universal-create --output "$FILE" "$FILE" "$X86_FILE"
# macho-universal-create screws up the permissions.
chmod 755 "$FILE"
for FILE in fish.app/Contents/Resources/base/usr/local/bin/*; do
X86_FILE="$PKGDIR/build_x86_64/fish.app/Contents/Resources/base/usr/local/bin/$(basename "$FILE")"
rcodesign macho-universal-create --output "$FILE" "$FILE" "$X86_FILE"
# macho-universal-create screws up the permissions.
chmod 755 "$FILE"
done
if test -n "$SIGN"; then
echo "Signing app"

View File

@@ -1,43 +1,79 @@
#!/bin/sh
# Script to generate a tarball
# We use git to output a tree. But we also want to build the user documentation
# and put that in the tarball, so that nobody needs to have sphinx installed
# to build it.
# Outputs to $FISH_ARTEFACT_PATH or ~/fish_built by default
# Exit on error
set -e
# We will generate a tarball with a prefix "fish-VERSION"
# git can do that automatically for us via git-archive
# but to get the documentation in, we need to make a symlink called "fish-VERSION"
# and tar from that, so that the documentation gets the right prefix
# Use Ninja if available, as it automatically parallelises
BUILD_TOOL="make"
BUILD_GENERATOR="Unix Makefiles"
if command -v ninja >/dev/null; then
BUILD_TOOL="ninja"
BUILD_GENERATOR="Ninja"
fi
# We need GNU tar as that supports the --mtime and --transform options
TAR=notfound
for try in tar gtar gnutar; do
if $try -Pcf /dev/null --mtime now /dev/null >/dev/null 2>&1; then
TAR=$try
break
fi
done
if [ "$TAR" = "notfound" ]; then
echo 'No suitable tar (supporting --mtime) found as tar/gtar/gnutar in PATH'
exit 1
fi
# Get the current directory, which we'll use for symlinks
wd="$PWD"
# Get the version
VERSION=$(build_tools/git_version_gen.sh)
VERSION=$(build_tools/git_version_gen.sh --stdout 2>/dev/null)
prefix=fish-$VERSION
path=${FISH_ARTEFACT_PATH:-~/fish_built}/$prefix.tar.xz
# The name of the prefix, which is the directory that you get when you untar
prefix="fish-$VERSION"
tmpdir=$(mktemp -d)
manifest=$tmpdir/Cargo.toml
lockfile=$tmpdir/Cargo.lock
# The path where we will output the tar file
# Defaults to ~/fish_built
path=${FISH_ARTEFACT_PATH:-~/fish_built}/$prefix.tar
sed "s/^version = \".*\"\$/version = \"$VERSION\"/g" Cargo.toml \
>"$manifest"
awk -v version=$VERSION '
/^name = "fish"$/ { ok=1 }
ok == 1 && /^version = ".*"$/ {
ok = 2;
$0 = "version = \"" version "\"";
}
{print}
' \
Cargo.lock >"$lockfile"
# Clean up stuff we've written before
rm -f "$path" "$path".xz
git archive \
--prefix="$prefix/" \
--add-virtual-file="$prefix/Cargo.toml:$(cat "$manifest")" \
--add-virtual-file="$prefix/Cargo.lock:$(cat "$lockfile")" \
HEAD |
xz >"$path"
# git starts the archive
git archive --format=tar --prefix="$prefix"/ HEAD > "$path"
rm "$manifest"
rm "$lockfile"
rmdir "$tmpdir"
# tarball out the documentation, generate a version file
PREFIX_TMPDIR=$(mktemp -d)
cd "$PREFIX_TMPDIR"
echo "$VERSION" > version
cmake -G "$BUILD_GENERATOR" -DCMAKE_BUILD_TYPE=Debug "$wd"
$BUILD_TOOL doc
TAR_APPEND="$TAR --append --file=$path --mtime=now --owner=0 --group=0 \
--mode=g+w,a+rX --transform s/^/$prefix\//"
$TAR_APPEND --no-recursion user_doc
$TAR_APPEND user_doc/html user_doc/man
$TAR_APPEND version
cd -
rm -r "$PREFIX_TMPDIR"
# xz it
xz "$path"
# Output what we did, and the sha256 hash
echo "Tarball written to $path"
openssl dgst -sha256 "$path"
echo "Tarball written to $path".xz
openssl dgst -sha256 "$path".xz

View File

@@ -26,7 +26,7 @@ fi
wd="$PWD"
# Get the version from git-describe
VERSION=$(build_tools/git_version_gen.sh)
VERSION=$(build_tools/git_version_gen.sh --stdout 2>/dev/null)
# The name of the prefix, which is the directory that you get when you untar
prefix="fish-$VERSION"
@@ -42,14 +42,8 @@ rm -f "$path" "$path".xz
PREFIX_TMPDIR=$(mktemp -d)
cd "$PREFIX_TMPDIR"
# Add .cargo/config.toml. This means that the caller may need to remove that file from the tarball.
# See e4674cd7b5f (.cargo/config.toml: exclude from tarball, 2025-01-12)
mkdir .cargo
{
cat "$wd"/.cargo/config.toml
cargo vendor --manifest-path "$wd/Cargo.toml"
} > .cargo/config.toml
cargo vendor --manifest-path "$wd/Cargo.toml" > .cargo/config.toml
tar cfvJ "$path".xz vendor .cargo

View File

@@ -1,28 +1,28 @@
<html>
<head>
<style>
body {
font-family: system-ui, -apple-system, "Helvetica Neue", sans-serif;
font-size: 10pt;
}
code, tt {
font-family: ui-monospace, Menlo, monospace;
}
</style>
</head>
<body>
<p>
<strong>fish</strong> is a smart and user-friendly command line shell. For more information, visit <a href="https://fishshell.com">fishshell.com</a>.
</p>
<p>
<strong>fish</strong> will be installed into <tt>/usr/local/</tt>, and its path will be added to <wbr><tt>/etc/shells</tt> if necessary.
</p>
<p>
Your default shell will <em>not</em> be changed. To make <strong>fish</strong> your login shell after the installation, run:
</p>
<p>
<code>chsh -s /usr/local/bin/fish</code>
</p>
<p>Enjoy! Bugs can be reported on <a href="https://github.com/fish-shell/fish-shell/">GitHub</a>.</p>
</body>
<head>
<style>
body {
font-family: system-ui, -apple-system, "Helvetica Neue", sans-serif;
font-size: 10pt;
}
code, tt {
font-family: ui-monospace, Menlo, monospace;
}
</style>
</head>
<body>
<p>
<strong>fish</strong> is a smart and user-friendly command line shell. For more information, visit <a href="https://fishshell.com">fishshell.com</a>.
</p>
<p>
<strong>fish</strong> will be installed into <tt>/usr/local/</tt>, and its path will be added to <wbr><tt>/etc/shells</tt> if necessary.
</p>
<p>
Your default shell will <em>not</em> be changed. To make <strong>fish</strong> your login shell after the installation, run:
</p>
<p>
<code>chsh -s /usr/local/bin/fish</code>
</p>
<p>Enjoy! Bugs can be reported on <a href="https://github.org/fish-shell/fish-shell/">GitHub</a>.</p>
</body>
</html>

View File

@@ -4,8 +4,8 @@
if test $# -eq 0
then
echo "usage: $0 shellname [shellname ...]"
exit 1
echo "usage: $0 shellname [shellname ...]"
exit 1
fi
scriptname=$(basename "$0")

View File

@@ -11,38 +11,37 @@ mkdir -p "$relnotes_tmp/fake-workspace" "$relnotes_tmp/out"
cp -r doc_src CONTRIBUTING.rst README.rst "$relnotes_tmp/fake-workspace"
)
version=$(sed 's,^fish \(\S*\) .*,\1,; 1q' "$workspace_root/CHANGELOG.rst")
add_stats=false
# Skip on shallow clone (CI) for now.
if test -z "$CI" || [ "$(git -C "$workspace_root" tag | wc -l)" -gt 1 ]; then {
previous_version=$(
cd "$workspace_root"
git for-each-ref --format='%(objecttype) %(refname:strip=2)' refs/tags |
awk '/tag/ {print $2}' | sort --version-sort |
grep -vxF "$(git describe)" | tail -1
)
minor_version=${version%.*}
previous_minor_version=${previous_version%.*}
if [ "$minor_version" != "$previous_minor_version" ]; then
add_stats=true
fi
} fi
previous_version=$(
cd "$workspace_root"
awk <CHANGELOG.rst '
( /^fish \S*\.\S*\.\S* \(released .*\)$/ &&
NR > 1 &&
# Skip tags that have not been created yet..
system("git rev-parse --verify >/dev/null --quiet refs/tags/"$2) == 0 \
) {
print $2; ok = 1; exit
}
END { exit !ok }
'
)
minor_version=${version%.*}
previous_minor_version=${previous_version%.*}
{
sed -n 1,2p <"$workspace_root/CHANGELOG.rst"
if $add_stats; then {
ExtractCommitters() {
git log "$1" --format="%aN"
trailers='Co-authored-by|Signed-off-by'
git log "$1" --format="%b" | sed -En "/^($trailers):\s*/{s///;s/\s*<.*//;p}"
}
ListCommitters() {
comm "$@" "$relnotes_tmp/committers-then" "$relnotes_tmp/committers-now"
}
ListCommitters() {
comm "$@" "$relnotes_tmp/committers-then" "$relnotes_tmp/committers-now"
}
(
cd "$workspace_root"
git log "$previous_version" --format="%aN" | sort -u >"$relnotes_tmp/committers-then"
git log "$previous_version".. --format="%aN" | sort -u >"$relnotes_tmp/committers-now"
ListCommitters -13 >"$relnotes_tmp/committers-new"
ListCommitters -12 >"$relnotes_tmp/committers-returning"
)
if [ "$minor_version" != "$previous_minor_version" ]; then
(
cd "$workspace_root"
ExtractCommitters "$previous_version" | sort -u >"$relnotes_tmp/committers-then"
ExtractCommitters "$previous_version".. | sort -u >"$relnotes_tmp/committers-now"
ListCommitters -13 >"$relnotes_tmp/committers-new"
ListCommitters -12 >"$relnotes_tmp/committers-returning"
num_commits=$(git log --no-merges --format=%H "$previous_version".. | wc -l)
num_authors=$(wc -l <"$relnotes_tmp/committers-now")
num_new_authors=$(wc -l <"$relnotes_tmp/committers-new")
@@ -52,7 +51,7 @@ if test -z "$CI" || [ "$(git -C "$workspace_root" tag | wc -l)" -gt 1 ]; then {
echo
echo
)
} fi
fi
printf '%s\n' "$(awk <"$workspace_root/CHANGELOG.rst" '
NR <= 2 || /^\.\. ignore / { next }
@@ -61,9 +60,9 @@ if test -z "$CI" || [ "$(git -C "$workspace_root" tag | wc -l)" -gt 1 ]; then {
' | sed '$d')" |
sed -e '$s/^----*$//' # Remove spurious transitions at the end of the document.
if $add_stats; then {
if [ "$minor_version" != "$previous_minor_version" ]; then {
JoinEscaped() {
LC_CTYPE=C.UTF-8 sed 's/\S/\\&/g' |
sed 's/\S/\\&/g' |
awk '
NR != 1 { printf ",\n" }
{ printf "%s", $0 }
@@ -84,14 +83,9 @@ if test -z "$CI" || [ "$(git -C "$workspace_root" tag | wc -l)" -gt 1 ]; then {
echo
echo "---"
echo
echo 'Download links:'
echo 'To download the source code for fish, we suggest the file named ``fish-'"$version"'.tar.xz``.'
echo 'The file downloaded from ``Source code (tar.gz)`` will not build correctly.'
echo 'A GPG signature using `this key <'"${FISH_GPG_PUBLIC_KEY_URL:-???}"'>`__ is available as ``fish-'"$version"'.tar.xz.asc``.'
echo "*Download links: To download the source code for fish, we suggest the file named \"fish-$version.tar.xz\". The file downloaded from \"Source code (tar.gz)\" will not build correctly.*"
echo
echo 'The files called ``fish-'"$version"'-linux-*.tar.xz`` contain'
echo '`standalone fish binaries <https://github.com/fish-shell/fish-shell/?tab=readme-ov-file#building-fish-with-cargo>`__'
echo 'for any Linux with the given CPU architecture.'
echo "*The files called fish-$version-linux-\*.tar.xz are experimental packages containing a single standalone ``fish`` binary for any Linux with the given CPU architecture.*"
} >"$relnotes_tmp/fake-workspace"/CHANGELOG.rst
sphinx-build >&2 -j auto \
@@ -99,7 +93,7 @@ sphinx-build >&2 -j auto \
-d "$relnotes_tmp/doctree" "$relnotes_tmp/fake-workspace/doc_src" "$relnotes_tmp/out" \
-D markdown_http_base="https://fishshell.com/docs/$minor_version" \
-D markdown_uri_doc_suffix=".html" \
-D markdown_flavor=github \
-D markdown_github_flavored=1 \
"$@"
# Skip changelog header

View File

@@ -18,18 +18,11 @@ fi
[ -n "$version" ]
for tool in \
cmake \
bundle \
diff \
gh \
gpg \
jq \
ninja \
ruby \
tar \
timeout \
uv \
xz \
; do
if ! command -v "$tool" >/dev/null; then
echo >&2 "$0: missing command: $1"
@@ -37,14 +30,8 @@ for tool in \
fi
done
committer=$(git var GIT_AUTHOR_IDENT)
committer=${committer% *} # strip timezone
committer=${committer% *} # strip timestamp
gpg --local-user="$committer" --sign </dev/null >/dev/null
repo_root="$(dirname "$0")/.."
fish_site=$repo_root/../fish-site
fish_site_repo=git@github.com:$repository_owner/fish-site
for path in . "$fish_site"
do
@@ -55,13 +42,6 @@ do
fi
done
(
cd "$fish_site"
[ "$(git rev-parse HEAD)" = \
"$(git ls-remote "$fish_site_repo" refs/heads/master |
awk '{print $1}')" ]
)
if git tag | grep -qxF "$version"; then
echo >&2 "$0: tag $version already exists"
exit 1
@@ -83,37 +63,19 @@ sed -i \
-e "2c$(printf %s "$changelog_title" | sed s/./=/g)" \
CHANGELOG.rst
CreateCommit() {
git commit -m "$1
CommitVersion() {
sed -i "s/^version = \".*\"/version = \"$1\"/g" Cargo.toml
cargo fetch --offline
git add CHANGELOG.rst Cargo.toml Cargo.lock
git commit -m "$2
Created by ./build_tools/release.sh $version"
}
sed -i "s/^version = \".*\"/version = \"$1\"/g" Cargo.toml
cargo fetch --offline # bumps the version in Cargo.lock
if [ "$1" = "$version" ]; then
# debchange is a Debian script to manage the Debian changelog, but
# it's too annoying to install everywhere. Just do it by hand.
cat - contrib/debian/changelog > contrib/debian/changelog.new <<EOF
fish (${version}-1) stable; urgency=medium
CommitVersion "$version" "Release $version"
* Release of new version $version.
See https://github.com/fish-shell/fish-shell/releases/tag/$version for details.
-- $committer $(date -R)
EOF
mv contrib/debian/changelog.new contrib/debian/changelog
git add contrib/debian/changelog
fi
git add CHANGELOG.rst Cargo.toml Cargo.lock
CreateCommit "Release $version"
# Tags must be full objects, not lightweight tags, for
# git_version-gen.sh to work.
git -c "user.signingKey=$committer" \
tag --sign --message="Release $version" $version
# N.B. this is not GPG-signed.
git tag --annotate --message="Release $version" $version
git push $remote $version
@@ -138,21 +100,13 @@ done
# Update fishshell.com
tag_oid=$(git rev-parse "$version")
tmpdir=$(mktemp -d)
fish_tar_xz=fish-$version.tar.xz
(
local_tarball=$tmpdir/local-tarball
mkdir "$local_tarball"
FISH_ARTEFACT_PATH=$local_tarball ./build_tools/make_tarball.sh
cd "$local_tarball"
tar xf "$fish_tar_xz"
)
# TODO This works on draft releases only if "gh" is configured to
# have write access to the fish-shell repository. Unless we are fine
# publishing the release at this point, we should at least fail if
# "gh" doesn't have write access.
while ! \
gh release download "$version" --dir="$tmpdir" \
--pattern="$fish_tar_xz"
--pattern="fish-$version.tar.xz"
do
TIMEOUT=30 gh run watch "$run_id" ||:
sleep 5
@@ -160,26 +114,10 @@ done
actual_tag_oid=$(git ls-remote "$remote" |
awk '$2 == "refs/tags/'"$version"'" { print $1 }')
[ "$tag_oid" = "$actual_tag_oid" ]
(
cd "$tmpdir"
tar xf "$fish_tar_xz"
diff -ur "fish-$version" "local-tarball/fish-$version"
gpg --local-user="$committer" --sign --detach --armor \
"$fish_tar_xz"
gh release upload "$version" "$fish_tar_xz.asc"
)
(
cd "$tmpdir/local-tarball/fish-$version"
uv --no-managed-python venv
. .venv/bin/activate
cmake -GNinja -DCMAKE_BUILD_TYPE=Debug .
ninja doc
)
( cd "$tmpdir" && tar xf fish-$version.tar.xz )
CopyDocs() {
rm -rf "$fish_site/site/docs/$1"
cp -r "$tmpdir/local-tarball/fish-$version/cargo/fish-docs/html" "$fish_site/site/docs/$1"
cp -r "$tmpdir/fish-$version/user_doc/html" "$fish_site/site/docs/$1"
git -C $fish_site add "site/docs/$1"
}
minor_version=${version%.*}
@@ -215,7 +153,7 @@ gh_api_repo() {
command gh api \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"/repos/$repository_owner/fish-shell/$path" \
"/repos/$repository_owner/fish-shell/$1" \
"$@"
}
@@ -266,9 +204,7 @@ done
" | sed 's,^\s*| \?,,')"
# This takes care to support remote names that are different from
# fish-shell remote name. Also, support detached HEAD state.
git push "$fish_site_repo" HEAD:master
git fetch "$fish_site_repo" \
"$(git rev-parse HEAD):refs/remotes/origin/master"
git push git@github.com:$repository_owner/fish-site HEAD:master
)
if [ -n "$integration_branch" ]; then {
@@ -281,32 +217,27 @@ fish ?.?.? (released ???)
EOF
)
printf %s\\n "$changelog" >CHANGELOG.rst
git add CHANGELOG.rst
CreateCommit "start new cycle"
CommitVersion ${version}-snapshot "start new cycle"
git push $remote HEAD:master
} fi
milestone_version="$(
if echo "$version" | grep -q '\.0$'; then
echo "$minor_version"
else
echo "$version"
fi
)"
milestone_number() {
milestone_number=$(
gh_api_repo milestones?state=open |
jq --arg name "fish $1" '
.[] | select(.title == $name) | .number
'
}
gh_api_repo milestones/"$(milestone_number "$milestone_version")" \
--method PATCH --raw-field state=closed
jq '.[] | select(.title == "fish '"$version"'") | .number'
)
gh_api_repo --method PATCH milestones/$milestone_number \
--raw-field state=closed
next_minor_version=$(echo "$minor_version" |
awk -F. '{ printf "%s.%s", $1, $2+1 }')
if [ -z "$(milestone_number "$next_minor_version")" ]; then
gh_api_repo milestones --method POST \
--raw-field title="fish $next_minor_version"
next_patch_version=$(
echo "$version" | awk -F. '
NF == 3 && $3 ~ /[0-9]+/ {
printf "%s.%s.%s", $1, $2, $3+1
}
'
)
if [ -n "$next_patch_version" ]; then
gh_api_repo --method POST milestones \
--raw-field title="fish $next_patch_version"
fi
exit

View File

@@ -24,7 +24,7 @@ if set -l -q _flag_all
end
end
set -l workspace_root (realpath (status dirname)/..)
set -l workspace_root (status dirname)/..
if test $all = yes
if not set -l -q _flag_force; and not set -l -q _flag_check
@@ -44,7 +44,7 @@ if test $all = yes
end
end
set fish_files $workspace_root/{benchmarks,build_tools,etc,share}/**.fish
set python_files $workspace_root
set python_files {doc_src,share,tests}/**.py
else
# Format the files specified as arguments.
set -l files $argv
@@ -58,11 +58,6 @@ set -l green (set_color green)
set -l yellow (set_color yellow)
set -l normal (set_color normal)
function die -V red -V normal
echo $red$argv[1]$normal
exit 1
end
if set -q fish_files[1]
if not type -q fish_indent
echo
@@ -71,52 +66,58 @@ if set -q fish_files[1]
end
echo === Running "$green"fish_indent"$normal"
if set -l -q _flag_check
fish_indent --check -- $fish_files
or die "Fish files are not formatted correctly."
if not fish_indent --check -- $fish_files
echo $red"Fish files are not formatted correctly."$normal
exit 1
end
else
fish_indent -w -- $fish_files
end
end
if set -q python_files[1]
if not type -q ruff
if not type -q black
echo
echo $yellow'Please install `ruff` to style python'$normal
echo $yellow'Please install `black` to style python'$normal
exit 127
end
echo === Running "$green"ruff format"$normal"
echo === Running "$green"black"$normal"
if set -l -q _flag_check
ruff format --check $python_files
or die "Python files are not formatted correctly."
if not black --check $python_files
echo $red"Python files are not formatted correctly."$normal
exit 1
end
else
ruff format $python_files
black $python_files
end
end
if test $all = yes; or set -q rust_files[1]
if not cargo fmt --version >/dev/null
echo
echo $yellow'Please install "rustfmt" to style Rust, e.g. via:'
echo "rustup component add rustfmt"$normal
exit 127
end
set -l edition_spec string match -r '^edition\s*=.*'
test "$($edition_spec <Cargo.toml)" = "$($edition_spec <.rustfmt.toml)"
or die "Cargo.toml and .rustfmt.toml use different editions"
echo === Running "$green"rustfmt"$normal"
if set -l -q _flag_check
if test $all = yes
cargo fmt --all --check
else
rustfmt --check --files-with-diff $rust_files
if not cargo fmt --version >/dev/null
echo
echo $yellow'Please install "rustfmt" to style Rust, e.g. via:'
echo "rustup component add rustfmt"$normal
exit 127
end
echo === Running "$green"rustfmt"$normal"
if set -l -q _flag_check
if set -l -q _flag_all
if not cargo fmt --check
echo $red"Rust files are not formatted correctly."$normal
exit 1
end
or die "Rust files are not formatted correctly."
else
if test $all = yes
cargo fmt --all
else
if set -q rust_files[1]
if not rustfmt --check --files-with-diff $rust_files
echo $red"Rust files are not formatted correctly."
exit 1
end
end
end
else
if set -l -q _flag_all
cargo fmt
else
if set -q rust_files[1]
rustfmt $rust_files
end
end

View File

@@ -1,22 +0,0 @@
# /// script
# requires-python = ">=3.5"
# dependencies = [
# "launchpadlib",
# ]
# ///
from launchpadlib.launchpad import Launchpad
if __name__ == "__main__":
launchpad = Launchpad.login_anonymously(
"fish shell build script", "production", "~/.cache", version="devel"
)
ubu = launchpad.projects("ubuntu")
print(
"\n".join(
x["name"]
for x in ubu.series.entries
if x["supported"] == True
and x["name"] not in ("trusty", "xenial", "bionic", "focal")
)
)

View File

@@ -1,60 +0,0 @@
#!/bin/sh
set -ex
command -v curl
command -v gcloud
command -v jq
command -v rustup
command -v updatecli
command -v uv
sort --version-sort </dev/null
# TODO This is copied from .github/actions/install-sphinx/action.yml
uv lock --check --exclude-newer="$(awk -F'"' <uv.lock '/^exclude-newer[[:space:]]*=/ {print $2}')"
update_gh_action() {
repo=$1
version=$(curl -fsS "https://api.github.com/repos/$repo/releases/latest" | jq -r .tag_name)
[ -n "$version" ]
tag_oid=$(git ls-remote "https://github.com/$repo.git" "refs/tags/$version" | cut -f1)
[ -n "$tag_oid" ]
find .github/workflows -name '*.yml' -type f -exec \
sed -i "s|uses: $repo@\S\+\( \+#.*\)\?|\
uses: $repo@$tag_oid # $version, build_tools/update-dependencies.sh|g" {} +
}
update_gh_action actions/checkout
update_gh_action actions/github-script
update_gh_action actions/upload-artifact
update_gh_action actions/download-artifact
update_gh_action docker/login-action
update_gh_action docker/build-push-action
update_gh_action docker/metadata-action
update_gh_action EmbarkStudios/cargo-deny-action
update_gh_action dessant/lock-threads
update_gh_action softprops/action-gh-release
update_gh_action msys2/setup-msys2
updatecli "${@:-apply}"
# Python version constraints may have changed.
uv lock --upgrade --exclude-newer="$(date --date='7 days ago' --iso-8601)"
from_gh() {
repo=$1
path=$2
destination=$3
contents=$(curl -fsS https://raw.githubusercontent.com/"${repo}"/refs/heads/master/"${path}")
printf '%s\n' >"$destination" "$contents"
}
from_gh ridiculousfish/widecharwidth widechar_width.rs crates/widecharwidth/src/widechar_width.rs
from_gh ridiculousfish/littlecheck littlecheck/littlecheck.py tests/littlecheck.py
from_gh catppuccin/fish 'themes/Catppuccin Frappe.theme' share/themes/catppuccin-frappe.theme
from_gh catppuccin/fish 'themes/Catppuccin Macchiato.theme' share/themes/catppuccin-macchiato.theme
from_gh catppuccin/fish 'themes/Catppuccin Mocha.theme' share/themes/catppuccin-mocha.theme
# Update Cargo.lock
cargo update
# Update Cargo.toml and Cargo.lock
cargo +nightly -Zunstable-options update --breaking

View File

@@ -9,32 +9,32 @@
# For developers:
# - Run with no args to update all PO files after making changes to Rust/fish sources.
# For translators:
# - Specify the language you want to work on as an argument, which must be a file in the
# localization/po/ directory. You can specify a language which does not have translations
# yet by specifying the name of a file which does not yet exist.
# Make sure to follow the naming convention.
# - Specify the language you want to work on as an argument, which must be a file in the po/
# directory. You can specify a language which does not have translations yet by specifying the
# name of a file which does not yet exist. Make sure to follow the naming convention.
# For testing:
# - Specify `--dry-run` to see if any updates to the PO files would by applied by this script.
# If this flag is specified, the script will exit with an error if there are outstanding
# changes, and will display the diff. Do not specify other flags if `--dry-run` is specified.
#
# Specify `--use-existing-template=DIR` to prevent running cargo for extracting an up-to-date
# Specify `--use-existing-template=FILE` to prevent running cargo for extracting an up-to-date
# version of the localized strings. This flag is intended for testing setups which make it
# inconvenient to run cargo here, but run it in an earlier step to ensure up-to-date values.
# This argument is passed on to the `fish_xgettext.fish` script and has no other uses.
# `DIR` must be the path to a gettext template file generated from our compilation process.
# `FILE` must be the path to a gettext template file generated from our compilation process.
# It can be obtained by running:
# set -l DIR (mktemp -d)
# FISH_GETTEXT_EXTRACTION_DIR=$DIR cargo check --features=gettext-extract
# set -l FILE (mktemp)
# FISH_GETTEXT_EXTRACTION_FILE=$FILE cargo check --features=gettext-extract
# The sort utility is locale-sensitive.
# Ensure that sorting output is consistent by setting LC_ALL here.
set -gx LC_ALL C.UTF-8
set -l build_tools (status dirname)
set -l po_dir $build_tools/../localization/po
set -l po_dir $build_tools/../po
set -l extract
set -l po
argparse dry-run use-existing-template= -- $argv
or exit $status
@@ -90,9 +90,8 @@ if set -l --query extract
end
if set -l --query _flag_dry_run
# On a dry run, we do not modify localization/po/ but write to a temporary directory instead
# and check if there is a difference between localization/po/ and the tmpdir after re-generating
# the PO files.
# On a dry run, we do not modify po/ but write to a temporary directory instead and check if
# there is a difference between po/ and the tmpdir after re-generating the PO files.
set -g tmpdir (mktemp -d)
# Ensure tmpdir has the same initial state as the po dir.
@@ -135,20 +134,22 @@ for po_file in $po_files
if set --query tmpdir[1]
set po_file $tmpdir/(basename $po_file)
end
if test -e $po_file
merge_po_files $template_file $po_file
else
begin
print_header
cat $template_file
end >$po_file
if set -l --query po
if test -e $po_file
merge_po_files $template_file $po_file
else
begin
print_header
cat $template_file
end >$po_file
end
end
end
if set -g --query tmpdir[1]
diff -ur $po_dir $tmpdir
or begin
echo ERROR: translations in localization/po/ are stale. Try running build_tools/update_translations.fish
echo ERROR: translations in ./po/ are stale. Try running build_tools/update_translations.fish
cleanup_exit
end
end

View File

@@ -1,16 +0,0 @@
#!/bin/bash
set -euo pipefail
channel=$1 # e.g. stable, testing
package=$2 # e.g. rustc, sphinx
codename=$(
curl -fsS https://ftp.debian.org/debian/dists/"${channel}"/Release |
grep '^Codename:' | cut -d' ' -f2)
curl -fsS https://sources.debian.org/api/src/"${package}"/ |
jq -r --arg codename "${codename}" '
.versions[] | select(.suites[] == $codename) | .version' |
sed 's/^\([0-9]\+\.[0-9]\+\).*/\1/' |
sort --version-sort |
tail -1

View File

@@ -1,59 +1,86 @@
find_program(SPHINX_EXECUTABLE NAMES sphinx-build
HINTS
$ENV{SPHINX_DIR}
PATH_SUFFIXES bin
DOC "Sphinx documentation generator")
include(FeatureSummary)
set(SPHINX_OUTPUT_DIR "${FISH_RUST_BUILD_DIR}/fish-docs")
set(FISH_INDENT_FOR_BUILDING_DOCS "" CACHE FILEPATH "Path to fish_indent executable for building HTML docs")
if(FISH_INDENT_FOR_BUILDING_DOCS)
set(SPHINX_HTML_FISH_INDENT_DEP)
else()
set(FISH_INDENT_FOR_BUILDING_DOCS "${CMAKE_CURRENT_BINARY_DIR}/fish_indent")
set(SPHINX_HTML_FISH_INDENT_DEP fish_indent)
endif()
set(VARS_FOR_CARGO_SPHINX_WRAPPER
"CARGO_TARGET_DIR=${FISH_RUST_BUILD_DIR}"
"FISH_SPHINX=${SPHINX_EXECUTABLE}"
)
set(SPHINX_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/doc_src")
set(SPHINX_ROOT_DIR "${CMAKE_CURRENT_BINARY_DIR}/user_doc")
set(SPHINX_BUILD_DIR "${SPHINX_ROOT_DIR}/build")
set(SPHINX_HTML_DIR "${SPHINX_ROOT_DIR}/html")
set(SPHINX_MANPAGE_DIR "${SPHINX_ROOT_DIR}/man")
# sphinx-docs uses fish_indent for highlighting.
# Prepend the output dir of fish_indent to PATH.
add_custom_target(sphinx-docs
COMMAND env ${VARS_FOR_CARGO_SPHINX_WRAPPER}
${Rust_CARGO} xtask html-docs --fish-indent=${FISH_INDENT_FOR_BUILDING_DOCS}
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
DEPENDS ${SPHINX_HTML_FISH_INDENT_DEP}
mkdir -p ${SPHINX_HTML_DIR}/_static/
COMMAND env PATH="${CMAKE_BINARY_DIR}:$$PATH"
${SPHINX_EXECUTABLE}
-j auto
-q -b html
-c "${SPHINX_SRC_DIR}"
-d "${SPHINX_ROOT_DIR}/.doctrees-html"
"${SPHINX_SRC_DIR}"
"${SPHINX_HTML_DIR}"
DEPENDS ${SPHINX_SRC_DIR}/fish_indent_lexer.py fish_indent
COMMENT "Building HTML documentation with Sphinx")
# sphinx-manpages needs the fish_indent binary for the version number
add_custom_target(sphinx-manpages
COMMAND env ${VARS_FOR_CARGO_SPHINX_WRAPPER}
${Rust_CARGO} xtask man-pages
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
env FISH_BUILD_VERSION_FILE=${CMAKE_CURRENT_BINARY_DIR}/${FBVF}
${SPHINX_EXECUTABLE}
-j auto
-q -b man
-c "${SPHINX_SRC_DIR}"
-d "${SPHINX_ROOT_DIR}/.doctrees-man"
"${SPHINX_SRC_DIR}"
# TODO: This only works if we only have section 1 manpages.
"${SPHINX_MANPAGE_DIR}/man1"
DEPENDS CHECK-FISH-BUILD-VERSION-FILE
COMMENT "Building man pages with Sphinx")
if(NOT DEFINED WITH_DOCS) # Don't check for legacy options if the new one is defined, to help bisecting.
if(DEFINED BUILD_DOCS)
message(FATAL_ERROR "the BUILD_DOCS option is no longer supported, use -DWITH_DOCS=ON|OFF")
endif()
if(DEFINED INSTALL_DOCS)
message(FATAL_ERROR "the INSTALL_DOCS option is no longer supported, use -DWITH_DOCS=ON|OFF")
endif()
endif()
if(SPHINX_EXECUTABLE)
option(WITH_DOCS "build documentation (requires Sphinx)" ON)
else()
option(WITH_DOCS "build documentation (requires Sphinx)" OFF)
endif()
option(BUILD_DOCS "build documentation (requires Sphinx)" ON)
else(SPHINX_EXECUTABLE)
option(BUILD_DOCS "build documentation (requires Sphinx)" OFF)
endif(SPHINX_EXECUTABLE)
if(WITH_DOCS AND NOT SPHINX_EXECUTABLE)
if(BUILD_DOCS AND NOT SPHINX_EXECUTABLE)
message(FATAL_ERROR "build documentation selected, but sphinx-build could not be found")
endif()
add_feature_info(Documentation WITH_DOCS "user manual and documentation")
if(IS_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/user_doc/html
AND IS_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/user_doc/man)
set(HAVE_PREBUILT_DOCS TRUE)
else()
set(HAVE_PREBUILT_DOCS FALSE)
endif()
if(WITH_DOCS)
if(BUILD_DOCS OR HAVE_PREBUILT_DOCS)
set(INSTALL_DOCS ON)
else()
set(INSTALL_DOCS OFF)
endif()
add_feature_info(Documentation INSTALL_DOCS "user manual and documentation")
if(BUILD_DOCS)
configure_file("${SPHINX_SRC_DIR}/conf.py" "${SPHINX_BUILD_DIR}/conf.py" @ONLY)
add_custom_target(doc ALL
DEPENDS sphinx-docs sphinx-manpages)
# Group docs targets into a DocsTargets folder
set_property(TARGET doc sphinx-docs sphinx-manpages
PROPERTY FOLDER cmake/DocTargets)
endif()
elseif(HAVE_PREBUILT_DOCS)
if(NOT CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_CURRENT_BINARY_DIR)
# Out of tree build - link the prebuilt documentation to the build tree
add_custom_target(link_doc ALL)
add_custom_command(TARGET link_doc
COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_CURRENT_SOURCE_DIR}/user_doc ${CMAKE_CURRENT_BINARY_DIR}/user_doc
POST_BUILD)
endif()
endif(BUILD_DOCS)

View File

@@ -1,5 +1,7 @@
set(CMAKE_INSTALL_MESSAGE NEVER)
set(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/fish ${CMAKE_CURRENT_BINARY_DIR}/fish_indent ${CMAKE_CURRENT_BINARY_DIR}/fish_key_reader)
set(prefix ${CMAKE_INSTALL_PREFIX})
set(bindir ${CMAKE_INSTALL_BINDIR})
set(sysconfdir ${CMAKE_INSTALL_SYSCONFDIR})
@@ -28,18 +30,18 @@ set(extra_confdir
# These are the man pages that go in system manpath; all manpages go in the fish-specific manpath.
set(MANUALS ${SPHINX_OUTPUT_DIR}/man/man1/fish.1
${SPHINX_OUTPUT_DIR}/man/man1/fish_indent.1
${SPHINX_OUTPUT_DIR}/man/man1/fish_key_reader.1
${SPHINX_OUTPUT_DIR}/man/man1/fish-doc.1
${SPHINX_OUTPUT_DIR}/man/man1/fish-tutorial.1
${SPHINX_OUTPUT_DIR}/man/man1/fish-language.1
${SPHINX_OUTPUT_DIR}/man/man1/fish-interactive.1
${SPHINX_OUTPUT_DIR}/man/man1/fish-terminal-compatibility.1
${SPHINX_OUTPUT_DIR}/man/man1/fish-completions.1
${SPHINX_OUTPUT_DIR}/man/man1/fish-prompt-tutorial.1
${SPHINX_OUTPUT_DIR}/man/man1/fish-for-bash-users.1
${SPHINX_OUTPUT_DIR}/man/man1/fish-faq.1
set(MANUALS ${CMAKE_CURRENT_BINARY_DIR}/user_doc/man/man1/fish.1
${CMAKE_CURRENT_BINARY_DIR}/user_doc/man/man1/fish_indent.1
${CMAKE_CURRENT_BINARY_DIR}/user_doc/man/man1/fish_key_reader.1
${CMAKE_CURRENT_BINARY_DIR}/user_doc/man/man1/fish-doc.1
${CMAKE_CURRENT_BINARY_DIR}/user_doc/man/man1/fish-tutorial.1
${CMAKE_CURRENT_BINARY_DIR}/user_doc/man/man1/fish-language.1
${CMAKE_CURRENT_BINARY_DIR}/user_doc/man/man1/fish-interactive.1
${CMAKE_CURRENT_BINARY_DIR}/user_doc/man/man1/fish-terminal-compatibility.1
${CMAKE_CURRENT_BINARY_DIR}/user_doc/man/man1/fish-completions.1
${CMAKE_CURRENT_BINARY_DIR}/user_doc/man/man1/fish-prompt-tutorial.1
${CMAKE_CURRENT_BINARY_DIR}/user_doc/man/man1/fish-for-bash-users.1
${CMAKE_CURRENT_BINARY_DIR}/user_doc/man/man1/fish-faq.1
)
# Determine which man page we don't want to install.
@@ -73,30 +75,22 @@ function(FISH_TRY_CREATE_DIRS)
endforeach()
endfunction(FISH_TRY_CREATE_DIRS)
install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/fish
install(PROGRAMS ${PROGRAMS}
PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ
GROUP_EXECUTE WORLD_READ WORLD_EXECUTE
DESTINATION ${bindir})
if(NOT IS_ABSOLUTE ${bindir})
set(abs_bindir "\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${bindir}")
else()
set(abs_bindir "\$ENV{DESTDIR}${bindir}")
endif()
install(CODE "file(CREATE_LINK ${abs_bindir}/fish ${abs_bindir}/fish_indent)")
install(CODE "file(CREATE_LINK ${abs_bindir}/fish ${abs_bindir}/fish_key_reader)")
fish_create_dirs(${sysconfdir}/fish/conf.d ${sysconfdir}/fish/completions
${sysconfdir}/fish/functions)
install(FILES etc/config.fish DESTINATION ${sysconfdir}/fish/)
fish_create_dirs(${rel_datadir}/fish ${rel_datadir}/fish/completions
${rel_datadir}/fish/functions
${rel_datadir}/fish/functions ${rel_datadir}/fish/groff
${rel_datadir}/fish/man/man1 ${rel_datadir}/fish/tools
${rel_datadir}/fish/tools/web_config
${rel_datadir}/fish/tools/web_config/js
${rel_datadir}/fish/prompts
${rel_datadir}/fish/themes
${rel_datadir}/fish/tools/web_config/sample_prompts
${rel_datadir}/fish/tools/web_config/themes
)
configure_file(share/__fish_build_paths.fish.in share/__fish_build_paths.fish)
@@ -114,9 +108,9 @@ configure_file(fish.pc.in fish.pc.noversion @ONLY)
add_custom_command(OUTPUT fish.pc
COMMAND sed '/Version/d' fish.pc.noversion > fish.pc
COMMAND printf "Version: " >> fish.pc
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/build_tools/git_version_gen.sh >> fish.pc
COMMAND cat ${FBVF} >> fish.pc
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/fish.pc.noversion)
DEPENDS CHECK-FISH-BUILD-VERSION-FILE ${CMAKE_CURRENT_BINARY_DIR}/fish.pc.noversion)
add_custom_target(build_fish_pc ALL DEPENDS fish.pc)
@@ -131,23 +125,18 @@ install(DIRECTORY share/functions/
DESTINATION ${rel_datadir}/fish/functions
FILES_MATCHING PATTERN "*.fish")
install(DIRECTORY share/prompts/
DESTINATION ${rel_datadir}/fish/prompts
FILES_MATCHING PATTERN "*.fish")
install(DIRECTORY share/themes/
DESTINATION ${rel_datadir}/fish/themes
FILES_MATCHING PATTERN "*.theme")
install(DIRECTORY share/groff
DESTINATION ${rel_datadir}/fish)
# CONDEMNED_PAGE is managed by the conditional above
# Building the man pages is optional: if sphinx isn't installed, they're not built
install(DIRECTORY ${SPHINX_OUTPUT_DIR}/man/man1/
install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/user_doc/man/man1/
DESTINATION ${rel_datadir}/fish/man/man1
FILES_MATCHING
PATTERN "*.1"
PATTERN ${CONDEMNED_PAGE} EXCLUDE)
install(PROGRAMS share/tools/create_manpage_completions.py
install(PROGRAMS share/tools/create_manpage_completions.py share/tools/deroff.py
DESTINATION ${rel_datadir}/fish/tools/)
install(DIRECTORY share/tools/web_config
@@ -157,16 +146,18 @@ install(DIRECTORY share/tools/web_config
PATTERN "*.css"
PATTERN "*.html"
PATTERN "*.py"
PATTERN "*.js")
PATTERN "*.js"
PATTERN "*.theme"
PATTERN "*.fish")
# Building the man pages is optional: if Sphinx isn't installed, they're not built
install(FILES ${MANUALS} DESTINATION ${mandir}/man1/ OPTIONAL)
install(DIRECTORY ${SPHINX_OUTPUT_DIR}/html/ # Trailing slash is important!
install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/user_doc/html/ # Trailing slash is important!
DESTINATION ${docdir} OPTIONAL)
install(FILES CHANGELOG.rst DESTINATION ${docdir})
# Group install targets into a InstallTargets folder
set_property(TARGET build_fish_pc
set_property(TARGET build_fish_pc CHECK-FISH-BUILD-VERSION-FILE
PROPERTY FOLDER cmake/InstallTargets)
# Make a target build_root that installs into the buildroot directory, for testing.

View File

@@ -26,7 +26,7 @@ add_executable(fish_macapp EXCLUDE_FROM_ALL
# so cmake must be re-run after version changes for the app to be updated. But
# generally this will be run by make_macos_pkg.sh which always re-runs cmake.
execute_process(
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/build_tools/git_version_gen.sh
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/build_tools/git_version_gen.sh --stdout
COMMAND cut -d- -f1
OUTPUT_VARIABLE FISH_SHORT_VERSION
OUTPUT_STRIP_TRAILING_WHITESPACE)

View File

@@ -1,11 +1,11 @@
include(FeatureSummary)
include(FindRust)
find_package(Rust REQUIRED)
set(FISH_RUST_BUILD_DIR "${CMAKE_BINARY_DIR}/cargo")
set(FISH_RUST_BUILD_DIR "${CMAKE_BINARY_DIR}/cargo/build")
if(DEFINED ASAN)
list(APPEND CARGO_FLAGS "-Z" "build-std")
list(APPEND FISH_CARGO_FEATURES_LIST "asan")
endif()
if(DEFINED TSAN)
list(APPEND CARGO_FLAGS "-Z" "build-std")
@@ -19,18 +19,28 @@ else()
endif()
set(rust_profile $<IF:$<CONFIG:Debug>,debug,$<IF:$<CONFIG:RelWithDebInfo>,release-with-debug,release>>)
set(rust_debugflags "$<$<CONFIG:Debug>:-g>$<$<CONFIG:RelWithDebInfo>:-g>")
if (NOT DEFINED WITH_MESSAGE_LOCALIZATION) # Don't check for legacy options if the new one is defined, to help bisecting.
if(DEFINED WITH_GETTEXT)
message(FATAL_ERROR "the WITH_GETTEXT option is no longer supported, use -DWITH_MESSAGE_LOCALIZATION=ON|OFF")
endif()
endif()
option(WITH_MESSAGE_LOCALIZATION "Build with localization support. Requires `msgfmt` to work." ON)
option(WITH_GETTEXT "Build with gettext localization support. Requires `msgfmt` to work." ON)
# Enable gettext feature unless explicitly disabled.
if(NOT DEFINED WITH_MESSAGE_LOCALIZATION OR "${WITH_MESSAGE_LOCALIZATION}")
if(NOT DEFINED WITH_GETTEXT OR "${WITH_GETTEXT}")
list(APPEND FISH_CARGO_FEATURES_LIST "localize-messages")
endif()
add_feature_info(Translation WITH_MESSAGE_LOCALIZATION "message localization (requires gettext)")
list(JOIN FISH_CARGO_FEATURES_LIST , FISH_CARGO_FEATURES)
# Tell Cargo where our build directory is so it can find Cargo.toml.
set(VARS_FOR_CARGO
"FISH_BUILD_DIR=${CMAKE_BINARY_DIR}"
"PREFIX=${CMAKE_INSTALL_PREFIX}"
# Cheesy so we can tell cmake was used to build
"CMAKE=1"
"DOCDIR=${CMAKE_INSTALL_FULL_DOCDIR}"
"DATADIR=${CMAKE_INSTALL_FULL_DATADIR}"
"SYSCONFDIR=${CMAKE_INSTALL_FULL_SYSCONFDIR}"
"BINDIR=${CMAKE_INSTALL_FULL_BINDIR}"
"CARGO_TARGET_DIR=${FISH_RUST_BUILD_DIR}"
"CARGO_BUILD_RUSTC=${Rust_COMPILER}"
"${FISH_PCRE2_BUILDFLAG}"
"RUSTFLAGS=$ENV{RUSTFLAGS} ${rust_debugflags}"
)

View File

@@ -1,3 +1,5 @@
add_executable(fish_test_helper tests/fish_test_helper.c)
FILE(GLOB FISH_CHECKS CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/tests/checks/*.fish)
foreach(CHECK ${FISH_CHECKS})
get_filename_component(CHECK_NAME ${CHECK} NAME)
@@ -6,7 +8,7 @@ foreach(CHECK ${FISH_CHECKS})
COMMAND ${CMAKE_SOURCE_DIR}/tests/test_driver.py ${CMAKE_CURRENT_BINARY_DIR}
checks/${CHECK_NAME}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests
DEPENDS fish fish_indent fish_key_reader
DEPENDS fish fish_indent fish_key_reader fish_test_helper
USES_TERMINAL
)
endforeach(CHECK)
@@ -19,7 +21,7 @@ foreach(PEXPECT ${PEXPECTS})
COMMAND ${CMAKE_SOURCE_DIR}/tests/test_driver.py ${CMAKE_CURRENT_BINARY_DIR}
pexpects/${PEXPECT}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests
DEPENDS fish fish_indent fish_key_reader
DEPENDS fish fish_indent fish_key_reader fish_test_helper
USES_TERMINAL
)
endforeach(PEXPECT)
@@ -55,16 +57,8 @@ endif()
add_custom_target(fish_run_tests
# TODO: This should be replaced with a unified solution, possibly build_tools/check.sh.
COMMAND ${CMAKE_SOURCE_DIR}/tests/test_driver.py ${max_concurrency_flag} ${CMAKE_CURRENT_BINARY_DIR}
COMMAND env ${VARS_FOR_CARGO}
${Rust_CARGO}
test
--no-default-features
--features=${FISH_CARGO_FEATURES}
${CARGO_FLAGS}
--workspace
--target-dir ${rust_target_dir}
${cargo_test_flags}
COMMAND env ${VARS_FOR_CARGO} cargo test --no-default-features ${CARGO_FLAGS} --workspace --target-dir ${rust_target_dir} ${cargo_test_flags}
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
DEPENDS fish fish_indent fish_key_reader
DEPENDS fish fish_indent fish_key_reader fish_test_helper
USES_TERMINAL
)

55
cmake/Version.cmake Normal file
View File

@@ -0,0 +1,55 @@
# This file adds commands to manage the FISH-BUILD-VERSION-FILE (hereafter
# FBVF). This file exists in the build directory and is used to populate the
# documentation and also the version string in fish_version.o (printed with
# `echo $version` and also fish --version). The essential idea is that we are
# going to invoke git_version_gen.sh, which will update the
# FISH-BUILD-VERSION-FILE only if it needs to change; this is what makes
# incremental rebuilds fast.
#
# This code is delicate, with the chief subtlety revolving around Ninja. A
# natural and naive approach would tell the generated build system that FBVF is
# a dependency of fish_version.o, and that git_version_gen.sh updates it. Make
# will then invoke the script, check the timestamp on fish_version.o and FBVF,
# see that FBVF is earlier, and then not rebuild fish_version.o. Ninja,
# however, decides what to build up-front and will unconditionally rebuild
# fish_version.o.
#
# To avoid this with Ninja, we want to hook into its 'restat' option which we
# can do through the BYPRODUCTS feature of CMake. See
# https://cmake.org/cmake/help/latest/policy/CMP0058.html
#
# Unfortunately BYPRODUCTS behaves strangely with the Makefile generator: it
# marks FBVF as generated and then CMake itself will `touch` it on every build,
# meaning that using BYPRODUCTS will cause fish_version.o to be rebuilt
# unconditionally with the Makefile generator. Thus we want to use the
# natural-and-naive approach for Makefiles.
# **IMPORTANT** If you touch these build rules, please test both Ninja and
# Makefile generators with both a clean and dirty git tree. Verify that both
# generated build systems rebuild fish when the git tree goes from dirty to
# clean (and vice versa), and verify they do NOT rebuild it when the git tree
# stays the same (incremental builds must be fast).
# Just a handy abbreviation.
set(FBVF FISH-BUILD-VERSION-FILE)
# TODO: find a cleaner way to do this.
IF (${CMAKE_GENERATOR} STREQUAL Ninja)
set(FBVF-OUTPUT fish-build-version-witness.txt)
set(CFBVF-BYPRODUCTS ${FBVF})
else(${CMAKE_GENERATOR} STREQUAL Ninja)
set(FBVF-OUTPUT ${FBVF})
set(CFBVF-BYPRODUCTS)
endif(${CMAKE_GENERATOR} STREQUAL Ninja)
# Set up the version targets
add_custom_target(CHECK-FISH-BUILD-VERSION-FILE
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/build_tools/git_version_gen.sh ${CMAKE_CURRENT_BINARY_DIR}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
BYPRODUCTS ${CFBVF-BYPRODUCTS})
add_custom_command(OUTPUT ${FBVF-OUTPUT}
DEPENDS CHECK-FISH-BUILD-VERSION-FILE)
# Abbreviation for the target.
set(CFBVF CHECK-FISH-BUILD-VERSION-FILE)

View File

@@ -1,504 +0,0 @@
fish (4.5.0-1) stable; urgency=medium
* Release of new version 4.5.0.
See https://github.com/fish-shell/fish-shell/releases/tag/4.5.0 for details.
-- Johannes Altmanninger <aclopte@gmail.com> Tue, 17 Feb 2026 11:32:33 +1100
fish (4.4.0-1) stable; urgency=medium
* Release of new version 4.4.0.
See https://github.com/fish-shell/fish-shell/releases/tag/4.4.0 for details.
-- Johannes Altmanninger <aclopte@gmail.com> Tue, 03 Feb 2026 12:11:51 +1100
fish (4.3.3-1) stable; urgency=medium
* Release of new version 4.3.3.
See https://github.com/fish-shell/fish-shell/releases/tag/4.3.3 for details.
-- Johannes Altmanninger <aclopte@gmail.com> Wed, 07 Jan 2026 08:34:20 +0100
fish (4.3.2-1) stable; urgency=medium
* Release of new version 4.3.2.
See https://github.com/fish-shell/fish-shell/releases/tag/4.3.2 for details.
-- Johannes Altmanninger <aclopte@gmail.com> Tue, 30 Dec 2025 17:21:04 +0100
fish (4.3.1-1) stable; urgency=medium
* Release of new version 4.3.1.
See https://github.com/fish-shell/fish-shell/releases/tag/4.3.1 for details.
-- Johannes Altmanninger <aclopte@gmail.com> Sun, 28 Dec 2025 16:54:44 +0100
fish (4.3.0-1) stable; urgency=medium
* Release of new version 4.3.0.
See https://github.com/fish-shell/fish-shell/releases/tag/4.3.0 for details.
-- Johannes Altmanninger <aclopte@gmail.com> Sun, 28 Dec 2025 10:20:47 +0100
fish (4.2.1-1) testing; urgency=medium
* Release of new version 4.2.1.
See https://github.com/fish-shell/fish-shell/releases/tag/4.2.1 for details.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Thu, 13 Nov 2025 20:42:43 +0800
fish (4.2.0-1) testing; urgency=medium
* Release of new version 4.2.0.
See https://github.com/fish-shell/fish-shell/releases/tag/4.2.0 for details.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Mon, 10 Nov 2025 19:29:03 +0800
fish (4.1.2-1) testing; urgency=medium
* Release of new version 4.1.2.
See https://github.com/fish-shell/fish-shell/releases/tag/4.1.2 for details.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Wed, 08 Oct 2025 13:46:45 +0800
fish (4.1.1-1) testing; urgency=medium
* Release of new version 4.1.1.
See https://github.com/fish-shell/fish-shell/releases/tag/4.1.1 for details.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Fri, 03 Oct 2025 16:43:43 +0800
fish (4.0.8-1) testing; urgency=medium
* Release of new version 4.0.8.
See https://github.com/fish-shell/fish-shell/releases/tag/4.0.8 for details.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Thu, 18 Sep 2025 22:17:43 +0800
fish (4.0.6-1) testing; urgency=medium
* Release of new version 4.0.6.
See https://github.com/fish-shell/fish-shell/releases/tag/4.0.6 for details.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Wed, 17 Sep 2025 12:27:09 +0800
fish (4.0.2-2) testing; urgency=medium
* Fix tests on Debian.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Sun, 20 Apr 2025 23:08:14 +0800
fish (4.0.2-1) testing; urgency=medium
* Release of new version 4.0.2.
See https://github.com/fish-shell/fish-shell/releases/tag/4.0.2 for details.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Sun, 20 Apr 2025 21:24:18 +0800
fish (4.0.1-1) testing; urgency=medium
* Release of new version 4.0.1.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Thu, 13 Mar 2025 11:30:21 +0800
fish (4.0.0-2) testing; urgency=medium
* Fix tests on Debian.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Thu, 27 Feb 2025 21:50:33 +0800
fish (4.0.0-1) testing; urgency=medium
* Release of new version 4.0.0.
See https://github.com/fish-shell/fish-shell/releases/tag/4.0.0 for details.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Thu, 27 Feb 2025 19:22:30 +0800
fish (4.0.1-1) testing; urgency=medium
* Release of new beta version 4.0b1.
See https://github.com/fish-shell/fish-shell/releases/tag/4.0b1 for details.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Tue, 17 Dec 2024 23:42:25 +0800
fish (3.7.1-1) testing; urgency=medium
* Release of new version 3.7.1.
See https://github.com/fish-shell/fish-shell/releases/tag/3.7.1 for details.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Tue, 19 Mar 2024 13:26:22 +0800
fish (3.7.0-1) testing; urgency=medium
* Release of new version 3.7.0.
See https://github.com/fish-shell/fish-shell/releases/tag/3.7.0 for details.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Mon, 01 Jan 2024 23:32:55 +0800
fish (3.6.4-1) testing; urgency=medium
* Release of new version 3.6.4.
See https://github.com/fish-shell/fish-shell/releases/tag/3.6.4 for details.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Tue, 05 Dec 2023 22:34:09 +0800
fish (3.6.3-1) testing; urgency=medium
* Release of new version 3.6.3.
See https://github.com/fish-shell/fish-shell/releases/tag/3.6.3 for details.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Tue, 05 Dec 2023 00:11:12 +0800
fish (3.6.2-1) testing; urgency=medium
* Release of new version 3.6.2.
* Includes a fix for CVE-2023-49284.
See https://github.com/fish-shell/fish-shell/releases/tag/3.6.2 for details.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Mon, 04 Dec 2023 23:16:42 +0800
fish (3.6.1-1) testing; urgency=medium
* Release of new version 3.6.1.
See https://github.com/fish-shell/fish-shell/releases/tag/3.6.1 for
significant changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Sat, 25 Mar 2023 17:22:12 +0800
fish (3.6.0-1) testing; urgency=medium
* Release of new version 3.6.0.
See https://github.com/fish-shell/fish-shell/releases/tag/3.6.0 for
significant changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Sat, 07 Jan 2023 22:41:32 +0800
fish (3.5.1-1) testing; urgency=medium
* Release of new version 3.5.1.
See https://github.com/fish-shell/fish-shell/releases/tag/3.5.1 for
significant changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Wed, 20 Jul 2022 21:54:09 +0800
fish (3.5.0-1) testing; urgency=medium
* Release of new version 3.5.0.
See https://github.com/fish-shell/fish-shell/releases/tag/3.5.0 for
significant changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Thu, 16 Jun 2022 19:45:33 +0800
fish (3.4.0-1) testing; urgency=medium
* Release of new version 3.4.0.
See https://github.com/fish-shell/fish-shell/releases/tag/3.4.0 for
significant changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Sat, 12 Mar 2022 23:24:22 +0800
fish (3.3.1-1) testing; urgency=medium
* Release of new version 3.3.1.
See https://github.com/fish-shell/fish-shell/releases/tag/3.3.1 for
significant changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Tue, 06 Jul 2021 23:22:36 +0800
fish (3.3.0-1) testing; urgency=medium
* Release of new version 3.3.0.
See https://github.com/fish-shell/fish-shell/releases/tag/3.3.0 for
significant changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Mon, 28 Jun 2021 23:06:36 +0800
fish (3.2.2-1) testing; urgency=medium
* Release of new version 3.2.2.
See https://github.com/fish-shell/fish-shell/releases/tag/3.2.2 for
significant changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Wed, 07 Apr 2021 21:10:54 +0800
fish (3.2.1-1) testing; urgency=medium
* Release of new version 3.2.1.
See https://github.com/fish-shell/fish-shell/releases/tag/3.2.1 for
significant changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Thu, 18 Mar 2021 12:08:13 +0800
fish (3.2.0-1) testing; urgency=medium
* Release of new version 3.2.0.
See https://github.com/fish-shell/fish-shell/releases/tag/3.2.0 for
significant changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Mon, 01 Mar 2021 21:22:39 +0800
fish (3.1.2-1) testing; urgency=medium
* Release of new version 3.1.2.
See https://github.com/fish-shell/fish-shell/releases/tag/3.1.2 for
significant changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Wed, 29 Apr 2020 11:22:35 +0800
fish (3.1.1-1) testing; urgency=medium
* Release of new version 3.1.1.
See https://github.com/fish-shell/fish-shell/releases/tag/3.1.1 for
significant changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Mon, 27 Apr 2020 22:45:35 +0800
fish (3.1.0-1) testing; urgency=medium
* Release of new version 3.1.0.
See https://github.com/fish-shell/fish-shell/releases/tag/3.1.0 for
significant changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Wed, 12 Feb 2020 22:34:53 +0800
fish (3.1.1-1) testing; urgency=medium
* Release of new beta version 3.1b1.
See https://github.com/fish-shell/fish-shell/releases/tag/3.1b1 for
significant changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Sun, 26 Jan 2020 21:42:46 +0800
fish (3.0.2-1) testing; urgency=medium
* Release of new version 3.0.2.
See https://github.com/fish-shell/fish-shell/releases/tag/3.0.2 for
significant changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Tue, 19 Feb 2019 21:45:05 +0800
fish (3.0.1-1) testing; urgency=medium
* Release of new version 3.0.1.
See https://github.com/fish-shell/fish-shell/releases/tag/3.0.1 for
significant changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Mon, 11 Feb 2019 20:23:55 +0800
fish (3.0.0-1) testing; urgency=medium
* Release of new version 3.0.0.
See https://github.com/fish-shell/fish-shell/releases/tag/3.0.0 for
significant changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Fri, 28 Dec 2018 21:10:28 +0800
fish (3.0.1-1) testing; urgency=medium
* Release of new beta version 3.0b1.
See https://github.com/fish-shell/fish-shell/releases/tag/3.0b1 for
significant changes, which includes backward incompatibility.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Tue, 11 Dec 2018 22:59:15 +0800
fish (2.7.1-1) testing; urgency=medium
* Release of new bug fix version 2.7.1. On all Linux platforms, this is
release will behave identically to 2.7.0.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Sat, 23 Dec 2017 00:43:12 +0800
fish (2.7.0-1) testing; urgency=medium
* Release of new version 2.7.0.
See https://github.com/fish-shell/fish-shell/releases/tag/2.7.0 for
significant changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Thu, 23 Nov 2017 18:38:21 +0800
fish (2.7.1-1) testing; urgency=medium
* Release of new beta version 2.7b1.
See https://github.com/fish-shell/fish-shell/releases/tag/2.7b1 for
significant changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Tue, 31 Oct 2017 20:32:29 +0800
fish (2.6.0-1) testing; urgency=medium
* Relase of new version 2.6.0.
See https://github.com/fish-shell/fish-shell/releases/tag/2.6.0 for
significant changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Sat, 03 Jun 2017 20:51:50 +0800
fish (2.6b1-1) testing; urgency=medium
* Release of new beta version 2.6b1.
See https://github.com/fish-shell/fish-shell/releases/tag/2.6b1 for
significant changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Sun, 14 May 2017 10:47:39 +0800
fish (2.5.0-1) testing; urgency=medium
* Release of new version 2.5.0.
See https://github.com/fish-shell/fish-shell/releases/tag/2.5.0 for
significant changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Fri, 03 Feb 2017 09:52:57 +0800
fish (2.5b1-1) testing; urgency=medium
* Release of new beta version 2.5b1.
See https://github.com/fish-shell/fish-shell/releases/tag/2.5b1 for
significant changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Sat, 14 Jan 2017 08:49:34 +0800
fish (2.4.0-2) testing; urgency=medium
* Change recommendation of xdg-utils to suggestion (closes
https://github.com/fish-shell/fish-shell/issues/3534).
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Wed, 09 Nov 2016 22:56:16 +0800
fish (2.4.0-1) testing; urgency=medium
* Release of new version 2.4.0.
See https://github.com/fish-shell/fish-shell/releases/tag/2.4.0 for
significant changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Tue, 08 Nov 2016 11:29:57 +0800
fish (2.4b1-1) testing; urgency=medium
* Release of new beta version 2.4b1.
See https://github.com/fish-shell/fish-shell/releases/tag/2.4b1 for
significant changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Tue, 18 Oct 2016 22:25:26 +0800
fish (2.3.1-1) testing; urgency=medium
* Release of new version 2.3.1.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Sun, 03 Jul 2016 21:21:51 +0800
fish (2.3.0-1) testing; urgency=medium
* Release of new version 2.3.0.
See http://fishshell.com/release_notes.html for significant changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Sat, 21 May 2016 06:59:54 +0800
fish (2.3b2-1) testing; urgency=medium
* Release of new beta version 2.3b2.
See https://github.com/fish-shell/fish-shell/releases/tag/2.3b2 for
significant changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Thu, 05 May 2016 06:22:37 +0800
fish (2.3.1-1) testing; urgency=medium
* Release of new beta version 2.3b1.
See http://github.com/fish-shell/fish-shell/releases/tag/2.3b1 for
significant changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Tue, 19 Apr 2016 21:07:17 +0800
fish (2.2.0-2) testing; urgency=medium
* Binary rebuild only to resynchronise repository state.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Mon, 15 Feb 2016 22:45:05 +0800
fish (2.2.0-1) testing; urgency=medium
* Release of new version 2.2.0.
See http://fishshell.com/release_notes.html for significant changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Sun, 12 Jul 2015 18:52:29 +0800
fish (2.2b1-1) testing; urgency=low
* Release of new beta version 2.2b1.
See http://fishshell.com/staging/release_notes.html for significant
changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Thu, 07 May 2015 14:48:21 +0800
fish (2.1.1-1) unstable; urgency=high
* Release of new version 2.1.1.
See http://fishshell.com/release_notes.html for significant changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Sun, 07 Sep 2014 17:06:31 +0800
fish (2.1.0-1) unstable; urgency=low
* Release of new version 2.1.0.
See http://fishshell.com/staging/release_notes.html for significant
changes.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Sat, 19 Oct 2013 14:35:23 +0800
fish (2.0.0-0) unstable; urgency=low
* Initial release of fish 2.0.0.
-- David Adam <zanchey@ucc.gu.uwa.edu.au> Thu, 19 Jul 2012 23:17:58 +0800

View File

@@ -1,3 +0,0 @@
# The vendor tarball drops a new version of .cargo/config into place. Representing this as a patch
# in automated workflows is tricky, so for our purposes auto-commit is fine.
auto-commit

View File

@@ -4,7 +4,6 @@ edition.workspace = true
rust-version.workspace = true
version = "0.0.0"
repository.workspace = true
license.workspace = true
[dependencies]
rsconf.workspace = true

View File

@@ -1,21 +1,4 @@
use std::{borrow::Cow, env, os::unix::ffi::OsStrExt as _, path::Path};
pub fn env_var(name: &str) -> Option<String> {
let err = match env::var(name) {
Ok(p) => return Some(p),
Err(err) => err,
};
use env::VarError::*;
match err {
NotPresent => None,
NotUnicode(os_string) => {
panic!(
"Environment variable {name} is not valid Unicode: {:?}",
os_string.as_bytes()
)
}
}
}
use std::{borrow::Cow, env, path::Path};
pub fn workspace_root() -> &'static Path {
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
@@ -29,15 +12,12 @@ fn cargo_target_dir() -> Cow<'static, Path> {
}
pub fn fish_build_dir() -> Cow<'static, Path> {
option_env!("FISH_CMAKE_BINARY_DIR")
// FISH_BUILD_DIR is set by CMake, if we are using it.
option_env!("FISH_BUILD_DIR")
.map(|d| Cow::Borrowed(Path::new(d)))
.unwrap_or(cargo_target_dir())
}
pub fn fish_doc_dir() -> Cow<'static, Path> {
cargo_target_dir().join("fish-docs").into()
}
// TODO Move this to rsconf
pub fn rebuild_if_path_changed<P: AsRef<Path>>(path: P) {
rsconf::rebuild_if_path_changed(path.as_ref().to_str().unwrap());
@@ -49,56 +29,3 @@ pub fn rebuild_if_paths_changed<P: AsRef<Path>, I: IntoIterator<Item = P>>(paths
rsconf::rebuild_if_path_changed(path.as_ref().to_str().unwrap());
}
}
pub fn rebuild_if_embedded_path_changed<P: AsRef<Path>>(path: P) {
// Not necessary in debug builds, where rust-embed reads from the filesystem.
if cfg!(any(not(debug_assertions), windows)) {
rebuild_if_path_changed(path);
}
}
// Target OS for compiling our crates, as opposed to the build script.
pub fn target_os() -> String {
env_var("CARGO_CFG_TARGET_OS").unwrap()
}
pub fn target_os_is_apple() -> bool {
matches!(target_os().as_str(), "ios" | "macos")
}
/// Detect if we're being compiled for a BSD-derived OS, allowing targeting code conditionally with
/// `#[cfg(bsd)]`.
///
/// Rust offers fine-grained conditional compilation per-os for the popular operating systems, but
/// doesn't necessarily include less-popular forks nor does it group them into families more
/// specific than "windows" vs "unix" so we can conditionally compile code for BSD systems.
pub fn target_os_is_bsd() -> bool {
let target_os = target_os();
let is_bsd = target_os.ends_with("bsd") || target_os == "dragonfly";
if matches!(
target_os.as_str(),
"dragonfly" | "freebsd" | "netbsd" | "openbsd"
) {
assert!(is_bsd, "Target incorrectly detected as not BSD!");
}
is_bsd
}
pub fn target_os_is_cygwin() -> bool {
target_os() == "cygwin"
}
#[macro_export]
macro_rules! as_os_strs {
[ $( $x:expr, )* ] => {
{
use std::ffi::OsStr;
fn as_os_str<S: AsRef<OsStr> + ?Sized>(s: &S) -> &OsStr {
s.as_ref()
}
&[
$( as_os_str($x), )*
]
}
}
}

View File

@@ -4,7 +4,6 @@ edition.workspace = true
rust-version.workspace = true
version = "0.0.0"
repository.workspace = true
license.workspace = true
[build-dependencies]
fish-build-helper.workspace = true

View File

@@ -1,49 +1,53 @@
#[cfg(not(clippy))]
use std::path::Path;
use fish_build_helper::{as_os_strs, fish_doc_dir};
fn main() {
let sec1_dir = fish_doc_dir().join("man").join("man1");
// Running `cargo clippy` on a clean build directory panics, because when rust-embed
// tries to embed a directory which does not exist it will panic.
let _ = std::fs::create_dir_all(&sec1_dir);
if !cfg!(clippy) {
build_man(&sec1_dir);
}
let mandir = fish_build_helper::fish_build_dir().join("fish-man");
let sec1dir = mandir.join("man1");
// Running `cargo clippy` on a clean build directory panics, because when rust-embed tries to
// embed a directory which does not exist it will panic.
let _ = std::fs::create_dir_all(sec1dir.to_str().unwrap());
#[cfg(not(clippy))]
build_man(&mandir);
}
fn build_man(sec1_dir: &Path) {
use fish_build_helper::{env_var, workspace_root};
use std::process::{Command, Stdio};
#[cfg(not(clippy))]
fn build_man(man_dir: &Path) {
use std::{
env,
process::{Command, Stdio},
};
use fish_build_helper::workspace_root;
let workspace_root = workspace_root();
let doc_src_dir = workspace_root.join("doc_src");
let doctrees_dir = fish_doc_dir().join(".doctrees-man");
fish_build_helper::rebuild_if_paths_changed([
&workspace_root.join("CHANGELOG.rst"),
&workspace_root.join("CONTRIBUTING.rst"),
&doc_src_dir,
]);
let man_str = man_dir.to_str().unwrap();
let args = as_os_strs![
"-j",
"auto",
"-q",
"-b",
"man",
"-c",
&doc_src_dir,
let sec1_dir = man_dir.join("man1");
let sec1_str = sec1_dir.to_str().unwrap();
let docsrc_dir = workspace_root.join("doc_src");
let docsrc_str = docsrc_dir.to_str().unwrap();
let sphinx_doc_sources = [
workspace_root.join("CHANGELOG.rst"),
workspace_root.join("CONTRIBUTING.rst"),
docsrc_dir.clone(),
];
fish_build_helper::rebuild_if_paths_changed(sphinx_doc_sources);
let args = &[
"-j", "auto", "-q", "-b", "man", "-c", docsrc_str,
// doctree path - put this *above* the man1 dir to exclude it.
// this is ~6M
"-d",
&doctrees_dir,
&doc_src_dir,
&sec1_dir,
"-d", man_str, docsrc_str, sec1_str,
];
let _ = std::fs::create_dir_all(sec1_str);
rsconf::rebuild_if_env_changed("FISH_BUILD_DOCS");
if env_var("FISH_BUILD_DOCS") == Some("0".to_owned()) {
if env::var("FISH_BUILD_DOCS") == Ok("0".to_string()) {
rsconf::warn!("Skipping man pages because $FISH_BUILD_DOCS is set to 0");
return;
}
@@ -53,24 +57,18 @@ fn build_man(sec1_dir: &Path) {
// - if we skipped the docs with sphinx not installed, installing it would not then build the docs.
// That means you need to explicitly set $FISH_BUILD_DOCS=0 (`FISH_BUILD_DOCS=0 cargo install --path .`),
// which is unfortunate - but the docs are pretty important because they're also used for --help.
let sphinx_build = match Command::new(option_env!("FISH_SPHINX").unwrap_or("sphinx-build"))
let sphinx_build = match Command::new("sphinx-build")
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
{
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
assert_ne!(
env_var("FISH_BUILD_DOCS"),
Some("1".to_owned()),
"Could not find sphinx-build required to build man pages.\n\
Install Sphinx or disable building the docs by setting $FISH_BUILD_DOCS=0."
);
rsconf::warn!(
"Could not find sphinx-build required to build man pages. \
If you install Sphinx now, you need to trigger a rebuild to include man pages. \
For example by running `touch doc_src` followed by the build command."
);
if env::var("FISH_BUILD_DOCS") == Ok("1".to_string()) {
panic!("Could not find sphinx-build to build man pages.\nInstall sphinx or disable building the docs by setting $FISH_BUILD_DOCS=0.");
}
rsconf::warn!("Cannot find sphinx-build to build man pages.");
rsconf::warn!("If you install it now you need to run `cargo clean` and rebuild, or set $FISH_BUILD_DOCS=1 explicitly.");
return;
}
Err(e) => {
@@ -92,10 +90,9 @@ fn build_man(sec1_dir: &Path) {
rsconf::warn!("sphinx-build: {}", String::from_utf8_lossy(&out.stderr));
}
assert_eq!(&String::from_utf8_lossy(&out.stdout), "");
assert!(
out.status.success(),
"sphinx-build failed to build the man pages."
);
if !out.status.success() {
panic!("sphinx-build failed to build the man pages.");
}
}
}
}

View File

@@ -1,14 +0,0 @@
[package]
name = "fish-color"
edition.workspace = true
rust-version.workspace = true
version = "0.0.0"
repository.workspace = true
license.workspace = true
[dependencies]
fish-common.workspace = true
fish-widestring.workspace = true
[lints]
workspace = true

View File

@@ -1,16 +0,0 @@
[package]
name = "fish-common"
edition.workspace = true
rust-version.workspace = true
version = "0.0.0"
repository.workspace = true
license.workspace = true
[dependencies]
bitflags.workspace = true
fish-widestring.workspace = true
libc.workspace = true
nix.workspace = true
[lints]
workspace = true

View File

@@ -1,709 +0,0 @@
use bitflags::bitflags;
use fish_widestring::{L, char_offset, wstr};
use libc::{SIG_IGN, SIGTTOU, STDIN_FILENO};
use std::cell::{Cell, RefCell};
use std::io::Read;
use std::ops::{Deref, DerefMut};
use std::os::fd::{AsRawFd, BorrowedFd, RawFd};
use std::os::unix::ffi::OsStrExt as _;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicI32, AtomicU32, Ordering};
use std::{env, mem, time};
pub const PACKAGE_NAME: &str = env!("CARGO_PKG_NAME");
// Highest legal ASCII value.
pub const ASCII_MAX: char = 127 as char;
// Highest legal 16-bit Unicode value.
pub const UCS2_MAX: char = '\u{FFFF}';
// Highest legal byte value.
pub const BYTE_MAX: char = 0xFF as char;
// Unicode BOM value.
pub const UTF8_BOM_WCHAR: char = '\u{FEFF}';
// Use Unicode "non-characters" for internal characters as much as we can. This
// gives us 32 "characters" for internal use that we can guarantee should not
// appear in our input stream. See http://www.unicode.org/faq/private_use.html.
pub const RESERVED_CHAR_BASE: char = '\u{FDD0}';
pub const RESERVED_CHAR_END: char = '\u{FDF0}';
// Split the available non-character values into two ranges to ensure there are
// no conflicts among the places we use these special characters.
pub const EXPAND_RESERVED_BASE: char = RESERVED_CHAR_BASE;
pub const EXPAND_RESERVED_END: char = char_offset(EXPAND_RESERVED_BASE, 16);
pub const WILDCARD_RESERVED_BASE: char = EXPAND_RESERVED_END;
pub const WILDCARD_RESERVED_END: char = char_offset(WILDCARD_RESERVED_BASE, 16);
// Make sure the ranges defined above don't exceed the range for non-characters.
// This is to make sure we didn't do something stupid in subdividing the
// Unicode range for our needs.
const _: () = assert!(WILDCARD_RESERVED_END <= RESERVED_CHAR_END);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EscapeStringStyle {
Script(EscapeFlags),
Url,
Var,
Regex,
}
impl Default for EscapeStringStyle {
fn default() -> Self {
Self::Script(EscapeFlags::default())
}
}
impl TryFrom<&wstr> for EscapeStringStyle {
type Error = &'static wstr;
fn try_from(s: &wstr) -> Result<Self, Self::Error> {
use EscapeStringStyle::*;
match s {
s if s == "script" => Ok(Self::default()),
s if s == "var" => Ok(Var),
s if s == "url" => Ok(Url),
s if s == "regex" => Ok(Regex),
_ => Err(L!("Invalid escape style")),
}
}
}
bitflags! {
/// Flags for the [`escape_string()`] function. These are only applicable when the escape style is
/// [`EscapeStringStyle::Script`].
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub struct EscapeFlags: u32 {
/// Do not escape special fish syntax characters like the semicolon. Only escape non-printable
/// characters and backslashes.
const NO_PRINTABLES = 1 << 0;
/// Do not try to use 'simplified' quoted escapes, and do not use empty quotes as the empty
/// string.
const NO_QUOTED = 1 << 1;
/// Do not escape tildes.
const NO_TILDE = 1 << 2;
/// Replace non-printable control characters with Unicode symbols.
const SYMBOLIC = 1 << 3;
/// Escape ,
const COMMA = 1 << 4;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnescapeStringStyle {
Script(UnescapeFlags),
Url,
Var,
}
impl Default for UnescapeStringStyle {
fn default() -> Self {
Self::Script(UnescapeFlags::default())
}
}
impl TryFrom<&wstr> for UnescapeStringStyle {
type Error = &'static wstr;
fn try_from(s: &wstr) -> Result<Self, Self::Error> {
use UnescapeStringStyle::*;
match s {
s if s == "script" => Ok(Self::default()),
s if s == "var" => Ok(Var),
s if s == "url" => Ok(Url),
_ => Err(L!("Invalid escape style")),
}
}
}
bitflags! {
/// Flags for unescape_string functions.
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub struct UnescapeFlags: u32 {
/// escape special fish syntax characters like the semicolon
const SPECIAL = 1 << 0;
/// allow incomplete escape sequences
const INCOMPLETE = 1 << 1;
/// don't handle backslash escapes
const NO_BACKSLASHES = 1 << 2;
}
}
/// This function attempts to distinguish between a console session (at the actual login vty) and a
/// session within a terminal emulator inside a desktop environment or over SSH. Unfortunately
/// there are few values of $TERM that we can interpret as being exclusively console sessions, and
/// most common operating systems do not use them. The value is cached for the duration of the fish
/// session. We err on the side of assuming it's not a console session. This approach isn't
/// bullet-proof and that's OK.
pub fn is_console_session() -> bool {
static IS_CONSOLE_SESSION: OnceLock<bool> = OnceLock::new();
// TODO(terminal-workaround)
*IS_CONSOLE_SESSION.get_or_init(|| {
nix::unistd::ttyname(unsafe { std::os::fd::BorrowedFd::borrow_raw(STDIN_FILENO) })
.is_ok_and(|buf| {
// Check if the tty matches /dev/(console|dcons|tty[uv\d])
let is_console_tty = match buf.as_os_str().as_bytes() {
b"/dev/console" => true,
b"/dev/dcons" => true,
bytes => bytes.strip_prefix(b"/dev/tty").is_some_and(|rest| {
matches!(rest.first(), Some(b'u' | b'v' | b'0'..=b'9'))
}),
};
// and that $TERM is simple, e.g. `xterm` or `vt100`, not `xterm-something` or `sun-color`.
is_console_tty && env::var_os("TERM").is_none_or(|t| !t.as_bytes().contains(&b'-'))
})
})
}
/// Exits without invoking destructors (via _exit), useful for code after fork.
pub fn exit_without_destructors(code: libc::c_int) -> ! {
unsafe { libc::_exit(code) };
}
/// The character to use where the text has been truncated.
pub fn get_ellipsis_char() -> char {
'\u{2026}' // ('…')
}
/// The character or string to use where text has been truncated (ellipsis if possible, otherwise
/// ...)
pub fn get_ellipsis_str() -> &'static wstr {
L!("\u{2026}")
}
// Only pub for `src/common.rs`
pub static OBFUSCATION_READ_CHAR: AtomicU32 = AtomicU32::new(0);
pub fn get_obfuscation_read_char() -> char {
char::from_u32(OBFUSCATION_READ_CHAR.load(Ordering::Relaxed)).unwrap()
}
/// Call read, blocking and repeating on EINTR. Exits on EAGAIN.
/// Return the number of bytes read, or 0 on EOF, or an error.
pub fn read_blocked(fd: RawFd, buf: &mut [u8]) -> nix::Result<usize> {
loop {
let res = nix::unistd::read(unsafe { BorrowedFd::borrow_raw(fd) }, buf);
if let Err(nix::Error::EINTR) = res {
continue;
}
return res;
}
}
pub trait ReadExt {
/// Like [`std::io::Read::read_to_end`], but does not retry on EINTR.
fn read_to_end_interruptible(&mut self, buf: &mut Vec<u8>) -> std::io::Result<()>;
}
impl<T: Read + ?Sized> ReadExt for T {
fn read_to_end_interruptible(&mut self, buf: &mut Vec<u8>) -> std::io::Result<()> {
let mut chunk = [0_u8; 4096];
loop {
match self.read(&mut chunk)? {
0 => return Ok(()),
n => buf.extend_from_slice(&chunk[..n]),
}
}
}
}
/// A rusty port of the C++ `write_loop()` function from `common.cpp`. This should be deprecated in
/// favor of native rust read/write methods at some point.
pub fn safe_write_loop<Fd: AsRawFd>(fd: &Fd, buf: &[u8]) -> std::io::Result<()> {
let fd = fd.as_raw_fd();
let mut total = 0;
while total < buf.len() {
match nix::unistd::write(unsafe { BorrowedFd::borrow_raw(fd) }, &buf[total..]) {
Ok(written) => {
total += written;
}
Err(err) => {
if matches!(err, nix::Error::EAGAIN | nix::Error::EINTR) {
continue;
}
return Err(std::io::Error::from(err));
}
}
}
Ok(())
}
pub use safe_write_loop as write_loop;
pub const fn help_section_exists(section: &str) -> bool {
let haystack = include_str!("../../../share/help_sections");
let needle = section;
let needle = needle.as_bytes();
let haystack = haystack.as_bytes();
let nlen = needle.len();
let mut line_start = 0;
let mut i = 0;
while i <= haystack.len() {
if i == haystack.len() || haystack[i] == b'\n' {
let line_len = i - line_start;
if line_len == nlen {
let mut j = 0;
while j < nlen && haystack[line_start + j] == needle[j] {
j += 1;
}
if j == nlen {
return true;
}
}
line_start = i + 1;
}
i += 1;
}
false
}
#[macro_export]
macro_rules! help_section {
($section:expr) => {{
const {
assert!($crate::help_section_exists($section));
}
$section
}};
}
pub type Timepoint = f64;
/// Return the number of seconds from the UNIX epoch, with subsecond precision. This function uses
/// the gettimeofday function and will have the same precision as that function.
pub fn timef() -> Timepoint {
match time::SystemTime::now().duration_since(time::UNIX_EPOCH) {
Ok(difference) => difference.as_secs() as f64,
Err(until_epoch) => -(until_epoch.duration().as_secs() as f64),
}
}
/// Be able to restore the term's foreground process group.
/// This is set during startup and not modified after.
static INITIAL_FG_PROCESS_GROUP: AtomicI32 = AtomicI32::new(-1); // HACK, should be pid_t
const _: () = assert!(mem::size_of::<i32>() >= mem::size_of::<libc::pid_t>());
/// Save the value of tcgetpgrp so we can restore it on exit.
pub fn save_term_foreground_process_group() {
INITIAL_FG_PROCESS_GROUP.store(unsafe { libc::tcgetpgrp(STDIN_FILENO) }, Ordering::Relaxed);
}
pub fn restore_term_foreground_process_group_for_exit() {
// We wish to restore the tty to the initial owner. There's two ways this can go wrong:
// 1. We may steal the tty from someone else (#7060).
// 2. The call to tcsetpgrp may deliver SIGSTOP to us, and we will not exit.
// Hanging on exit seems worse, so ensure that SIGTTOU is ignored so we do not get SIGSTOP.
// Note initial_fg_process_group == 0 is possible with Linux pid namespaces.
// This is called during shutdown and from a signal handler. We don't bother to complain on
// failure because doing so is unlikely to be noticed.
// Safety: All of getpgrp, signal, and tcsetpgrp are async-signal-safe.
let initial_fg_process_group = INITIAL_FG_PROCESS_GROUP.load(Ordering::Relaxed);
if initial_fg_process_group > 0 && initial_fg_process_group != unsafe { libc::getpgrp() } {
unsafe {
libc::signal(SIGTTOU, SIG_IGN);
libc::tcsetpgrp(STDIN_FILENO, initial_fg_process_group);
}
}
}
/// A wrapper around Cell which supports modifying the contents, scoped to a region of code.
/// This provides a somewhat nicer API than ScopedRefCell because you can directly modify the value,
/// instead of requiring an accessor function which returns a mutable reference to a field.
pub struct ScopedCell<T>(Cell<T>);
impl<T> Deref for ScopedCell<T> {
type Target = Cell<T>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> DerefMut for ScopedCell<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<T: Copy> ScopedCell<T> {
pub fn new(value: T) -> Self {
Self(Cell::new(value))
}
/// Temporarily modify a value in the ScopedCell, restoring it when the returned object is dropped.
///
/// This is useful when you want to apply a change for the duration of a scope
/// without having to manually restore the previous value.
///
/// # Example
///
/// ```
/// use fish_common::ScopedCell;
///
/// let cell = ScopedCell::new(5);
/// assert_eq!(cell.get(), 5);
///
/// {
/// let _guard = cell.scoped_mod(|v| *v += 10);
/// assert_eq!(cell.get(), 15);
/// }
///
/// // Restored after scope
/// assert_eq!(cell.get(), 5);
/// ```
pub fn scoped_mod<'a, Modifier: FnOnce(&mut T)>(
&'a self,
modifier: Modifier,
) -> impl ScopeGuarding + 'a {
let mut val = self.get();
modifier(&mut val);
let saved = self.replace(val);
ScopeGuard::new(self, move |cell| cell.set(saved))
}
}
/// A wrapper around RefCell which supports modifying the contents, scoped to a region of code.
pub struct ScopedRefCell<T>(RefCell<T>);
impl<T> Deref for ScopedRefCell<T> {
type Target = RefCell<T>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> DerefMut for ScopedRefCell<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<T> ScopedRefCell<T> {
pub fn new(value: T) -> Self {
Self(RefCell::new(value))
}
/// Temporarily modify a field in the ScopedRefCell, restoring it when the returned guard is dropped.
///
/// This is useful when you want to change part of a data structure for the duration of a scope,
/// and automatically restore the original value afterward.
///
/// The `accessor` function selects the field to modify by returning a mutable reference to it.
///
/// # Example
/// ```
/// use fish_common::ScopedRefCell;
///
/// struct State { flag: bool }
///
/// let cell = ScopedRefCell::new(State { flag: false });
/// assert_eq!(cell.borrow().flag, false);
///
/// {
/// let _guard = cell.scoped_set(true, |s| &mut s.flag);
/// assert_eq!(cell.borrow().flag, true);
/// }
///
/// // Restored after scope
/// assert_eq!(cell.borrow().flag, false);
/// ```
pub fn scoped_set<'a, Accessor, Value: 'a>(
&'a self,
value: Value,
accessor: Accessor,
) -> impl ScopeGuarding + 'a
where
Accessor: Fn(&mut T) -> &mut Value + 'a,
{
let mut data = self.borrow_mut();
let mut saved = std::mem::replace(accessor(&mut data), value);
ScopeGuard::new(self, move |cell| {
let mut data = cell.borrow_mut();
std::mem::swap((accessor)(&mut data), &mut saved);
})
}
/// Convenience method for replacing the entire contents of the ScopedRefCell, restoring it when dropped.
///
/// Equivalent to `scoped_set(value, |s| s)`.
///
/// # Example
/// ```
/// use fish_common::ScopedRefCell;
///
/// let cell = ScopedRefCell::new(10);
/// assert_eq!(*cell.borrow(), 10);
///
/// {
/// let _guard = cell.scoped_replace(99);
/// assert_eq!(*cell.borrow(), 99);
/// }
///
/// assert_eq!(*cell.borrow(), 10);
/// ```
pub fn scoped_replace<'a>(&'a self, value: T) -> impl ScopeGuarding + 'a {
self.scoped_set(value, |s| s)
}
}
/// A RAII cleanup object. Unlike in C++ where there is no borrow checker, we can't just provide a
/// callback that modifies live objects willy-nilly because then there would be two &mut references
/// to the same object - the original variables we keep around to use and their captured references
/// held by the closure until its scope expires.
///
/// Instead we have a `ScopeGuard` type that takes exclusive ownership of (a mutable reference to)
/// the object to be managed. In lieu of keeping the original value around, we obtain a regular or
/// mutable reference to it via ScopeGuard's [`Deref`] and [`DerefMut`] impls.
///
/// The `ScopeGuard` is considered to be the exclusively owner of the passed value for the
/// duration of its lifetime. If you need to use the value again, use `ScopeGuard` to shadow the
/// value and obtain a reference to it via the `ScopeGuard` itself:
///
/// ```rust
/// use std::io::prelude::*;
/// use fish_common::ScopeGuard;
///
/// let file = std::fs::File::create("/dev/null").unwrap();
/// // Create a scope guard to write to the file when the scope expires.
/// // To be able to still use the file, shadow `file` with the ScopeGuard itself.
/// let mut file = ScopeGuard::new(file, |mut file| file.write_all(b"goodbye\n").unwrap());
/// // Now write to the file normally "through" the capturing ScopeGuard instance.
/// file.write_all(b"hello\n").unwrap();
///
/// // hello will be written first, then goodbye.
/// ```
pub struct ScopeGuard<T, F: FnOnce(T)>(Option<(T, F)>);
impl<T, F: FnOnce(T)> ScopeGuard<T, F> {
/// Creates a new `ScopeGuard` wrapping `value`. The `on_drop` callback is executed when the
/// ScopeGuard's lifetime expires or when it is manually dropped.
pub fn new(value: T, on_drop: F) -> Self {
Self(Some((value, on_drop)))
}
/// Invokes the callback, consuming the ScopeGuard.
pub fn commit(guard: Self) {
std::mem::drop(guard);
}
/// Cancels the invocation of the callback, returning the original wrapped value.
pub fn cancel(mut guard: Self) -> T {
let (value, _) = guard.0.take().expect("Should always have Some value");
value
}
}
impl<T, F: FnOnce(T)> Deref for ScopeGuard<T, F> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0.as_ref().unwrap().0
}
}
impl<T, F: FnOnce(T)> DerefMut for ScopeGuard<T, F> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0.as_mut().unwrap().0
}
}
impl<T, F: FnOnce(T)> Drop for ScopeGuard<T, F> {
fn drop(&mut self) {
if let Some((value, on_drop)) = self.0.take() {
on_drop(value);
}
}
}
/// A trait expressing what ScopeGuard can do. This is necessary because our scoped cells return an
/// `impl Trait` object and therefore methods on ScopeGuard which take a self parameter cannot be
/// used.
pub trait ScopeGuarding: DerefMut + Sized {
/// Invokes the callback, consuming the guard.
fn commit(guard: Self) {
std::mem::drop(guard);
}
}
impl<T, F: FnOnce(T)> ScopeGuarding for ScopeGuard<T, F> {}
pub const fn assert_send<T: Send>() {}
pub const fn assert_sync<T: Sync>() {}
/// Asserts that a slice is alphabetically sorted by a <code>&[wstr]</code> `name` field.
///
/// Mainly useful for static asserts/const eval.
///
/// # Panics
///
/// This function panics if the given slice is unsorted.
///
/// # Examples
///
/// ```
/// use fish_widestring::{L, wstr};
/// use fish_common::assert_sorted_by_name;
///
/// const COLORS: &[(&wstr, u32)] = &[
/// // must be in alphabetical order
/// (L!("blue"), 0x0000ff),
/// (L!("green"), 0x00ff00),
/// (L!("red"), 0xff0000),
/// ];
///
/// assert_sorted_by_name!(COLORS, 0);
/// ```
///
/// While this example would fail to compile:
///
/// ```compile_fail
/// use fish_widestring::{L, wstr};
/// use fish_common::assert_sorted_by_name;
///
/// const COLORS: &[(&wstr, u32)] = &[
/// // not in alphabetical order
/// (L!("green"), 0x00ff00),
/// (L!("blue"), 0x0000ff),
/// (L!("red"), 0xff0000),
/// ];
///
/// assert_sorted_by_name!(COLORS, 0);
/// ```
#[macro_export]
macro_rules! assert_sorted_by_name {
($slice:expr, $field:tt) => {
const _: () = {
use std::cmp::Ordering;
// ugly const eval workarounds below.
const fn cmp_i32(lhs: i32, rhs: i32) -> Ordering {
match lhs - rhs {
..=-1 => Ordering::Less,
0 => Ordering::Equal,
1.. => Ordering::Greater,
}
}
const fn cmp_slice(s1: &[char], s2: &[char]) -> Ordering {
let mut i = 0;
while i < s1.len() && i < s2.len() {
match cmp_i32(s1[i] as i32, s2[i] as i32) {
Ordering::Equal => i += 1,
other => return other,
}
}
cmp_i32(s1.len() as i32, s2.len() as i32)
}
let mut i = 1;
while i < $slice.len() {
let prev = $slice[i - 1].$field.as_char_slice();
let cur = $slice[i].$field.as_char_slice();
if matches!(cmp_slice(prev, cur), Ordering::Greater) {
panic!("array must be sorted");
}
i += 1;
}
};
};
($slice:expr) => {
assert_sorted_by_name!($slice, name);
};
}
pub trait Named {
fn name(&self) -> &'static wstr;
}
/// Return a reference to the first entry with the given name, assuming the entries are sorted by
/// name. Return None if not found.
pub fn get_by_sorted_name<T: Named>(name: &wstr, vals: &'static [T]) -> Option<&'static T> {
match vals.binary_search_by_key(&name, |val| val.name()) {
Ok(index) => Some(&vals[index]),
Err(_) => None,
}
}
/// Given an input string, return a prefix of the string up to the first NUL character,
/// or the entire string if there is no NUL character.
pub fn truncate_at_nul(input: &wstr) -> &wstr {
match input.chars().position(|c| c == '\0') {
Some(nul_pos) => &input[..nul_pos],
None => input,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scoped_cell() {
let cell = ScopedCell::new(42);
{
let _guard = cell.scoped_mod(|x| *x += 1);
assert_eq!(cell.get(), 43);
}
assert_eq!(cell.get(), 42);
}
#[test]
fn test_scoped_refcell() {
#[derive(Debug, PartialEq, Clone)]
struct Data {
x: i32,
y: i32,
}
let cell = ScopedRefCell::new(Data { x: 1, y: 2 });
{
let _guard = cell.scoped_set(10, |d| &mut d.x);
assert_eq!(cell.borrow().x, 10);
}
assert_eq!(cell.borrow().x, 1);
{
let _guard = cell.scoped_replace(Data { x: 42, y: 99 });
assert_eq!(*cell.borrow(), Data { x: 42, y: 99 });
}
assert_eq!(*cell.borrow(), Data { x: 1, y: 2 });
}
#[test]
fn test_scope_guard() {
let relaxed = std::sync::atomic::Ordering::Relaxed;
let counter = std::sync::atomic::AtomicUsize::new(0);
{
let guard = ScopeGuard::new(123, |arg| {
assert_eq!(arg, 123);
counter.fetch_add(1, relaxed);
});
assert_eq!(counter.load(relaxed), 0);
std::mem::drop(guard);
assert_eq!(counter.load(relaxed), 1);
}
// commit also invokes the callback.
{
let guard = ScopeGuard::new(123, |arg| {
assert_eq!(arg, 123);
counter.fetch_add(1, relaxed);
});
assert_eq!(counter.load(relaxed), 1);
ScopeGuard::commit(guard);
assert_eq!(counter.load(relaxed), 2);
}
}
#[test]
fn test_truncate_at_nul() {
assert_eq!(truncate_at_nul(L!("abc\0def")), L!("abc"));
assert_eq!(truncate_at_nul(L!("abc")), L!("abc"));
assert_eq!(truncate_at_nul(L!("\0abc")), L!(""));
}
}

View File

@@ -1,21 +0,0 @@
[package]
name = "fish-fallback"
edition.workspace = true
rust-version.workspace = true
version = "0.0.0"
repository.workspace = true
license.workspace = true
[dependencies]
fish-common.workspace = true
fish-widecharwidth.workspace = true
fish-widestring.workspace = true
libc.workspace = true
widestring.workspace = true
[build-dependencies]
fish-build-helper.workspace = true
rsconf.workspace = true
[lints]
workspace = true

View File

@@ -1,5 +0,0 @@
use fish_build_helper::target_os_is_cygwin;
fn main() {
rsconf::declare_cfg("cygwin", target_os_is_cygwin());
}

View File

@@ -4,18 +4,13 @@ edition.workspace = true
rust-version.workspace = true
version = "0.0.0"
repository.workspace = true
license.workspace = true
description = "proc-macro for extracting strings for gettext translation"
[lib]
proc-macro = true
[dependencies]
fish-tempfile.workspace = true
proc-macro2.workspace = true
[build-dependencies]
rsconf.workspace = true
[lints]
workspace = true

View File

@@ -1,3 +0,0 @@
fn main() {
rsconf::rebuild_if_env_changed("FISH_GETTEXT_EXTRACTION_DIR");
}

View File

@@ -1,7 +1,6 @@
extern crate proc_macro;
use fish_tempfile::random_filename;
use proc_macro::TokenStream;
use std::{ffi::OsString, io::Write as _, path::PathBuf};
use std::{ffi::OsString, fs::OpenOptions, io::Write};
fn unescape_multiline_rust_string(s: String) -> String {
if !s.contains('\n') {
@@ -26,7 +25,7 @@ enum State {
Escaped => match c {
'\\' => {
unescaped.push('\\');
state = Ground;
state = Ground
}
'\n' => state = ContinuationLineLeadingWhitespace,
_ => panic!("Unsupported escape sequence '\\{c}' in message string '{s}'"),
@@ -35,7 +34,7 @@ enum State {
' ' | '\t' => (),
_ => {
unescaped.push(c);
state = Ground;
state = Ground
}
},
}
@@ -43,22 +42,16 @@ enum State {
unescaped
}
// Each entry is written to a fresh file to avoid race conditions arising when there are multiple
// unsynchronized writers to the same file.
fn write_po_entry_to_file(message: &TokenStream, dir: &OsString) {
fn append_po_entry_to_file(message: &TokenStream, file_name: &OsString) {
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(file_name)
.unwrap_or_else(|e| panic!("Could not open file {file_name:?}: {e}"));
let message_string = unescape_multiline_rust_string(message.to_string());
assert!(
!message_string.contains('\n'),
"Gettext strings may not contain unescaped newlines. Unescaped newline found in '{message_string}'"
);
let msgid_without_quotes = &message_string[1..(message_string.len() - 1)];
// We don't want leading or trailing whitespace in our messages.
let trimmed_msgid = msgid_without_quotes.trim();
assert_eq!(msgid_without_quotes, trimmed_msgid);
assert!(!trimmed_msgid.starts_with("\\n"));
assert!(!trimmed_msgid.ends_with("\\n"));
assert!(!trimmed_msgid.starts_with("\\t"));
assert!(!trimmed_msgid.ends_with("\\t"));
if message_string.contains('\n') {
panic!("Gettext strings may not contain unescaped newlines. Unescaped newline found in '{message_string}'")
}
// Crude check for format strings. This might result in false positives.
let format_string_annotation = if message_string.contains('%') {
"#, c-format\n"
@@ -66,56 +59,48 @@ fn write_po_entry_to_file(message: &TokenStream, dir: &OsString) {
""
};
let po_entry = format!("{format_string_annotation}msgid {message_string}\nmsgstr \"\"\n\n");
let dir = PathBuf::from(dir);
let (path, result) =
fish_tempfile::create_file_with_retry(|| dir.join(random_filename(OsString::new())));
let mut file = result.unwrap_or_else(|e| {
panic!("Failed to create temporary file {path:?}:\n{e}");
});
file.write_all(po_entry.as_bytes()).unwrap();
}
/// The `message` is passed through unmodified.
/// If `FISH_GETTEXT_EXTRACTION_DIR` is defined in the environment,
/// the message ID is written into a new file in this directory,
/// If `FISH_GETTEXT_EXTRACTION_FILE` is defined in the environment,
/// this file is used to write the message,
/// so that it can then be used for generating gettext PO files.
/// The `message` must be a string literal.
///
/// # Panics
///
/// This macro panics if the `FISH_GETTEXT_EXTRACTION_DIR` variable is set and `message` has an
/// This macro panics if the `FISH_GETTEXT_EXTRACTION_FILE` variable is set and `message` has an
/// unexpected format.
/// Note that for example `concat!(...)` cannot be passed to this macro, because expansion works
/// outside in, meaning this macro would still see the `concat!` macro invocation, instead of a
/// string literal.
#[proc_macro]
pub fn gettext_extract(message: TokenStream) -> TokenStream {
if let Some(dir_path) = std::env::var_os("FISH_GETTEXT_EXTRACTION_DIR") {
if let Some(file_path) = std::env::var_os("FISH_GETTEXT_EXTRACTION_FILE") {
let pm2_message = proc_macro2::TokenStream::from(message.clone());
let mut token_trees = pm2_message.into_iter();
let first_token = token_trees
.next()
.expect("gettext_extract got empty token stream. Expected one token.");
assert!(
token_trees.next().is_none(),
"Invalid number of tokens passed to gettext_extract. Expected one token, but got more."
);
let proc_macro2::TokenTree::Group(group) = first_token else {
panic!("Expected group in gettext_extract, but got: {first_token:?}");
};
let mut group_tokens = group.stream().into_iter();
let first_group_token = group_tokens
.next()
.expect("gettext_extract expected one group token but got none.");
assert!(
group_tokens.next().is_none(),
"Invalid number of tokens in group passed to gettext_extract. Expected one token, but got more."
);
if let proc_macro2::TokenTree::Literal(_) = first_group_token {
write_po_entry_to_file(&message, &dir_path);
if token_trees.next().is_some() {
panic!("Invalid number of tokens passed to gettext_extract. Expected one token, but got more.")
}
if let proc_macro2::TokenTree::Group(group) = first_token {
let mut group_tokens = group.stream().into_iter();
let first_group_token = group_tokens
.next()
.expect("gettext_extract expected one group token but got none.");
if group_tokens.next().is_some() {
panic!("Invalid number of tokens in group passed to gettext_extract. Expected one token, but got more.")
}
if let proc_macro2::TokenTree::Literal(_) = first_group_token {
append_po_entry_to_file(&message, &file_path);
} else {
panic!("Expected literal in gettext_extract, but got: {first_group_token:?}");
}
} else {
panic!("Expected literal in gettext_extract, but got: {first_group_token:?}");
panic!("Expected group in gettext_extract, but got: {first_token:?}");
}
}
message

View File

@@ -4,7 +4,6 @@ edition.workspace = true
rust-version.workspace = true
version = "0.0.0"
repository.workspace = true
license.workspace = true
[dependencies]
phf.workspace = true

View File

@@ -1,61 +1,59 @@
use std::{
env,
ffi::OsStr,
path::{Path, PathBuf},
process::Command,
process::{Command, Stdio},
};
use fish_build_helper::env_var;
fn main() {
let cache_dir =
PathBuf::from(fish_build_helper::fish_build_dir()).join("fish-localization-map-cache");
embed_localizations(&cache_dir);
fish_build_helper::rebuild_if_path_changed(
fish_build_helper::workspace_root()
.join("localization")
.join("po"),
);
fish_build_helper::rebuild_if_path_changed(fish_build_helper::workspace_root().join("po"));
}
fn embed_localizations(cache_dir: &Path) {
use fish_gettext_mo_file_parser::parse_mo_file;
use std::{
fs::File,
io::{BufWriter, Write as _},
io::{BufWriter, Write},
};
let po_dir = fish_build_helper::workspace_root()
.join("localization")
.join("po");
let po_dir = fish_build_helper::workspace_root().join("po");
// Ensure that the directory is created, because clippy cannot compile the code if the
// directory does not exist.
std::fs::create_dir_all(cache_dir).unwrap();
let localization_map_path =
Path::new(&env_var("OUT_DIR").unwrap()).join("localization_maps.rs");
Path::new(&env::var("OUT_DIR").unwrap()).join("localization_maps.rs");
let mut localization_map_file = BufWriter::new(File::create(&localization_map_path).unwrap());
// This will become a map which maps from language identifiers to maps containing localizations
// for the respective language.
let mut catalogs = phf_codegen::Map::new();
match Command::new("msgfmt").arg("-h").output() {
match Command::new("msgfmt")
.arg("-h")
.stdout(Stdio::null())
.status()
{
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
rsconf::warn!(
"Could not find msgfmt required to build message catalogs. \
Localization will not work. \
If you install gettext now, you need to trigger a rebuild to include localization support. \
For example by running `touch localization/po` followed by the build command."
"Cannot find msgfmt to build gettext message catalogs. Localization will not work."
);
rsconf::warn!(
"If you install it now you need to trigger a rebuild to get localization support."
);
rsconf::warn!(
"One way to achieve that is running `touch po` followed by the build command."
);
}
Err(e) => {
panic!("Error when trying to run `msgfmt -h`: {e:?}");
}
Ok(output) => {
let has_check_format =
String::from_utf8_lossy(&output.stdout).contains("--check-format");
Ok(_) => {
for dir_entry_result in po_dir.read_dir().unwrap() {
let dir_entry = dir_entry_result.unwrap();
let po_file_path = dir_entry.path();
@@ -91,37 +89,25 @@ fn embed_localizations(cache_dir: &Path) {
if cached_map_mtime > po_mtime {
// Cached map file is considered up-to-date.
continue;
}
};
}
// Generate the map file.
// Try to create new MO data and load it into `mo_data`.
let mut tmp_mo_file = None;
let output = {
let mut cmd = &mut Command::new("msgfmt");
if has_check_format {
cmd = cmd.arg("--check-format");
} else {
tmp_mo_file = Some(cache_dir.join("messages.mo"));
}
cmd.arg(format!(
"--output-file={}",
tmp_mo_file
.as_ref()
.map_or("-", |path| path.to_str().unwrap())
))
let output = Command::new("msgfmt")
.arg("--check-format")
.arg("--output-file=-")
.arg(&po_file_path)
.output()
.unwrap()
};
assert!(
output.status.success(),
"msgfmt failed:\n{}",
String::from_utf8(output.stderr).unwrap()
);
let mo_data =
tmp_mo_file.map_or(output.stdout, |path| std::fs::read(path).unwrap());
.unwrap();
if !output.status.success() {
panic!(
"msgfmt failed:\n{}",
String::from_utf8(output.stderr).unwrap()
);
}
let mo_data = output.stdout;
// Extract map from MO data.
let language_localizations = parse_mo_file(&mo_data).unwrap();

View File

@@ -4,7 +4,6 @@ edition.workspace = true
rust-version.workspace = true
version = "0.0.0"
repository.workspace = true
license.workspace = true
[lints]
workspace = true

View File

@@ -1,6 +1,6 @@
use std::collections::HashMap;
const U32_SIZE: usize = size_of::<u32>();
const U32_SIZE: usize = std::mem::size_of::<u32>();
fn read_le_u32(bytes: &[u8]) -> u32 {
u32::from_le_bytes(bytes[..U32_SIZE].try_into().unwrap())
@@ -47,12 +47,9 @@ fn check_if_revision_is_supported(revision: u32) -> std::io::Result<()> {
}
fn as_usize(value: u32) -> usize {
const {
assert!(size_of::<u32>() <= size_of::<usize>());
}
// SAFETY: `usize` is guaranteed to be at least as wide as `u32` by the const assert above.
unsafe { usize::try_from(value).unwrap_unchecked() }
use std::mem::size_of;
const _: () = assert!(size_of::<u32>() <= size_of::<usize>());
usize::try_from(value).unwrap()
}
fn parse_strings(

View File

@@ -1,14 +0,0 @@
[package]
name = "fish-gettext"
edition.workspace = true
rust-version.workspace = true
version = "0.0.0"
repository.workspace = true
license.workspace = true
[dependencies]
fish-gettext-maps.workspace = true
phf.workspace = true
[lints]
workspace = true

View File

@@ -1,59 +0,0 @@
use fish_gettext_maps::CATALOGS;
use std::{
collections::HashMap,
sync::{LazyLock, Mutex},
};
type Catalog = &'static phf::Map<&'static str, &'static str>;
static LANGUAGE_PRECEDENCE: Mutex<Vec<(&'static str, Catalog)>> = Mutex::new(Vec::new());
pub fn gettext(message_str: &'static str) -> Option<&'static str> {
let language_precedence = LANGUAGE_PRECEDENCE.lock().unwrap();
// Use the localization from the highest-precedence language that has one available.
for (_, catalog) in language_precedence.iter() {
if let Some(localized_str) = catalog.get(message_str) {
return Some(localized_str);
}
}
None
}
#[derive(Clone, Copy)]
pub struct GettextLocalizationLanguage {
language: &'static str,
}
static AVAILABLE_LANGUAGES: LazyLock<HashMap<&'static str, GettextLocalizationLanguage>> =
LazyLock::new(|| {
HashMap::from_iter(
CATALOGS
.entries()
.map(|(&language, _)| (language, GettextLocalizationLanguage { language })),
)
});
pub fn get_available_languages() -> &'static HashMap<&'static str, GettextLocalizationLanguage> {
&AVAILABLE_LANGUAGES
}
pub fn set_language_precedence(new_precedence: &[GettextLocalizationLanguage]) {
let catalogs = new_precedence
.iter()
.map(|lang| {
(
lang.language,
*CATALOGS
.get(lang.language)
.expect("Only languages for which catalogs exist may be passed to gettext."),
)
})
.collect();
*LANGUAGE_PRECEDENCE.lock().unwrap() = catalogs;
}
pub fn get_language_precedence() -> Vec<&'static str> {
let language_precedence = LANGUAGE_PRECEDENCE.lock().unwrap();
language_precedence.iter().map(|&(lang, _)| lang).collect()
}

View File

@@ -8,11 +8,10 @@ description = "printf implementation, based on musl"
license = "MIT"
[dependencies]
assert_matches.workspace = true
libc.workspace = true
widestring = { workspace = true, optional = true }
unicode-segmentation.workspace = true
unicode-width.workspace = true
widestring = { workspace = true, optional = true }
[lints]
workspace = true

View File

@@ -15,7 +15,7 @@ pub enum Arg<'a> {
#[cfg(feature = "widestring")]
WString(WString),
UInt(u64),
SInt(i64),
SInt(i64, u8), // signed integers track their width as the number of bits
Float(f64),
USizeRef(&'a mut usize), // for use with %n
}
@@ -59,7 +59,7 @@ pub fn as_str<'s>(&'s self, storage: &'s mut String) -> Result<&'s str, Error>
pub fn as_uint(&self) -> Result<u64, Error> {
match *self {
Arg::UInt(u) => Ok(u),
Arg::SInt(i) => i.try_into().map_err(|_| Error::Overflow),
Arg::SInt(i, _w) => i.try_into().map_err(|_| Error::Overflow),
_ => Err(Error::BadArgType),
}
}
@@ -68,18 +68,25 @@ pub fn as_uint(&self) -> Result<u64, Error> {
pub fn as_sint(&self) -> Result<i64, Error> {
match *self {
Arg::UInt(u) => u.try_into().map_err(|_| Error::Overflow),
Arg::SInt(i) => Ok(i),
Arg::SInt(i, _w) => Ok(i),
_ => Err(Error::BadArgType),
}
}
/// Unwraps [`Arg::UInt`] to [`u64`].
/// Unwraps [`Arg::SInt`] and casts the [`i64`] to [`u64`].
/// Calling this on other variants of `[Arg]` is an error.
pub fn as_wrapping_sint(&self) -> Result<u64, Error> {
// If this is a signed value, then return the sign (true if negative) and the magnitude,
// masked to the value's width. This allows for e.g. -1 to be returned as 0xFF, 0xFFFF, etc.
// depending on the original width.
// If this is an unsigned value, simply return (false, u64).
pub fn as_wrapping_sint(&self) -> Result<(bool, u64), Error> {
match *self {
Arg::UInt(u) => Ok(u),
Arg::SInt(i) => Ok(i as u64),
Arg::UInt(u) => Ok((false, u)),
Arg::SInt(i, w) => {
// Need to shift twice in case w is 64.
debug_assert!(w > 0);
let mask = ((1u64 << (w - 1)) << 1).wrapping_sub(1);
let ui = (i as u64) & mask;
Ok((i < 0, ui))
}
_ => Err(Error::BadArgType),
}
}
@@ -90,7 +97,7 @@ pub fn as_float(&self) -> Result<f64, Error> {
match *self {
Arg::Float(f) => Ok(f),
Arg::UInt(u) => Ok(u as f64),
Arg::SInt(i) => Ok(i as f64),
Arg::SInt(i, _w) => Ok(i as f64),
_ => Err(Error::BadArgType),
}
}
@@ -174,7 +181,7 @@ macro_rules! impl_to_arg {
$(
impl<'a> ToArg<'a> for $t {
fn to_arg(self) -> Arg<'a> {
Arg::SInt(self as i64)
Arg::SInt(self as i64, <$t>::BITS as u8)
}
}
)*
@@ -199,43 +206,44 @@ fn to_arg(self) -> Arg<'a> {
#[cfg(test)]
mod tests {
use super::*;
use assert_matches::assert_matches;
#[cfg(feature = "widestring")]
use widestring::utf32str;
#[test]
fn test_to_arg() {
assert_matches!("test".to_arg(), Arg::Str("test"));
assert_matches!(String::from("test").to_arg(), Arg::Str(_));
const SIZE_WIDTH: u8 = isize::BITS as u8;
assert!(matches!("test".to_arg(), Arg::Str("test")));
assert!(matches!(String::from("test").to_arg(), Arg::Str(_)));
#[cfg(feature = "widestring")]
assert_matches!(utf32str!("test").to_arg(), Arg::WStr(_));
assert!(matches!(utf32str!("test").to_arg(), Arg::WStr(_)));
#[cfg(feature = "widestring")]
assert_matches!(WString::from("test").to_arg(), Arg::WStr(_));
assert_matches!(42f32.to_arg(), Arg::Float(_));
assert_matches!(42f64.to_arg(), Arg::Float(_));
assert_matches!('x'.to_arg(), Arg::UInt(120));
assert!(matches!(WString::from("test").to_arg(), Arg::WStr(_)));
assert!(matches!(42f32.to_arg(), Arg::Float(_)));
assert!(matches!(42f64.to_arg(), Arg::Float(_)));
assert!(matches!('x'.to_arg(), Arg::UInt(120)));
let mut usize_val: usize = 0;
assert_matches!((&mut usize_val).to_arg(), Arg::USizeRef(_));
assert_matches!(42i8.to_arg(), Arg::SInt(42));
assert_matches!(42i16.to_arg(), Arg::SInt(42));
assert_matches!(42i32.to_arg(), Arg::SInt(42));
assert_matches!(42i64.to_arg(), Arg::SInt(42));
assert_matches!(42isize.to_arg(), Arg::SInt(42));
assert!(matches!((&mut usize_val).to_arg(), Arg::USizeRef(_)));
assert!(matches!(42i8.to_arg(), Arg::SInt(42, 8)));
assert!(matches!(42i16.to_arg(), Arg::SInt(42, 16)));
assert!(matches!(42i32.to_arg(), Arg::SInt(42, 32)));
assert!(matches!(42i64.to_arg(), Arg::SInt(42, 64)));
assert!(matches!(42isize.to_arg(), Arg::SInt(42, SIZE_WIDTH)));
assert_eq!((-42i8).to_arg(), Arg::SInt(-42));
assert_eq!((-42i16).to_arg(), Arg::SInt(-42));
assert_eq!((-42i32).to_arg(), Arg::SInt(-42));
assert_eq!((-42i64).to_arg(), Arg::SInt(-42));
assert_eq!((-42isize).to_arg(), Arg::SInt(-42));
assert_eq!((-42i8).to_arg(), Arg::SInt(-42, 8));
assert_eq!((-42i16).to_arg(), Arg::SInt(-42, 16));
assert_eq!((-42i32).to_arg(), Arg::SInt(-42, 32));
assert_eq!((-42i64).to_arg(), Arg::SInt(-42, 64));
assert_eq!((-42isize).to_arg(), Arg::SInt(-42, SIZE_WIDTH));
assert_matches!(42u8.to_arg(), Arg::UInt(42));
assert_matches!(42u16.to_arg(), Arg::UInt(42));
assert_matches!(42u32.to_arg(), Arg::UInt(42));
assert_matches!(42u64.to_arg(), Arg::UInt(42));
assert_matches!(42usize.to_arg(), Arg::UInt(42));
assert!(matches!(42u8.to_arg(), Arg::UInt(42)));
assert!(matches!(42u16.to_arg(), Arg::UInt(42)));
assert!(matches!(42u32.to_arg(), Arg::UInt(42)));
assert!(matches!(42u64.to_arg(), Arg::UInt(42)));
assert!(matches!(42usize.to_arg(), Arg::UInt(42)));
let ptr = std::ptr::from_ref(&42f32);
assert_matches!(ptr.to_arg(), Arg::UInt(_));
let ptr = &42f32 as *const f32;
assert!(matches!(ptr.to_arg(), Arg::UInt(_)));
}
#[test]

View File

@@ -266,7 +266,7 @@ fn should_round_up(&self, digit_idx: i32, remainder: u32, mod_base: u32) -> bool
if rounding_digit & 1 != 0 {
round += 2.0;
// round now has an odd lsb (though round itself is even).
debug_assert_ne!(round.to_bits() & 1, 0);
debug_assert!(round.to_bits() & 1 != 0);
}
// Set 'small' to a value which is less than halfway, exactly halfway, or more than halfway

View File

@@ -3,9 +3,8 @@
mod tests;
use super::locale::Locale;
use super::printf_impl::{ConversionSpec, Error, ModifierFlags, pad};
use assert_matches::debug_assert_matches;
use decimal::{DIGIT_WIDTH, Decimal, DigitLimit};
use super::printf_impl::{pad, ConversionSpec, Error, ModifierFlags};
use decimal::{Decimal, DigitLimit, DIGIT_WIDTH};
use std::cmp::min;
use std::fmt::Write;
@@ -130,10 +129,10 @@ pub(crate) fn format_float(
) -> Result<usize, Error> {
// Only float conversions are expected.
type CS = ConversionSpec;
debug_assert_matches!(
debug_assert!(matches!(
conv_spec,
CS::e | CS::E | CS::f | CS::F | CS::g | CS::G | CS::a | CS::A
);
));
let prefix = match (y.is_sign_negative(), flags.mark_pos, flags.pad_pos) {
(true, _, _) => "-",
(false, true, _) => "+",
@@ -280,6 +279,7 @@ fn format_a(mut y: f64, params: FormatParams<'_, impl Write>) -> Result<usize, E
// Compute the number of hex digits in the mantissa after the decimal.
// -1 for leading 1 bit (we are to the range [1, 2)), then divide by 4, rounding up.
#[allow(unknown_lints)] // for old clippy
#[allow(clippy::manual_div_ceil)]
const MANTISSA_HEX_DIGITS: usize = (MANTISSA_BITS - 1 + 3) / 4;
if had_prec && prec < MANTISSA_HEX_DIGITS {

View File

@@ -4,8 +4,9 @@
mod fmt_fp;
mod printf_impl;
pub use printf_impl::{Error, FormatString, sprintf_locale};
pub use printf_impl::{sprintf_locale, Error, FormatString};
pub mod locale;
pub use locale::{Locale, C_LOCALE, EN_US_LOCALE};
#[cfg(test)]
mod tests;
@@ -51,7 +52,7 @@ macro_rules! sprintf {
{
// May be no args!
#[allow(unused_imports)]
use $crate::ToArg as _;
use $crate::ToArg;
$crate::printf_c_locale(
$target,
$fmt,
@@ -84,7 +85,7 @@ macro_rules! sprintf {
///
/// let result = printf_c_locale(&mut output, fmt, &mut args);
///
/// assert_eq!(result, Ok(10));
/// assert!(result == Ok(10));
/// assert_eq!(output, "1.2346e+05");
/// ```
pub fn printf_c_locale(

View File

@@ -66,7 +66,11 @@ fn next_group_size(&self, digits_left: usize) -> usize {
// Divide remaining digits by repeat_group.
// Apply any remainder to the first group.
let res = (digits_left - accum) % (repeat_group as usize);
if res > 0 { res } else { repeat_group as usize }
if res > 0 {
res
} else {
repeat_group as usize
}
}
}
@@ -118,91 +122,86 @@ pub fn separator_count(&self, digits_count: usize) -> usize {
group_repeat: true,
};
#[cfg(test)]
mod tests {
use super::{C_LOCALE, EN_US_LOCALE, Locale};
#[test]
fn test_apply_grouping() {
let input = "123456789";
let mut result: String;
#[test]
fn test_apply_grouping() {
let input = "123456789";
let mut result: String;
// en_US has commas.
assert_eq!(EN_US_LOCALE.thousands_sep, Some(','));
result = EN_US_LOCALE.apply_grouping(input);
assert_eq!(result, "123,456,789");
// en_US has commas.
assert_eq!(EN_US_LOCALE.thousands_sep, Some(','));
result = EN_US_LOCALE.apply_grouping(input);
assert_eq!(result, "123,456,789");
// Test weird locales.
let input: &str = "1234567890123456";
let mut locale: Locale = C_LOCALE;
locale.thousands_sep = Some('!');
// Test weird locales.
let input: &str = "1234567890123456";
let mut locale: Locale = C_LOCALE;
locale.thousands_sep = Some('!');
locale.grouping = [5, 3, 1, 0];
locale.group_repeat = false;
result = locale.apply_grouping(input);
assert_eq!(result, "1234567!8!901!23456");
locale.grouping = [5, 3, 1, 0];
locale.group_repeat = false;
result = locale.apply_grouping(input);
assert_eq!(result, "1234567!8!901!23456");
// group_repeat doesn't matter because trailing group is 0
locale.grouping = [5, 3, 1, 0];
locale.group_repeat = true;
result = locale.apply_grouping(input);
assert_eq!(result, "1234567!8!901!23456");
// group_repeat doesn't matter because trailing group is 0
locale.grouping = [5, 3, 1, 0];
locale.group_repeat = true;
result = locale.apply_grouping(input);
assert_eq!(result, "1234567!8!901!23456");
locale.grouping = [5, 3, 1, 2];
locale.group_repeat = false;
result = locale.apply_grouping(input);
assert_eq!(result, "12345!67!8!901!23456");
locale.grouping = [5, 3, 1, 2];
locale.group_repeat = false;
result = locale.apply_grouping(input);
assert_eq!(result, "12345!67!8!901!23456");
locale.grouping = [5, 3, 1, 2];
locale.group_repeat = true;
result = locale.apply_grouping(input);
assert_eq!(result, "1!23!45!67!8!901!23456");
}
#[test]
#[should_panic]
fn test_thousands_grouping_length_panics_if_no_sep() {
// We should panic if we try to group with no thousands separator.
assert_eq!(C_LOCALE.thousands_sep, None);
C_LOCALE.apply_grouping("123");
}
#[test]
fn test_thousands_grouping_length() {
fn validate_grouping_length_hint(locale: Locale, mut input: &str) {
loop {
let expected = locale.separator_count(input.len()) + input.len();
let actual = locale.apply_grouping(input).len();
assert_eq!(expected, actual);
if input.is_empty() {
break;
}
input = &input[1..];
}
}
validate_grouping_length_hint(EN_US_LOCALE, "123456789");
// Test weird locales.
let input = "1234567890123456";
let mut locale: Locale = C_LOCALE;
locale.thousands_sep = Some('!');
locale.grouping = [5, 3, 1, 0];
locale.group_repeat = false;
validate_grouping_length_hint(locale, input);
// group_repeat doesn't matter because trailing group is 0
locale.grouping = [5, 3, 1, 0];
locale.group_repeat = true;
validate_grouping_length_hint(locale, input);
locale.grouping = [5, 3, 1, 2];
locale.group_repeat = false;
validate_grouping_length_hint(locale, input);
locale.grouping = [5, 3, 1, 2];
locale.group_repeat = true;
validate_grouping_length_hint(locale, input);
}
locale.grouping = [5, 3, 1, 2];
locale.group_repeat = true;
result = locale.apply_grouping(input);
assert_eq!(result, "1!23!45!67!8!901!23456");
}
#[test]
#[should_panic]
fn test_thousands_grouping_length_panics_if_no_sep() {
// We should panic if we try to group with no thousands separator.
assert_eq!(C_LOCALE.thousands_sep, None);
C_LOCALE.apply_grouping("123");
}
#[test]
fn test_thousands_grouping_length() {
fn validate_grouping_length_hint(locale: Locale, mut input: &str) {
loop {
let expected = locale.separator_count(input.len()) + input.len();
let actual = locale.apply_grouping(input).len();
assert_eq!(expected, actual);
if input.is_empty() {
break;
}
input = &input[1..];
}
}
validate_grouping_length_hint(EN_US_LOCALE, "123456789");
// Test weird locales.
let input = "1234567890123456";
let mut locale: Locale = C_LOCALE;
locale.thousands_sep = Some('!');
locale.grouping = [5, 3, 1, 0];
locale.group_repeat = false;
validate_grouping_length_hint(locale, input);
// group_repeat doesn't matter because trailing group is 0
locale.grouping = [5, 3, 1, 0];
locale.group_repeat = true;
validate_grouping_length_hint(locale, input);
locale.grouping = [5, 3, 1, 2];
locale.group_repeat = false;
validate_grouping_length_hint(locale, input);
locale.grouping = [5, 3, 1, 2];
locale.group_repeat = true;
validate_grouping_length_hint(locale, input);
}

View File

@@ -2,12 +2,11 @@
use super::arg::Arg;
use super::fmt_fp::format_float;
use super::locale::Locale;
use assert_matches::assert_matches;
use std::fmt::{self, Write};
use std::mem;
use std::result::Result;
use unicode_segmentation::UnicodeSegmentation as _;
use unicode_width::UnicodeWidthStr as _;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
#[cfg(feature = "widestring")]
use widestring::Utf32Str as wstr;
@@ -58,7 +57,7 @@ fn try_set(&mut self, c: char) -> bool {
'+' => self.mark_pos = true,
'\'' => self.grouped = true,
_ => return false,
}
};
true
}
}
@@ -302,7 +301,7 @@ pub(super) fn pad(
min_width: usize,
current_width: usize,
) -> fmt::Result {
assert_matches!(c, '0' | ' ');
assert!(c == '0' || c == ' ');
if current_width >= min_width {
return Ok(());
}
@@ -342,7 +341,7 @@ pub(super) fn pad(
///
/// let result = sprintf_locale(&mut output, fmt, &locale::EN_US_LOCALE, &mut args);
///
/// assert_eq!(result, Ok(12));
/// assert!(result == Ok(12));
/// assert_eq!(output, "1,234,567.89");
/// ```
pub fn sprintf_locale(
@@ -372,7 +371,7 @@ pub fn sprintf_locale(
}
// Consume the % at the start of the format specifier.
debug_assert_eq!(s.at(0), Some('%'));
debug_assert!(s.at(0) == Some('%'));
s.advance_by(1);
// Read modifier flags. '-' and '0' flags are mutually exclusive.
@@ -473,7 +472,7 @@ pub fn sprintf_locale(
// If someone passes us a negative value, format it with the width
// we were given.
let lower = conv_spec.is_lower();
let uint = arg.as_wrapping_sint()?;
let (_, uint) = arg.as_wrapping_sint()?;
if uint != 0 {
if flags.alt_form {
prefix = if lower { "0x" } else { "0X" };
@@ -562,7 +561,7 @@ pub fn sprintf_locale(
};
// Numeric output should be empty iff the value is 0.
if spec_is_numeric && body.is_empty() {
debug_assert_eq!(arg.as_uint().unwrap(), 0);
debug_assert!(arg.as_uint().unwrap() == 0);
}
// Decide if we want to apply thousands grouping to the body, and compute its size.

View File

@@ -1,8 +1,8 @@
use crate::arg::ToArg;
use crate::locale::{C_LOCALE, EN_US_LOCALE, Locale};
use crate::{Error, FormatString as _, sprintf_locale};
use crate::locale::{Locale, C_LOCALE, EN_US_LOCALE};
use crate::{sprintf_locale, Error, FormatString};
use libc::c_char;
use std::f64::consts::{E, PI, TAU};
use std::ffi::CStr;
use std::fmt;
// sprintf, checking length
@@ -13,7 +13,7 @@ macro_rules! sprintf_check {
$(,)? // optional trailing comma
) => {
{
use unicode_width::UnicodeWidthStr as _;
use unicode_width::UnicodeWidthStr;
let mut target = String::new();
let mut args = [$($arg.to_arg()),*];
let len = $crate::printf_c_locale(
@@ -77,7 +77,7 @@ fn write_str(&mut self, _s: &str) -> fmt::Result {
#[test]
fn smoke() {
assert_fmt!("Hello, %s!", "world" => "Hello, world!");
assert_fmt!("Hello, %ls!", "world" => "Hello, world!"); // length modifier
assert_fmt!("Hello, %ls!", "world" => "Hello, world!");
assert_fmt!("Hello, world! %d %%%%", 3 => "Hello, world! 3 %%");
assert_fmt!("" => "");
}
@@ -225,7 +225,7 @@ fn test_int() {
assert_fmt!("%d", -123 => "-123");
assert_fmt!("~%d~", 148 => "~148~");
assert_fmt!("00%dxx", -91232 => "00-91232xx");
assert_fmt!("%x", -9232 => "ffffffffffffdbf0");
assert_fmt!("%x", -9232 => "ffffdbf0");
assert_fmt!("%X", 432 => "1B0");
assert_fmt!("%09X", 432 => "0000001B0");
assert_fmt!("%9X", 432 => " 1B0");
@@ -234,7 +234,6 @@ fn test_int() {
assert_fmt!("%2o", 4 => " 4");
assert_fmt!("% 12d", -4 => " -4");
assert_fmt!("% 12d", 48 => " 48");
// with length modifier
assert_fmt!("%ld", -4_i64 => "-4");
assert_fmt!("%lld", -4_i64 => "-4");
assert_fmt!("%lX", -4_i64 => "FFFFFFFFFFFFFFFC");
@@ -249,7 +248,6 @@ fn test_int() {
assert_fmt!("%9X", 492 => " 1EC");
assert_fmt!("% 12u", 4 => " 4");
assert_fmt!("% 12u", 48 => " 48");
// with length modifier
assert_fmt!("%lu", 4_u64 => "4");
assert_fmt!("%llu", 4_u64 => "4");
assert_fmt!("%lX", 4_u64 => "4");
@@ -416,7 +414,6 @@ fn test_float() {
assert_fmt1!("%f", 0.0, "0.000000");
assert_fmt1!("%g", 0.0, "0");
assert_fmt1!("%#g", 0.0, "0.00000");
// with length modifier
assert_fmt1!("%la", 0.0, "0x0p+0");
assert_fmt1!("%le", 0.0, "0.000000e+00");
assert_fmt1!("%lf", 0.0, "0.000000");
@@ -433,7 +430,7 @@ fn test_float() {
assert_fmt1!("%.4f", 1.03125, "1.0312"); /* 0x1.08p0 */
assert_fmt1!("%.2f", 1.375, "1.38");
assert_fmt1!("%.1f", 1.375, "1.4");
assert_fmt1!("%.1lf", 1.375, "1.4"); // length modifier
assert_fmt1!("%.1lf", 1.375, "1.4");
assert_fmt1!("%.15f", 1.1, "1.100000000000000");
assert_fmt1!("%.16f", 1.1, "1.1000000000000001");
assert_fmt1!("%.17f", 1.1, "1.10000000000000009");
@@ -758,8 +755,8 @@ fn test_errors() {
sprintf_err!("%1", => BadFormatString);
sprintf_err!("%%%k", => BadFormatString);
sprintf_err!("%B", => BadFormatString);
sprintf_err!("%lC", 'q' => BadFormatString); // length modifier
sprintf_err!("%lS", 'q' => BadFormatString); // length modifier
sprintf_err!("%lC", 'q' => BadFormatString);
sprintf_err!("%lS", 'q' => BadFormatString);
sprintf_err!("%d", => MissingArg);
sprintf_err!("%d %u", 1 => MissingArg);
sprintf_err!("%*d", 5 => MissingArg);
@@ -855,20 +852,25 @@ fn test_float_hex_prec() {
// Note that our hex float formatting rounds according to the rounding mode,
// while libc may not; as a result we may differ in the last digit. So this
// requires manual comparison.
let mut c_storage = [0u8; 256];
let c_storage_ptr = c_storage.as_mut_ptr() as *mut c_char;
let mut rust_str = String::with_capacity(256);
let mut c_storage = [0u8; 256];
let mut libc_sprintf = libc_sprintf_one_float_with_precision(&mut c_storage, c"%.*a");
let c_fmt = b"%.*a\0".as_ptr() as *const c_char;
let mut failed = false;
for sign in [1.0, -1.0] {
for mut v in [0.0, 0.5, 1.0, 1.5, PI, TAU, E] {
for sign in [1.0, -1.0].into_iter() {
for mut v in [0.0, 0.5, 1.0, 1.5, PI, TAU, E].into_iter() {
v *= sign;
for preci in 1..=200_usize {
for preci in 1..=200_i32 {
rust_str.clear();
crate::sprintf!(=> &mut rust_str, "%.*a", preci, v);
let printf_str = libc_sprintf(preci, v);
let printf_str = unsafe {
let len = libc::snprintf(c_storage_ptr, c_storage.len(), c_fmt, preci, v);
assert!(len >= 0);
let sl = std::slice::from_raw_parts(c_storage_ptr as *const u8, len as usize);
std::str::from_utf8(sl).unwrap()
};
if rust_str != printf_str {
println!(
"Our printf and libc disagree on hex formatting of float: {v}
@@ -884,33 +886,14 @@ fn test_float_hex_prec() {
assert!(!failed);
}
fn libc_sprintf_one_float_with_precision<'a>(
storage: &'a mut [u8],
fmt: &'a CStr,
) -> impl FnMut(usize, f64) -> &'a str {
|preci, float_val| unsafe {
let storage_ptr = storage.as_mut_ptr();
let len = libc::snprintf(
storage_ptr.cast(),
storage.len(),
fmt.as_ptr(),
preci,
float_val,
);
assert!(len >= 0);
let sl = std::slice::from_raw_parts(storage_ptr, len as usize);
std::str::from_utf8(sl).unwrap()
}
}
fn test_exhaustive(rust_fmt: &str, c_fmt: &CStr) {
fn test_exhaustive(rust_fmt: &str, c_fmt: *const c_char) {
// "There's only 4 billion floats so test them all."
// This tests a format string expected to be of the form "%.*g" or "%.*e".
// That is, it takes a precision and a double.
println!("Testing {rust_fmt}");
let mut rust_str = String::with_capacity(128);
let mut c_storage = [0u8; 128];
let mut libc_sprintf = libc_sprintf_one_float_with_precision(&mut c_storage, c_fmt);
let c_storage_ptr = c_storage.as_mut_ptr() as *mut c_char;
for i in 0..=u32::MAX {
if i % 1000000 == 0 {
@@ -922,7 +905,12 @@ fn test_exhaustive(rust_fmt: &str, c_fmt: &CStr) {
rust_str.clear();
crate::sprintf!(=> &mut rust_str, rust_fmt, preci, ff);
let printf_str = libc_sprintf(preci, ff);
let printf_str = unsafe {
let len = libc::snprintf(c_storage_ptr, c_storage.len(), c_fmt, preci, ff);
assert!(len >= 0);
let sl = std::slice::from_raw_parts(c_storage_ptr as *const u8, len as usize);
std::str::from_utf8(sl).unwrap()
};
if rust_str != printf_str {
println!(
"Rust and libc disagree on formatting float {i:x}: {ff}\n
@@ -941,19 +929,19 @@ fn test_exhaustive(rust_fmt: &str, c_fmt: &CStr) {
#[ignore]
fn test_float_g_exhaustive() {
// To run: cargo test test_float_g_exhaustive --release -- --ignored --nocapture
test_exhaustive("%.*g", c"%.*g");
test_exhaustive("%.*g", b"%.*g\0".as_ptr() as *const c_char);
}
#[test]
#[ignore]
fn test_float_e_exhaustive() {
// To run: cargo test test_float_e_exhaustive --release -- --ignored --nocapture
test_exhaustive("%.*e", c"%.*e");
test_exhaustive("%.*e", b"%.*e\0".as_ptr() as *const c_char);
}
#[test]
#[ignore]
fn test_float_f_exhaustive() {
// To run: cargo test test_float_f_exhaustive --release -- --ignored --nocapture
test_exhaustive("%.*f", c"%.*f");
test_exhaustive("%.*f", b"%.*f\0".as_ptr() as *const c_char);
}

View File

@@ -1,14 +0,0 @@
[package]
name = "fish-tempfile"
edition.workspace = true
rust-version.workspace = true
version = "0.0.0"
repository.workspace = true
license.workspace = true
[dependencies]
nix = { workspace = true, features = ["fs", "feature"] }
rand.workspace = true
[lints]
workspace = true

View File

@@ -1,171 +0,0 @@
use std::{
ffi::OsString,
fs::{File, OpenOptions},
io::ErrorKind,
path::PathBuf,
};
use rand::distr::{Alphanumeric, Distribution as _};
pub struct TempFile {
file: File,
path: PathBuf,
}
impl TempFile {
pub fn get(&self) -> &File {
&self.file
}
pub fn get_mut(&mut self) -> &mut File {
&mut self.file
}
pub fn path(&self) -> &PathBuf {
&self.path
}
}
impl Drop for TempFile {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
}
}
pub struct TempDir {
path: PathBuf,
}
impl TempDir {
pub fn path(&self) -> &PathBuf {
&self.path
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.path);
}
}
/// Creates a random filename with the given prefix.
/// Appends a random sequence of alphanumeric ASCII characters.
pub fn random_filename(mut prefix: OsString) -> OsString {
let mut rng = rand::rng();
let suffix_length = 10;
let random_part: String = Alphanumeric
.sample_iter(&mut rng)
.take(suffix_length)
.map(char::from)
.collect();
assert_eq!(random_part.len(), suffix_length);
prefix.push(random_part);
prefix
}
/// Tries to create a new file at the path returned by `generate_path`.
/// If a file already exists at this path, `generate_path` will be called again and file creation
/// will be retried. This is repeated until a new file is created, or an error occurs.
pub fn create_file_with_retry(
generate_path: impl Fn() -> PathBuf,
) -> (PathBuf, std::io::Result<File>) {
loop {
let path = generate_path();
match OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open(&path)
{
Ok(file) => {
return (path, Ok(file));
}
Err(e) => {
if e.kind() != ErrorKind::AlreadyExists {
return (path, Err(e));
}
}
}
}
}
/// Tries to create a new temporary file in the operating system's default location for temporary
/// files.
/// On success, a [`TempFile`] is returned.
/// When this struct is dropped, the backing file will be deleted.
pub fn new_file() -> std::io::Result<TempFile> {
let (path, result) = create_file_with_retry(|| {
std::env::temp_dir().join(random_filename(OsString::from("fish_tmp_")))
});
let file = result?;
Ok(TempFile { file, path })
}
/// Tries to create a new temporary directory using `mkdtemp`.
/// On success, a [`TempDir`] is returned.
/// When this struct is dropped, the backing directory, including all its contents, will be deleted.
pub fn new_dir() -> std::io::Result<TempDir> {
let template = std::env::temp_dir().join("fish_tmp_XXXXXX");
let path = nix::unistd::mkdtemp(&template)?;
Ok(TempDir { path })
}
#[cfg(test)]
mod tests {
use std::{
fs::File,
io::{Read as _, Seek as _, Write as _},
};
#[test]
fn create_tempfile() {
super::new_file().unwrap();
}
#[test]
#[should_panic(expected = "file should no longer exist")]
fn use_tempfile() {
let mut tempfile = super::new_file().unwrap();
let expected_content = "test";
{
let file = tempfile.get_mut();
file.write_all(expected_content.as_bytes()).unwrap();
file.seek(std::io::SeekFrom::Start(0)).unwrap();
}
let mut actual_content = String::new();
{
let mut file = tempfile.get();
file.read_to_string(&mut actual_content).unwrap();
}
let path = tempfile.path().to_owned();
drop(tempfile);
assert_eq!(expected_content, actual_content);
File::open(&path).expect("file should no longer exist");
}
#[test]
fn create_tempdir() {
super::new_dir().unwrap();
}
#[test]
#[should_panic(expected = "file should no longer exist")]
fn use_tempdir() {
let tempdir = super::new_dir().unwrap();
let file_path = tempdir.path().join("foo");
let expected_content = "test";
{
let mut file = File::create(&file_path).unwrap();
file.write_all(expected_content.as_bytes()).unwrap();
}
{
let mut file = File::open(&file_path).unwrap();
let mut actual_content = String::new();
file.read_to_string(&mut actual_content).unwrap();
assert_eq!(expected_content, actual_content);
}
drop(tempdir);
File::open(&file_path).expect("file should no longer exist");
}
}

View File

@@ -1,14 +0,0 @@
[package]
name = "fish-util"
edition.workspace = true
rust-version.workspace = true
version = "0.0.0"
repository.workspace = true
license.workspace = true
[dependencies]
fish-widestring.workspace = true
rand.workspace = true
[lints]
workspace = true

View File

@@ -1,21 +0,0 @@
[package]
name = "fish-wcstringutil"
edition.workspace = true
rust-version.workspace = true
version = "0.0.0"
repository.workspace = true
license.workspace = true
[dependencies]
fish-common.workspace = true
fish-fallback.workspace = true
fish-widestring.workspace = true
# Only needed for cygwin detection.
# TODO(MSRV>=1.86): remove
[build-dependencies]
fish-build-helper.workspace = true
rsconf.workspace = true
[lints]
workspace = true

View File

@@ -1,3 +0,0 @@
fn main() {
rsconf::declare_cfg("cygwin", fish_build_helper::target_os_is_cygwin());
}

View File

@@ -1,15 +0,0 @@
[package]
name = "fish-wgetopt"
edition.workspace = true
rust-version.workspace = true
version = "0.0.0"
repository.workspace = true
license.workspace = true
[dependencies]
assert_matches.workspace = true
fish-wcstringutil.workspace = true
fish-widestring.workspace = true
[lints]
workspace = true

View File

@@ -1,10 +0,0 @@
[package]
name = "fish-widecharwidth"
edition.workspace = true
rust-version.workspace = true
version = "0.0.0"
repository.workspace = true
license = "CC0-1.0"
[lints]
workspace = true

View File

@@ -1,13 +0,0 @@
[package]
name = "fish-widestring"
edition.workspace = true
rust-version.workspace = true
version = "0.0.0"
repository.workspace = true
license.workspace = true
[dependencies]
widestring.workspace = true
[lints]
workspace = true

View File

@@ -1,391 +0,0 @@
//! Support for character classification for vi-mode word movements
use std::{cmp::Ordering, ops::RangeInclusive};
/// Character class for word movements
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WordCharClass {
Blank, // whitespace
Newline, // newline
Punctuation, // punctuation and symbols
Word, // word character
Emoji, // emoji
Superscript, // superscript (U+2070-U+207F)
Subscript, // subscript (U+2080-U+2094)
Braille, // braille (U+2800-U+28FF)
Hiragana, // Hiragana (U+3040-U+309F)
Katakana, // Katakana (U+30A0-U+30FF)
Cjk, // CJK Ideographs
Hangul, // Hangul Syllables
}
pub fn is_blank(c: char) -> bool {
WordCharClass::from_char(c) == WordCharClass::Blank
}
impl WordCharClass {
/// Reference: <https://github.com/vim/vim/blob/48940d94/src/mbyte.c#L2866-L2982>
pub fn from_char(c: char) -> Self {
// Quick check for Latin1 characters
if u32::from(c) < 0x100 {
// newline
if c == '\n' {
return WordCharClass::Newline;
}
// space, tab, NUL, or non-breaking space
if matches!(c, ' ' | '\t' | '\0' | '\u{a0}' /* NBSP */) {
return WordCharClass::Blank;
}
if is_latin1_word_char(c) {
return WordCharClass::Word;
}
return WordCharClass::Punctuation;
}
// emoji check
if is_emoji(c) {
return WordCharClass::Emoji;
}
// binary search in table
CLASSES
.binary_search_by(|interval| compare_range_to_char(&interval.range, c))
.map_or(
// most other characters are "word" characters
WordCharClass::Word,
|i| CLASSES[i].class,
)
}
}
/// Check if codepoint is a word character (alphanumeric)
/// Note: Different from vim default behavior, we do not regard underscore as a word character!
fn is_latin1_word_char(c: char) -> bool {
c.is_ascii_alphanumeric() || (matches!( c, | 'À'..='ÿ') && c != '×' && c != '÷')
}
fn compare_range_to_char(range: &RangeInclusive<char>, c: char) -> Ordering {
if *range.end() < c {
Ordering::Less
} else if *range.start() > c {
Ordering::Greater
} else {
Ordering::Equal
}
}
/// Check if codepoint is in emoji table using binary search (like vim's intable)
fn is_emoji(c: char) -> bool {
EMOJI_ALL
.binary_search_by(|range| compare_range_to_char(range, c))
.is_ok()
}
/// Character class interval
struct ClassInterval {
range: RangeInclusive<char>,
class: WordCharClass,
}
impl ClassInterval {
const fn new(range: RangeInclusive<char>, class: WordCharClass) -> Self {
Self { range, class }
}
const fn single(c: char, class: WordCharClass) -> Self {
Self::new(c..=c, class)
}
}
/// Character classification table (sorted non-overlapping intervals)
/// Reference: <https://github.com/vim/vim/blob/48940d94/src/mbyte.c#L2867-L2948>
static CLASSES: &[ClassInterval] = {
use ClassInterval as I;
use WordCharClass::*;
&[
I::single('\u{037e}', Punctuation), // Greek question mark
I::single('\u{0387}', Punctuation), // Greek ano teleia
I::new('\u{055a}'..='\u{055f}', Punctuation), // Armenian punctuation
I::single('\u{0589}', Punctuation), // Armenian full stop
I::single('\u{05be}', Punctuation),
I::single('\u{05c0}', Punctuation),
I::single('\u{05c3}', Punctuation),
I::new('\u{05f3}'..='\u{05f4}', Punctuation),
I::single('\u{060c}', Punctuation),
I::single('\u{061b}', Punctuation),
I::single('\u{061f}', Punctuation),
I::new('\u{066a}'..='\u{066d}', Punctuation),
I::single('\u{06d4}', Punctuation),
I::new('\u{0700}'..='\u{070d}', Punctuation), // Syriac punctuation
I::new('\u{0964}'..='\u{0965}', Punctuation),
I::single('\u{0970}', Punctuation),
I::single('\u{0df4}', Punctuation),
I::single('\u{0e4f}', Punctuation),
I::new('\u{0e5a}'..='\u{0e5b}', Punctuation),
I::new('\u{0f04}'..='\u{0f12}', Punctuation),
I::new('\u{0f3a}'..='\u{0f3d}', Punctuation),
I::single('\u{0f85}', Punctuation),
I::new('\u{104a}'..='\u{104f}', Punctuation), // Myanmar punctuation
I::single('\u{10fb}', Punctuation), // Georgian punctuation
I::new('\u{1361}'..='\u{1368}', Punctuation), // Ethiopic punctuation
I::new('\u{166d}'..='\u{166e}', Punctuation), // Canadian Syl. punctuation
I::single('\u{1680}', Blank),
I::new('\u{169b}'..='\u{169c}', Punctuation),
I::new('\u{16eb}'..='\u{16ed}', Punctuation),
I::new('\u{1735}'..='\u{1736}', Punctuation),
I::new('\u{17d4}'..='\u{17dc}', Punctuation), // Khmer punctuation
I::new('\u{1800}'..='\u{180a}', Punctuation), // Mongolian punctuation
I::new('\u{2000}'..='\u{200b}', Blank), // spaces
I::new('\u{200c}'..='\u{2027}', Punctuation), // punctuation and symbols
I::new('\u{2028}'..='\u{2029}', Blank),
I::new('\u{202a}'..='\u{202e}', Punctuation), // punctuation and symbols
I::single('\u{202f}', Blank),
I::new('\u{2030}'..='\u{205e}', Punctuation), // punctuation and symbols
I::single('\u{205f}', Blank),
I::new('\u{2060}'..='\u{206f}', Punctuation), // punctuation and symbols
I::new('\u{2070}'..='\u{207f}', Superscript),
I::new('\u{2080}'..='\u{2094}', Subscript),
I::new('\u{20a0}'..='\u{27ff}', Punctuation), // all kinds of symbols
I::new('\u{2800}'..='\u{28ff}', Braille),
I::new('\u{2900}'..='\u{2998}', Punctuation), // arrows, brackets, etc.
I::new('\u{29d8}'..='\u{29db}', Punctuation),
I::new('\u{29fc}'..='\u{29fd}', Punctuation),
I::new('\u{2e00}'..='\u{2e7f}', Punctuation), // supplemental punctuation
I::single('\u{3000}', Blank), // ideographic space
I::new('\u{3001}'..='\u{3020}', Punctuation), // ideographic punctuation
I::single('\u{3030}', Punctuation),
I::single('\u{303d}', Punctuation),
I::new('\u{3040}'..='\u{309f}', Hiragana),
I::new('\u{30a0}'..='\u{30ff}', Katakana),
I::new('\u{3300}'..='\u{9fff}', Cjk),
I::new('\u{ac00}'..='\u{d7a3}', Hangul),
I::new('\u{f900}'..='\u{faff}', Cjk),
I::new('\u{fd3e}'..='\u{fd3f}', Punctuation),
I::new('\u{fe30}'..='\u{fe6b}', Punctuation), // punctuation forms
I::new('\u{ff00}'..='\u{ff0f}', Punctuation), // half/fullwidth ASCII
I::new('\u{ff1a}'..='\u{ff20}', Punctuation), // half/fullwidth ASCII
I::new('\u{ff3b}'..='\u{ff40}', Punctuation), // half/fullwidth ASCII
I::new('\u{ff5b}'..='\u{ff65}', Punctuation), // half/fullwidth ASCII
I::new('\u{1d000}'..='\u{1d24f}', Punctuation), // Musical notation
I::new('\u{1d400}'..='\u{1d7ff}', Punctuation), // Mathematical Alphanumeric Symbols
I::new('\u{1f000}'..='\u{1f2ff}', Punctuation), // Game pieces; enclosed characters
I::new('\u{1f300}'..='\u{1f9ff}', Punctuation), // Many symbol blocks
I::new('\u{20000}'..='\u{2a6df}', Cjk),
I::new('\u{2a700}'..='\u{2b73f}', Cjk),
I::new('\u{2b740}'..='\u{2b81f}', Cjk),
I::new('\u{2f800}'..='\u{2fa1f}', Cjk),
]
};
/// Reference: <https://github.com/vim/vim/blob/48940d94/src/mbyte.c#L2704-L2852>
static EMOJI_ALL: &[RangeInclusive<char>] = &[
'\u{203c}'..='\u{203c}',
'\u{2049}'..='\u{2049}',
'\u{2122}'..='\u{2122}',
'\u{2139}'..='\u{2139}',
'\u{2194}'..='\u{2199}',
'\u{21a9}'..='\u{21aa}',
'\u{231a}'..='\u{231b}',
'\u{2328}'..='\u{2328}',
'\u{23cf}'..='\u{23cf}',
'\u{23e9}'..='\u{23f3}',
'\u{23f8}'..='\u{23fa}',
'\u{24c2}'..='\u{24c2}',
'\u{25aa}'..='\u{25ab}',
'\u{25b6}'..='\u{25b6}',
'\u{25c0}'..='\u{25c0}',
'\u{25fb}'..='\u{25fe}',
'\u{2600}'..='\u{2604}',
'\u{260e}'..='\u{260e}',
'\u{2611}'..='\u{2611}',
'\u{2614}'..='\u{2615}',
'\u{2618}'..='\u{2618}',
'\u{261d}'..='\u{261d}',
'\u{2620}'..='\u{2620}',
'\u{2622}'..='\u{2623}',
'\u{2626}'..='\u{2626}',
'\u{262a}'..='\u{262a}',
'\u{262e}'..='\u{262f}',
'\u{2638}'..='\u{263a}',
'\u{2640}'..='\u{2640}',
'\u{2642}'..='\u{2642}',
'\u{2648}'..='\u{2653}',
'\u{265f}'..='\u{2660}',
'\u{2663}'..='\u{2663}',
'\u{2665}'..='\u{2666}',
'\u{2668}'..='\u{2668}',
'\u{267b}'..='\u{267b}',
'\u{267e}'..='\u{267f}',
'\u{2692}'..='\u{2697}',
'\u{2699}'..='\u{2699}',
'\u{269b}'..='\u{269c}',
'\u{26a0}'..='\u{26a1}',
'\u{26a7}'..='\u{26a7}',
'\u{26aa}'..='\u{26ab}',
'\u{26b0}'..='\u{26b1}',
'\u{26bd}'..='\u{26be}',
'\u{26c4}'..='\u{26c5}',
'\u{26c8}'..='\u{26c8}',
'\u{26ce}'..='\u{26cf}',
'\u{26d1}'..='\u{26d1}',
'\u{26d3}'..='\u{26d4}',
'\u{26e9}'..='\u{26ea}',
'\u{26f0}'..='\u{26f5}',
'\u{26f7}'..='\u{26fa}',
'\u{26fd}'..='\u{26fd}',
'\u{2702}'..='\u{2702}',
'\u{2705}'..='\u{2705}',
'\u{2708}'..='\u{270d}',
'\u{270f}'..='\u{270f}',
'\u{2712}'..='\u{2712}',
'\u{2714}'..='\u{2714}',
'\u{2716}'..='\u{2716}',
'\u{271d}'..='\u{271d}',
'\u{2721}'..='\u{2721}',
'\u{2728}'..='\u{2728}',
'\u{2733}'..='\u{2734}',
'\u{2744}'..='\u{2744}',
'\u{2747}'..='\u{2747}',
'\u{274c}'..='\u{274c}',
'\u{274e}'..='\u{274e}',
'\u{2753}'..='\u{2755}',
'\u{2757}'..='\u{2757}',
'\u{2763}'..='\u{2764}',
'\u{2795}'..='\u{2797}',
'\u{27a1}'..='\u{27a1}',
'\u{27b0}'..='\u{27b0}',
'\u{27bf}'..='\u{27bf}',
'\u{2934}'..='\u{2935}',
'\u{2b05}'..='\u{2b07}',
'\u{2b1b}'..='\u{2b1c}',
'\u{2b50}'..='\u{2b50}',
'\u{2b55}'..='\u{2b55}',
'\u{3030}'..='\u{3030}',
'\u{303d}'..='\u{303d}',
'\u{3297}'..='\u{3297}',
'\u{3299}'..='\u{3299}',
'\u{1f004}'..='\u{1f004}',
'\u{1f0cf}'..='\u{1f0cf}',
'\u{1f170}'..='\u{1f171}',
'\u{1f17e}'..='\u{1f17f}',
'\u{1f18e}'..='\u{1f18e}',
'\u{1f191}'..='\u{1f19a}',
'\u{1f1e6}'..='\u{1f1ff}',
'\u{1f201}'..='\u{1f202}',
'\u{1f21a}'..='\u{1f21a}',
'\u{1f22f}'..='\u{1f22f}',
'\u{1f232}'..='\u{1f23a}',
'\u{1f250}'..='\u{1f251}',
'\u{1f300}'..='\u{1f321}',
'\u{1f324}'..='\u{1f393}',
'\u{1f396}'..='\u{1f397}',
'\u{1f399}'..='\u{1f39b}',
'\u{1f39e}'..='\u{1f3f0}',
'\u{1f3f3}'..='\u{1f3f5}',
'\u{1f3f7}'..='\u{1f4fd}',
'\u{1f4ff}'..='\u{1f53d}',
'\u{1f549}'..='\u{1f54e}',
'\u{1f550}'..='\u{1f567}',
'\u{1f56f}'..='\u{1f570}',
'\u{1f573}'..='\u{1f57a}',
'\u{1f587}'..='\u{1f587}',
'\u{1f58a}'..='\u{1f58d}',
'\u{1f590}'..='\u{1f590}',
'\u{1f595}'..='\u{1f596}',
'\u{1f5a4}'..='\u{1f5a5}',
'\u{1f5a8}'..='\u{1f5a8}',
'\u{1f5b1}'..='\u{1f5b2}',
'\u{1f5bc}'..='\u{1f5bc}',
'\u{1f5c2}'..='\u{1f5c4}',
'\u{1f5d1}'..='\u{1f5d3}',
'\u{1f5dc}'..='\u{1f5de}',
'\u{1f5e1}'..='\u{1f5e1}',
'\u{1f5e3}'..='\u{1f5e3}',
'\u{1f5e8}'..='\u{1f5e8}',
'\u{1f5ef}'..='\u{1f5ef}',
'\u{1f5f3}'..='\u{1f5f3}',
'\u{1f5fa}'..='\u{1f64f}',
'\u{1f680}'..='\u{1f6c5}',
'\u{1f6cb}'..='\u{1f6d2}',
'\u{1f6d5}'..='\u{1f6d7}',
'\u{1f6dc}'..='\u{1f6e5}',
'\u{1f6e9}'..='\u{1f6e9}',
'\u{1f6eb}'..='\u{1f6ec}',
'\u{1f6f0}'..='\u{1f6f0}',
'\u{1f6f3}'..='\u{1f6fc}',
'\u{1f7e0}'..='\u{1f7eb}',
'\u{1f7f0}'..='\u{1f7f0}',
'\u{1f90c}'..='\u{1f93a}',
'\u{1f93c}'..='\u{1f945}',
'\u{1f947}'..='\u{1f9ff}',
'\u{1fa70}'..='\u{1fa7c}',
'\u{1fa80}'..='\u{1fa88}',
'\u{1fa90}'..='\u{1fabd}',
'\u{1fabf}'..='\u{1fac5}',
'\u{1face}'..='\u{1fadb}',
'\u{1fae0}'..='\u{1fae8}',
'\u{1faf0}'..='\u{1faf8}',
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_blank() {
assert_eq!(WordCharClass::from_char(' '), WordCharClass::Blank); // space
assert_eq!(WordCharClass::from_char('\t'), WordCharClass::Blank); // tab
assert_eq!(WordCharClass::from_char('\0'), WordCharClass::Blank); // NUL
assert_eq!(WordCharClass::from_char('\u{a0}'), WordCharClass::Blank); // non-breaking space
assert_eq!(WordCharClass::from_char('\u{3000}'), WordCharClass::Blank); // ideographic space
}
#[test]
fn test_word() {
assert_eq!(WordCharClass::from_char('a'), WordCharClass::Word); // 'a'
assert_eq!(WordCharClass::from_char('Z'), WordCharClass::Word); // 'Z'
assert_eq!(WordCharClass::from_char('0'), WordCharClass::Word); // '0'
assert_eq!(WordCharClass::from_char('e'), WordCharClass::Word); // 'e' with acute
}
#[test]
fn test_punctuation() {
assert_eq!(WordCharClass::from_char('.'), WordCharClass::Punctuation);
assert_eq!(WordCharClass::from_char(','), WordCharClass::Punctuation);
assert_eq!(WordCharClass::from_char(';'), WordCharClass::Punctuation);
assert_eq!(WordCharClass::from_char(''), WordCharClass::Punctuation); // ideographic comma
assert_eq!(WordCharClass::from_char('_'), WordCharClass::Punctuation);
}
#[test]
fn test_cjk() {
assert_eq!(WordCharClass::from_char('中'), WordCharClass::Cjk); // CJK character
assert_eq!(WordCharClass::from_char('𠀀'), WordCharClass::Cjk); // CJK Extension B
}
#[test]
fn test_japanese() {
assert_eq!(WordCharClass::from_char('あ'), WordCharClass::Hiragana);
assert_eq!(WordCharClass::from_char('ア'), WordCharClass::Katakana);
}
#[test]
fn test_hangul() {
assert_eq!(WordCharClass::from_char('한'), WordCharClass::Hangul);
}
#[test]
fn test_emoji() {
assert_eq!(WordCharClass::from_char('😀'), WordCharClass::Emoji); // grinning face
assert_eq!(WordCharClass::from_char('🚀'), WordCharClass::Emoji); // rocket
assert_eq!(WordCharClass::from_char('❤'), WordCharClass::Emoji); // red heart
assert_eq!(WordCharClass::from_char('✅'), WordCharClass::Emoji); // check mark
}
#[test]
fn test_special() {
assert_eq!(WordCharClass::from_char('⁰'), WordCharClass::Superscript); // superscript zero
assert_eq!(WordCharClass::from_char('₀'), WordCharClass::Subscript); // subscript zero
assert_eq!(WordCharClass::from_char('\u{2800}'), WordCharClass::Braille); // braille blank
}
}

View File

@@ -1,11 +0,0 @@
[package]
name = "xtask"
version = "0.0.0"
rust-version.workspace = true
edition.workspace = true
repository.workspace = true
[dependencies]
clap.workspace = true
fish-build-helper.workspace = true
fish-tempfile.workspace = true

View File

@@ -1,28 +0,0 @@
use std::{ffi::OsStr, process::Command};
pub trait CommandExt {
fn run_or_panic(&mut self);
}
impl CommandExt for Command {
fn run_or_panic(&mut self) {
match self.status() {
Ok(exit_status) => {
if !exit_status.success() {
panic!("Command did not run successfully: {:?}", self.get_program())
}
}
Err(err) => {
panic!("Failed to run command: {err}");
}
}
}
}
pub fn cargo<I, S>(cargo_args: I)
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
Command::new(env!("CARGO")).args(cargo_args).run_or_panic();
}

View File

@@ -1,95 +0,0 @@
use clap::{Parser, Subcommand};
use fish_build_helper::as_os_strs;
use std::{path::PathBuf, process::Command};
use xtask::{CommandExt as _, cargo};
#[derive(Parser)]
#[command(
name = "cargo xtask",
about = "Wrapper for running various utilities",
arg_required_else_help(true)
)]
struct Cli {
#[command(subcommand)]
task: Task,
}
#[derive(Subcommand)]
enum Task {
/// Run various checks on the repo.
Check,
/// Build HTML docs
HtmlDocs {
/// Path to a fish_indent executable. If none is specified, fish_indent will be built.
#[arg(long)]
fish_indent: Option<PathBuf>,
},
/// Build man pages
ManPages,
}
fn main() {
let cli = Cli::parse();
match cli.task {
Task::Check => run_checks(),
Task::HtmlDocs { fish_indent } => build_html_docs(fish_indent),
Task::ManPages => cargo(["build", "--package", "fish-build-man-pages"]),
}
}
fn run_checks() {
let repo_root_dir = fish_build_helper::workspace_root();
let check_script = repo_root_dir.join("build_tools").join("check.sh");
Command::new(check_script).run_or_panic();
}
fn build_html_docs(fish_indent: Option<PathBuf>) {
let fish_indent_path = fish_indent.unwrap_or_else(|| {
// Build fish_indent if no existing one is specified.
cargo([
"build",
"--bin",
"fish_indent",
"--profile",
"dev",
"--no-default-features",
]);
fish_build_helper::fish_build_dir()
.join("debug")
.join("fish_indent")
});
// Set path so `sphinx-build` can find `fish_indent`.
// Create tempdir to store symlink to fish_indent.
// This is done to avoid adding other binaries to the PATH.
let tempdir = fish_tempfile::new_dir().unwrap();
std::os::unix::fs::symlink(
std::fs::canonicalize(fish_indent_path).unwrap(),
tempdir.path().join("fish_indent"),
)
.unwrap();
let new_path = format!(
"{}:{}",
tempdir.path().to_str().unwrap(),
fish_build_helper::env_var("PATH").unwrap()
);
let doc_src_dir = fish_build_helper::workspace_root().join("doc_src");
let doctrees_dir = fish_build_helper::fish_doc_dir().join(".doctrees-html");
let html_dir = fish_build_helper::fish_doc_dir().join("html");
let args = as_os_strs![
"-j",
"auto",
"-q",
"-b",
"html",
"-c",
&doc_src_dir,
"-d",
&doctrees_dir,
&doc_src_dir,
&html_dir,
];
Command::new(option_env!("FISH_SPHINX").unwrap_or("sphinx-build"))
.env("PATH", new_path)
.args(args)
.run_or_panic();
}

1
debian/compat vendored Normal file
View File

@@ -0,0 +1 @@
12

Some files were not shown because too many files have changed in this diff Show More