diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..c0090a3 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,25 @@ +name: Lint + +on: [push, pull_request] + +env: + GITHUB_TOKEN: ${{ github.token }} + +jobs: + lint: + name: Check that code is lint clean using precious + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Configure Git + run: | + git config --global user.email "jdoe@example.com" + git config --global user.name "J. Doe" + - name: Run install-dev-tools.sh + run: | + set -e + mkdir $HOME/bin + ./dev/bin/install-dev-tools.sh + - name: Run precious + run: | + PATH=$PATH:$HOME/bin precious lint -a diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..f703d65 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,62 @@ +name: Self-test + +on: + push: + pull_request: + +jobs: + test: + name: Test + strategy: + fail-fast: false + matrix: + platform: + - platform_name: Linux-x86_64 + os: ubuntu-20.04 + target: x86_64-unknown-linux-musl + cache-cross-binary: true + - platform_name: Linux-powerpc64 + os: ubuntu-20.04 + target: powerpc64-unknown-linux-gnu + cache-cross-binary: true + - platform_name: Windows-x86_64 + os: windows-latest + target: x86_64-pc-windows-msvc + - platform_name: macOS-aarch64 + os: macOS-latest + target: aarch64-apple-darwin + + runs-on: ${{ matrix.platform.os }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Copy test project to root + shell: bash + run: | + cp -a test-project/* . + rm -fr test-project + - name: Build binary + uses: houseabsolute/actions-rust-cross@v0 + with: + command: build + args: "--release" + target: ${{ matrix.platform.target }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Release + id: release + uses: ./ + with: + executable-name: test-project + target: ${{ matrix.platform.target }} + - name: Install psutils on Windows + run: choco install --ignore-checksums psutils + if: runner.os == 'Windows' + - name: Check release artifacts + shell: bash + run: | + ./tests/check-release.pl \ + --artifact-id "${{ steps.release.outputs.artifact-id }}" \ + --executable-name test-project \ + --github-token "${{ github.token }}" \ + --repo houseabsolute/actions-rust-release \ + --target "${{ matrix.platform.target }}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d87531b --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/node_modules +/package-lock.json +/package.json +.\#* +\#*\# +test-project/**/target/** diff --git a/Changes.md b/Changes.md new file mode 100644 index 0000000..0747097 --- /dev/null +++ b/Changes.md @@ -0,0 +1,3 @@ +## 0.0.1 - 2024-09-14 + +- First release upon an unsuspecting world. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..634066c --- /dev/null +++ b/README.md @@ -0,0 +1,130 @@ +# GitHub Action to Release Rust Projects + +This action helps you create +[GitHub Releases](https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository) +for Rust projects that produce an executable. + +Here's an example from the release workflow for +[my tool `precious`](https://github.com/houseabsolute/precious): + +```yaml +jobs: + release: + name: Release - ${{ matrix.platform.release_for }} + strategy: + matrix: + platform: + - os_name: Linux-x86_64 + os: ubuntu-20.04 + target: x86_64-unknown-linux-musl + + - os_name: macOS-x86_64 + os: macOS-latest + target: x86_64-apple-darwin + + # more release targets here ... + + runs-on: ${{ matrix.platform.os }} + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Build executable + uses: houseabsolute/actions-rust-cross@v0 + with: + target: ${{ matrix.platform.target }} + args: "--locked --release" + strip: true + - name: Publish artifacts and release + uses: houseabsolute/actions-rust-release@v0 + with: + executable-name: ubi + target: ${{ matrix.platform.target }} + if: matrix.toolchain == 'stable' +``` + +## What It Does + +This action will do the following: + +- Package an executable, along with any additional files you specify (defaults to `README*` and + `Changes.md`). It will produce a tarball on all platforms but Windows, where it produces a zip + file. +- Create a SHA256 checksum file for the tarball or zip file using `shasum`. +- Upload the archive and checksum files as artifacts for your workflow. +- If this action is called for a tag that matches the specified prefix (defaults to `v`), then it + will also create/update a GitHub Release for this tag, attaching the archive and checksum files as + release artifacts + +If you have a matrix build that creates executables for many platforms, you should call this action +for each platform. It should work on any platform supported by GitHub Actions (Linux, macOS, +Windows). + +## Input Parameters + +This action takes the following parameters: + +### `executable-name` + +- **Required**: yes + +The name of the executable that your project compiles to. In most cases, this is just the name of +your project, like `cross` or `mise`. + +### `release-tag-prefix` + +- **Required**: no +- **Default**: `"v"` + +The prefix for release tags. The default is "v", so that tags like "v1.2.3" trigger a release. + +### `target` + +- **Required:** no, if `archive-name` is provided. + +The target triple that this release was compiled for. This should be one of the targets found by +running `rustup target list`. + +Either this input or the `archive-name` input must be provided. + +### `archive-name` + +- **Required**: no, if `target` is provided + +The name of the archive file to produce. This will contain the executable and any additional files +specified in the `files-to-package` input, if any. If this isn't given, then one will be created +based, starting with the `executable-name` and followed by elements from the `target` input. + +Either this input or the `target` input must be provided. + +### `extra-files` + +- **Required**: no + +This is a list of additional files or globs to include in the archive files for a release. This +should be provided as a newline-separate list. + +Defaults to the file specified by the `changes-file` input and any file matching `README*` in the +project root. + +If you _do_ specify any files, then you will need to also list the changes file and README +explicitly if you want them to be included. + +### `changes-file` + +- **Required**: no +- **Default**: `"Changes.md"` + +The name of the file that contains the changelog for this project. This will be used to generate a +description for the GitHub Release. + +## Outputs + +This action provides two outputs. + +### `artifact-id` + +The ID of the workflow artifact that was created. + +### `artifact-url` + +The URL of the workflow artifact that was created. diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..362925a --- /dev/null +++ b/action.yml @@ -0,0 +1,133 @@ +name: "Release Rust project binaries as GitHub releases" +author: "Dave Rolsky " +branding: + icon: home + color: gray-dark +description: | + This action provides tooling for releasing binaries from Rust projects as GitHub releases. It will + create a release from a tag and attach binaries to it. The binaries will be packed as either + tarballs (most platforms) or zip files (Windows). For each archive, a SHA-256 checksum file will + also published. + + This action also uploads an artifact containing the files that _would_ be released every time it + is run, even if it doesn't do a release. This is useful in letting you see what a release for the + code would look like at any stage of development. + + This is mostly a thin wrapper around + [softprops/action-gh-release](https://github.com/softprops/action-gh-release) with some additional + Rust-related bits. +inputs: + executable-name: + description: | + The name of the executable. In most cases, this is just the name of your project, like `cross` or + `mise`. This is required. + required: true + release-tag-prefix: + description: | + The prefix for release tags. The default is "v", so that tags like "v1.2.3" trigger a + release. + default: "v" + target: + description: | + The rust target name, like "x86_64-unknown-linux-gnu". This is used to find the output of + `cargo build`. If this isn't provided, then this action will look for the build output under + `target/release`, instead of something like `target/x86_64-unknown-linux-gnu/release`. + + Either this input or the `archive-name` input must be provided. + archive-name: + description: | + The name of the archive file to produce. This will contain the executable and any additional + files specified in the `files-to-package` input, if any. If this isn't given, then one will be + created based, starting with the `executable-name` and followed by elements from the `target` + input. + + Either this input or the `target` input must be provided. + extra-files: + description: | + A newline separated list of additional files or globs to include in the archive files for a + release. + + Defaults to the file specified by the `changes-file` input and any file matching `README*` in + the project root. + + If you _do_ specify any files, then you will need to also list the changes file and README + explicitly if you want them to be included. + changes-file: + description: | + The name of the file that contains the changelog for this project. This will be used to + generate a description for the GitHub Release. The default is `Changes.md`. + default: "Changes.md" +outputs: + artifact-id: + description: | + This is the ID of the artifact created by this action. + value: ${{ steps.publish-release-artifact.outputs.artifact-id }} + artifact-url: + description: | + This is the URL of the artifact created by this action. + value: ${{ steps.publish-release-artifact.outputs.artifact-url }} +runs: + using: composite + steps: + - name: Show config + shell: bash + run: | + echo "release-tag-prefix = ${{ inputs.release-tag-prefix }}" + echo "executable-name = ${{ inputs.executable-name }}" + echo "target = ${{ inputs.target }}" + echo "archive-name = ${{ inputs.archive-name }}" + echo "extra-files = ${{ inputs.extra-files }}" + echo "changes-file = ${{ inputs.changes-file }}" + echo "github.ref = ${{ github.ref }}" + echo "github.ref_type = ${{ github.ref_type }}" + echo "matches release-tag-prefix = ${{ startsWith( github.ref_name, inputs.release-tag-prefix ) }}" + - name: Add this action's path to PATH + shell: bash + run: echo "${{ github.action_path }}" >> $GITHUB_PATH + - name: Package as archive + id: package-archive + shell: bash + run: | + make-archive.pl \ + --executable-name "${{ inputs.executable-name }}" \ + --target "${{ inputs.target }}" \ + --archive-name "${{ inputs.archive-name }}" \ + --changes-file "${{ inputs.changes-file }}" \ + --extra-files "${{ inputs.extra-files }}" + - name: Generate SHA-256 checksum file (*nix) + shell: bash + run: | + set -e + set -x + + shasum --algorithm 256 \ + "${{ steps.package-archive.outputs.archive-basename }}" \ + > "${{ steps.package-archive.outputs.archive-basename }}.sha256" + if: runner.os != 'Windows' + - name: Install dos2unix and psutils on Windows + shell: powershell + run: | + choco install --ignore-checksums dos2unix psutils + if: runner.os == 'Windows' + - name: Generate SHA-256 checksum file (Windows) + shell: powershell + run: | + shasum --algorithm 256 ` + "${{ steps.package-archive.outputs.archive-basename }}" ` + > "${{ steps.package-archive.outputs.archive-basename }}.sha256" + dos2unix "${{ steps.package-archive.outputs.archive-basename }}.sha256" + if: runner.os == 'Windows' + - name: Publish release artifact for run + id: publish-release-artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.package-archive.outputs.archive-basename }} + path: ${{ steps.package-archive.outputs.archive-basename }}* + - name: Publish GitHub release + uses: softprops/action-gh-release@v2 + with: + draft: true + # The trailing "*" should pick up the checksum file. + files: ${{ steps.package-archive.outputs.archive-basename }}* + body_path: ${{ inputs.changes-file }} + if: github.ref_type == 'tag' && startsWith( github.ref_name, inputs.release-tag-prefix ) diff --git a/dev/bin/install-dev-tools.sh b/dev/bin/install-dev-tools.sh new file mode 100755 index 0000000..9234136 --- /dev/null +++ b/dev/bin/install-dev-tools.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +set -eo pipefail +set -x + +function run() { + echo "$1" + eval "$1" +} + +function install_tools() { + curl --silent --location \ + https://raw.githubusercontent.com/houseabsolute/ubi/master/bootstrap/bootstrap-ubi.sh | + sh + run "ubi --project houseabsolute/precious --in $HOME/bin" + run "ubi --project houseabsolute/omegasort --in $HOME/bin" + run "ubi --project koalaman/shellcheck --in $HOME/bin" + run "ubi --project mvdan/sh --in $HOME/bin --exe shfmt" + run "ubi --project crate-ci/typos --in $HOME/bin" + run "npm install prettier" + run "curl -L https://cpanmin.us/ -o $HOME/bin/cpanm" + run "chmod 0755 $HOME/bin/cpanm" + run "$HOME/bin/cpanm --sudo --notest Perl::Tidy" +} + +if [ "$1" == "-v" ]; then + set -x +fi + +mkdir -p "$HOME"/bin + +set +e +echo ":$PATH:" | grep --extended-regexp ":$HOME/bin:" >&/dev/null +# shellcheck disable=SC2181 +if [ "$?" -eq "0" ]; then + path_has_home_bin=1 +fi +set -e + +if [ -z "$path_has_home_bin" ]; then + PATH=$HOME/bin:$PATH +fi + +install_tools + +echo "Tools were installed into $HOME/bin." +if [ -z "$path_has_home_bin" ]; then + echo "You should add $HOME/bin to your PATH." +fi + +exit 0 diff --git a/git/hooks/pre-commit.sh b/git/hooks/pre-commit.sh new file mode 100755 index 0000000..c8b5eb4 --- /dev/null +++ b/git/hooks/pre-commit.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +status=0 + +PRECIOUS=$(which precious) +if [[ -z $PRECIOUS ]]; then + PRECIOUS=./bin/precious +fi + +if ! "$PRECIOUS" lint -s; then + status=$((status + 1)) +fi + +exit $status diff --git a/git/setup.pl b/git/setup.pl new file mode 100755 index 0000000..8c99cb8 --- /dev/null +++ b/git/setup.pl @@ -0,0 +1,27 @@ +#!/usr/bin/env perl + +use strict; +use warnings; + +use Cwd qw( abs_path ); + +symlink_hook('pre-commit'); + +sub symlink_hook { + my $hook = shift; + + my $dot = ".git/hooks/$hook"; + my $file = "git/hooks/$hook.sh"; + my $link = "../../$file"; + + if ( -e $dot ) { + if ( -l $dot ) { + return if readlink $dot eq $link; + } + warn "You already have a hook at $dot!\n"; + return; + } + + symlink $link, $dot + or die "Could not link $dot => $link: $!"; +} diff --git a/make-archive.pl b/make-archive.pl new file mode 100755 index 0000000..f73a8eb --- /dev/null +++ b/make-archive.pl @@ -0,0 +1,208 @@ +#!/usr/bin/perl + +use v5.30; +use strict; +use warnings; +no warnings 'experimental::signatures'; +use feature 'signatures'; +use autodie; + +use FindBin qw( $Bin ); +use File::Spec; +use lib File::Spec->catdir( $Bin, 'lib' ); + +use Cwd qw( abs_path ); +use File::Basename qw( basename ); +use File::Copy qw( copy ); +use File::Temp qw( tempdir ); +use Getopt::Long; + +sub main { + GetOptions( + 'executable-name=s' => \my $executable_name, + 'target=s' => \my $target, + 'archive-name=s' => \my $archive_name, + 'changes-file=s' => \my $changes_file, + 'extra-files=s' => \my $extra_files, + ); + + if ( !$executable_name ) { + die 'The --executable-name option is required.'; + } + + if ( !( $target || $archive_name ) ) { + die 'You must provide either a target or archive-name when using this action.'; + } + + if ( $changes_file && !-f $changes_file ) { + die "Changes file '$changes_file' does not exist."; + } + + if ( !$archive_name ) { + $archive_name = $executable_name . q{-} . target_to_archive_name($target); + } + + my $archive_extension = $ENV{RUNNER_OS} eq 'Windows' ? '.zip' : '.tar.gz'; + my $archive_file = File::Spec->catfile( abs_path('.'), $archive_name . $archive_extension ); + + $executable_name .= '.exe' if $ENV{RUNNER_OS} eq 'Windows'; + + my @look_for = ( + File::Spec->catfile( 'target', $target, 'release', $executable_name ), + File::Spec->catfile( 'target', 'release', $executable_name ) + ); + my @files; + for my $file (@look_for) { + if ( -f $file ) { + say "Found executable at $file"; + push @files, $file; + last; + } + } + + if ( !@files ) { + my $msg = "Could not find executable in any of:\n"; + $msg .= " $_\n" for @look_for; + die $msg; + } + + if ($extra_files) { + $extra_files =~ s/^\s+|\s+$//gs; + push @files, split /\n/, $extra_files; + } + else { + push @files, $changes_file; + push @files, glob 'README*'; + } + + my $td = tempdir( CLEANUP => 1 ); + for my $file (@files) { + copy( $file => $td ) + or die "Cannot copy $file => $td: $!"; + } + + chdir $td; + + my @cmd + = $ENV{RUNNER_OS} eq 'Windows' + ? ( '7z', 'a', $archive_file, glob('*') ) + : ( 'tar', 'czf', $archive_file, glob('*') ); + + say "Running [@cmd]"; + system(@cmd); + + open my $fh, '>>', $ENV{GITHUB_OUTPUT}; + my $archive_basename = basename($archive_file); + say {$fh} "archive-basename=$archive_basename"; + close $fh; +} + +sub target_to_archive_name($target) { + my ( $cpu, @rest ) = split /-/, $target; + $cpu =~ s/aarch64/arm64/; + + if ( $rest[0] =~ /^(?:apple|pc|sun|unknown)$/ ) { + shift @rest; + } + my $os = shift @rest; + + # If there's more it's something like "-gnu" or "-msvc". + if (@rest) { + $os .= '-' . $rest[0]; + } + + unless ( $os =~ s/darwin/macOS/ + || $os =~ s/freebsd/FreeBSD/ + || $os =~ s/ios/iOS/ + || $os =~ s/netbsd/NetBSD/ + || $os =~ s/openbsd/OpenBSD/ ) { + + $os = ucfirst $os; + } + + return "$os-$cpu"; +} + +sub test { + require Test::More; + Test::More->import; + + my %tests = ( + 'aarch64-apple-darwin' => 'macOS-arm64', + 'aarch64-apple-ios' => 'iOS-arm64', + 'aarch64-apple-ios-sim' => 'iOS-sim-arm64', + 'aarch64-linux-android' => 'Linux-android-arm64', + 'aarch64-pc-windows-gnullvm' => 'Windows-gnullvm-arm64', + 'aarch64-pc-windows-msvc' => 'Windows-msvc-arm64', + 'aarch64-unknown-fuchsia' => 'Fuchsia-arm64', + 'aarch64-unknown-linux-gnu' => 'Linux-gnu-arm64', + 'aarch64-unknown-linux-musl' => 'Linux-musl-arm64', + 'aarch64-unknown-linux-ohos' => 'Linux-ohos-arm64', + 'arm-linux-androideabi' => 'Linux-androideabi-arm', + 'arm-unknown-linux-gnueabi' => 'Linux-gnueabi-arm', + 'arm-unknown-linux-gnueabihf' => 'Linux-gnueabihf-arm', + 'arm-unknown-linux-musleabi' => 'Linux-musleabi-arm', + 'arm-unknown-linux-musleabihf' => 'Linux-musleabihf-arm', + 'armv5te-unknown-linux-gnueabi' => 'Linux-gnueabi-armv5te', + 'armv5te-unknown-linux-musleabi' => 'Linux-musleabi-armv5te', + 'armv7-linux-androideabi' => 'Linux-androideabi-armv7', + 'armv7-unknown-linux-gnueabi' => 'Linux-gnueabi-armv7', + 'armv7-unknown-linux-gnueabihf' => 'Linux-gnueabihf-armv7', + 'armv7-unknown-linux-musleabi' => 'Linux-musleabi-armv7', + 'armv7-unknown-linux-musleabihf' => 'Linux-musleabihf-armv7', + 'armv7-unknown-linux-ohos' => 'Linux-ohos-armv7', + 'i586-pc-windows-msvc' => 'Windows-msvc-i586', + 'i586-unknown-linux-gnu' => 'Linux-gnu-i586', + 'i586-unknown-linux-musl' => 'Linux-musl-i586', + 'i686-linux-android' => 'Linux-android-i686', + 'i686-pc-windows-gnu' => 'Windows-gnu-i686', + 'i686-pc-windows-gnullvm' => 'Windows-gnullvm-i686', + 'i686-pc-windows-msvc' => 'Windows-msvc-i686', + 'i686-unknown-freebsd' => 'FreeBSD-i686', + 'i686-unknown-linux-gnu' => 'Linux-gnu-i686', + 'i686-unknown-linux-musl' => 'Linux-musl-i686', + 'loongarch64-unknown-linux-gnu' => 'Linux-gnu-loongarch64', + 'powerpc-unknown-linux-gnu' => 'Linux-gnu-powerpc', + 'powerpc64-unknown-linux-gnu' => 'Linux-gnu-powerpc64', + 'powerpc64le-unknown-linux-gnu' => 'Linux-gnu-powerpc64le', + 'riscv64gc-unknown-linux-gnu' => 'Linux-gnu-riscv64gc', + 's390x-unknown-linux-gnu' => 'Linux-gnu-s390x', + 'sparc64-unknown-linux-gnu' => 'Linux-gnu-sparc64', + 'sparcv9-sun-solaris' => 'Solaris-sparcv9', + 'thumbv7neon-linux-androideabi' => 'Linux-androideabi-thumbv7neon', + 'thumbv7neon-unknown-linux-gnueabihf' => 'Linux-gnueabihf-thumbv7neon', + 'x86_64-apple-darwin' => 'macOS-x86_64', + 'x86_64-apple-ios' => 'iOS-x86_64', + 'x86_64-linux-android' => 'Linux-android-x86_64', + 'x86_64-pc-solaris' => 'Solaris-x86_64', + 'x86_64-pc-windows-gnu' => 'Windows-gnu-x86_64', + 'x86_64-pc-windows-gnullvm' => 'Windows-gnullvm-x86_64', + 'x86_64-pc-windows-msvc' => 'Windows-msvc-x86_64', + 'x86_64-unknown-freebsd' => 'FreeBSD-x86_64', + 'x86_64-unknown-fuchsia' => 'Fuchsia-x86_64', + 'x86_64-unknown-illumos' => 'Illumos-x86_64', + 'x86_64-unknown-linux-gnu' => 'Linux-gnu-x86_64', + 'x86_64-unknown-linux-gnux32' => 'Linux-gnux32-x86_64', + 'x86_64-unknown-linux-musl' => 'Linux-musl-x86_64', + 'x86_64-unknown-linux-ohos' => 'Linux-ohos-x86_64', + 'x86_64-unknown-netbsd' => 'NetBSD-x86_64', + 'x86_64-unknown-redox' => 'Redox-x86_64', + ); + + for my $target ( keys %tests ) { + is( + target_to_archive_name($target), + $tests{$target}, + "$target == $tests{$target}", + ); + } + + done_testing(); +} + +if ( $ENV{TAP_VERSION} ) { + test(); +} +else { + main(); +} diff --git a/perltidyrc b/perltidyrc new file mode 100644 index 0000000..863fe02 --- /dev/null +++ b/perltidyrc @@ -0,0 +1,22 @@ +-l=100 +-i=4 +-ci=4 +-se +-b +-bar +-boc +-vt=0 +-vtc=0 +-cti=0 +-pt=1 +-bt=1 +-sbt=1 +-bbt=1 +-nolq +-npro +-nsfs +--blank-lines-before-packages=0 +--opening-hash-brace-right +--no-outdent-long-comments +--iterations=2 +-wbb="% + - * / x != == >= <= =~ !~ < > | & >= < = **= += *= &= <<= &&= -= /= |= >>= ||= .= %= ^= x=" diff --git a/precious.toml b/precious.toml new file mode 100644 index 0000000..c6ef685 --- /dev/null +++ b/precious.toml @@ -0,0 +1,74 @@ +exclude = [ + "target", + "tests/lib/**/*", +] + +[commands.typos] +type = "both" +include = "**/*" +exclude = "**/*.tar.gz" +invoke = "once" +cmd = "typos" +tidy_flags = "--write-changes" +ok-exit-codes = 0 +lint-failure-exit-codes = 2 + +[commands.perltidy] +type = "both" +include = [ "**/*.{pl,pm,t,psgi}" ] +exclude = "tests/lib/**" +cmd = [ "perltidy", "--profile=$PRECIOUS_ROOT/perltidyrc" ] +lint_flags = [ "--assert-tidy", "--no-standard-output", "--outfile=/dev/null" ] +tidy_flags = [ "--backup-and-modify-in-place", "--backup-file-extension=/" ] +ok_exit_codes = 0 +lint_failure_exit_codes = 2 +ignore_stderr = "Begin Error Output Stream" + +[commands.prettier-md] +type = "both" +include = [ "**/*.md" ] +cmd = [ "./node_modules/.bin/prettier", "--no-config", "--print-width", "100", "--prose-wrap", "always" ] +lint_flags = "--check" +tidy_flags = "--write" +ok_exit_codes = 0 +lint_failure_exit_codes = 1 +ignore_stderr = [ "Code style issues" ] + +[commands.prettier-yml] +type = "both" +include = [ "**/*.yml" ] +cmd = [ "./node_modules/.bin/prettier", "--no-config" ] +lint_flags = "--check" +tidy_flags = "--write" +ok_exit_codes = 0 +lint_failure_exit_codes = 1 +ignore_stderr = [ "Code style issues" ] + +[commands.omegasort-gitignore] +type = "both" +include = "**/.gitignore" +cmd = [ "omegasort", "--sort", "path", "--unique" ] +lint_flags = "--check" +tidy_flags = "--in-place" +ok_exit_codes = 0 +lint_failure_exit_codes = 1 +ignore_stderr = [ + "The .+ file is not sorted", + "The .+ file is not unique", +] + +[commands.shellcheck] +type = "lint" +include = "**/*.sh" +cmd = "shellcheck" +ok_exit_codes = 0 +lint_failure_exit_codes = 1 + +[commands.shfmt] +type = "both" +include = "**/*.sh" +cmd = ["shfmt", "--simplify", "--indent", "4"] +lint_flags = "--diff" +tidy_flags = "--write" +ok_exit_codes = 0 +lint_failure_exit_codes = 1 diff --git a/test-project/Cargo.lock b/test-project/Cargo.lock new file mode 100644 index 0000000..6e82782 --- /dev/null +++ b/test-project/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "test-project" +version = "0.1.0" diff --git a/test-project/Cargo.toml b/test-project/Cargo.toml new file mode 100644 index 0000000..7fded9f --- /dev/null +++ b/test-project/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "test-project" +version = "0.1.0" +edition = "2021" + +# workaround for https://github.com/cross-rs/cross/issues/1345 +[package.metadata.cross.target.x86_64-unknown-netbsd] +pre-build = [ + "mkdir -p /tmp/netbsd", + "curl https://cdn.netbsd.org/pub/NetBSD/NetBSD-9.2/amd64/binary/sets/base.tar.xz -O", + "tar -C /tmp/netbsd -xJf base.tar.xz", + "cp /tmp/netbsd/usr/lib/libexecinfo.so /usr/local/x86_64-unknown-netbsd/lib", + "rm base.tar.xz", + "rm -rf /tmp/netbsd", +] diff --git a/test-project/src/main.rs b/test-project/src/main.rs new file mode 100644 index 0000000..b80f5b7 --- /dev/null +++ b/test-project/src/main.rs @@ -0,0 +1,11 @@ +fn main() { + println!("Hello, world!"); +} + +#[cfg(test)] +mod test { + #[test] + fn test_something() { + assert_eq!(1, 1); + } +} diff --git a/tests/check-release.pl b/tests/check-release.pl new file mode 100755 index 0000000..516e0ff --- /dev/null +++ b/tests/check-release.pl @@ -0,0 +1,89 @@ +#!/usr/bin/env perl + +use v5.30; +use strict; +use warnings; +no warnings 'experimental::signatures'; +use feature 'signatures'; + +use FindBin qw( $Bin ); +use File::Spec; +use lib File::Spec->catdir( $Bin, 'lib' ); + +use autodie qw( :all ); + +use Digest::SHA; +use File::Temp qw( tempdir ); +use Getopt::Long; +use IPC::System::Simple qw( capturex ); +use Test::More; + +sub main { + my $artifact_id; + my $executable_name; + my $github_token; + my $repo; + my $target; + + GetOptions( + 'artifact-id=s' => \$artifact_id, + 'executable-name=s' => \$executable_name, + 'github-token=s' => \$github_token, + 'repo=s' => \$repo, + 'target=s' => \$target, + ); + + # We want to run this in a clean dir to avoid any conflicts with files in the current dir, like + # the archive file containing the release. + my $td = tempdir(); + chdir $td; + + system( + 'curl', + '-L', + '-H', 'Accept: application/vnd.github+json', + '-H', "Authorization: Bearer $github_token", + '-o', 'artifact.zip', + "https://api.github.com/repos/$repo/actions/artifacts/$artifact_id/zip", + ); + + system( 'unzip', 'artifact.zip' ); + + my $glob = $target =~ /windows/i ? 'test-project*.zip*' : 'test-project*.tar.gz*'; + my @files = glob $glob; + + is( scalar @files, 2, 'found two files in the artifact tarball' ) + or diag("@files"); + my ($archive_file) = grep { !/sha256/ } @files; + my ($checksum_file) = grep {/sha256/} @files; + + ok( $archive_file, 'found an archive file in the artifact tarball' ); + ok( $checksum_file, 'found a checksum file in the artifact tarball' ); + + open my $fh, '<', $checksum_file; + my $sha256_contents = do { local $/; <$fh> }; + $sha256_contents =~ s/^\s+|\s+$//g; + my ( $checksum, $filename ) = $sha256_contents =~ /^(\S+) [ \*](\S+)$/; + is( $filename, $archive_file, 'filename in checksum file matches archive filename' ) + or diag($sha256_contents); + + # I would prefer to just run shasum but I wasn't able to get it to run on Windows. + my $sha = Digest::SHA->new(256); + $sha->addfile($filename); + is( $checksum, $sha->hexdigest, 'checksum in checksum file is correct' ); + + if ( $archive_file =~ /\.zip$/ ) { + system( 'unzip', $archive_file ); + } + else { + system( 'tar', 'xzf', $archive_file ); + } + + for my $file ( $executable_name, qw( README.md Changes.md ) ) { + ok( -f $file, "$file exists after unpacking archive" ); + } + + done_testing(); +} + +main(); diff --git a/tests/lib/IPC/System/Simple.pm b/tests/lib/IPC/System/Simple.pm new file mode 100644 index 0000000..c869862 --- /dev/null +++ b/tests/lib/IPC/System/Simple.pm @@ -0,0 +1,1115 @@ +package IPC::System::Simple; + +# ABSTRACT: Run commands simply, with detailed diagnostics + +use 5.006; +use strict; +use warnings; +use re 'taint'; +use Carp; +use List::Util qw(first); +use Scalar::Util qw(tainted); +use Config; +use constant WINDOWS => ($^O eq 'MSWin32'); +use constant VMS => ($^O eq 'VMS'); + +BEGIN { + + # It would be lovely to use the 'if' module here, but it didn't + # enter core until 5.6.2, and we want to keep 5.6.0 compatibility. + + + if (WINDOWS) { + + ## no critic (ProhibitStringyEval) + + eval q{ + use Win32::Process qw(INFINITE NORMAL_PRIORITY_CLASS); + use File::Spec; + use Win32; + use Win32::ShellQuote; + + # This uses the same rules as the core win32.c/get_shell() call. + use constant WINDOWS_SHELL => eval { Win32::IsWinNT() } + ? [ File::Spec->catfile(Win32::GetFolderPath(Win32::CSIDL_SYSTEM), 'cmd.exe'), '/x/d/c' ] + : [ File::Spec->catfile(Win32::GetFolderPath(Win32::CSIDL_SYSTEM), 'command.com'), '/c' ]; + + # These are used when invoking _win32_capture + use constant NO_SHELL => 0; + use constant USE_SHELL => 1; + + }; + + ## use critic + + # Die nosily if any of the above broke. + die $@ if $@; + } +} + +# Note that we don't use WIFSTOPPED because perl never uses +# the WUNTRACED flag, and hence will never return early from +# system() if the child processes is suspended with a SIGSTOP. + +use POSIX qw(WIFEXITED WEXITSTATUS WIFSIGNALED WTERMSIG); + +use constant FAIL_START => q{"%s" failed to start: "%s"}; +use constant FAIL_PLUMBING => q{Error in IPC::System::Simple plumbing: "%s" - "%s"}; +use constant FAIL_CMD_BLANK => q{Entirely blank command passed: "%s"}; +use constant FAIL_INTERNAL => q{Internal error in IPC::System::Simple: "%s"}; +use constant FAIL_TAINT => q{%s called with tainted argument "%s"}; +use constant FAIL_TAINT_ENV => q{%s called with tainted environment $ENV{%s}}; +use constant FAIL_SIGNAL => q{"%s" died to signal "%s" (%d)%s}; +use constant FAIL_BADEXIT => q{"%s" unexpectedly returned exit value %d}; + +use constant FAIL_UNDEF => q{%s called with undefined command}; + + +use constant FAIL_POSIX => q{IPC::System::Simple does not understand the POSIX error '%s'. Please check https://metacpan.org/pod/IPC::System::Simple to see if there is an updated version. If not please report this as a bug to https://github.com/pjf/ipc-system-simple/issues}; + +# On Perl's older than 5.8.x we can't assume that there'll be a +# $^{TAINT} for us to check, so we assume that our args may always +# be tainted. +use constant ASSUME_TAINTED => ($] < 5.008); + +use constant EXIT_ANY_CONST => -1; # Used internally +use constant EXIT_ANY => [ EXIT_ANY_CONST ]; # Exported + +use constant UNDEFINED_POSIX_RE => qr{not (?:defined|a valid) POSIX macro|not implemented on this architecture}; + +require Exporter; +our @ISA = qw(Exporter); + +our @EXPORT_OK = qw( + capture capturex + run runx + system systemx + $EXITVAL EXIT_ANY +); + +our $VERSION = '1.30'; +$VERSION =~ tr/_//d; + +our $EXITVAL = -1; + +my @Signal_from_number = split(' ', $Config{sig_name}); + +# Environment variables we don't want to see tainted. +my @Check_tainted_env = qw(PATH IFS CDPATH ENV BASH_ENV); +if (WINDOWS) { + push(@Check_tainted_env, 'PERL5SHELL'); +} +if (VMS) { + push(@Check_tainted_env, 'DCL$PATH'); +} + +# Not all systems implement the WIFEXITED calls, but POSIX +# will always export them (even if they're just stubs that +# die with an error). Test for the presence of a working +# WIFEXITED and friends, or define our own. + +eval { WIFEXITED(0); }; + +if ($@ =~ UNDEFINED_POSIX_RE) { + no warnings 'redefine'; ## no critic + *WIFEXITED = sub { not $_[0] & 0xff }; + *WEXITSTATUS = sub { $_[0] >> 8 }; + *WIFSIGNALED = sub { $_[0] & 127 }; + *WTERMSIG = sub { $_[0] & 127 }; +} elsif ($@) { + croak sprintf FAIL_POSIX, $@; +} + +# None of the POSIX modules I've found define WCOREDUMP, although +# many systems define it. Check the POSIX module in the hope that +# it may actually be there. + + +# TODO: Ideally, $NATIVE_WCOREDUMP should be a constant. + +my $NATIVE_WCOREDUMP; + +eval { POSIX::WCOREDUMP(1); }; + +if ($@ =~ UNDEFINED_POSIX_RE) { + *WCOREDUMP = sub { $_[0] & 128 }; + $NATIVE_WCOREDUMP = 0; +} elsif ($@) { + croak sprintf FAIL_POSIX, $@; +} else { + # POSIX actually has it defined! Huzzah! + *WCOREDUMP = \&POSIX::WCOREDUMP; + $NATIVE_WCOREDUMP = 1; +} + +sub _native_wcoredump { + return $NATIVE_WCOREDUMP; +} + +# system simply calls run + +no warnings 'once'; ## no critic +*system = \&run; +*systemx = \&runx; +use warnings; + +# run is our way of running a process with system() semantics + +sub run { + + _check_taint(@_); + + my ($valid_returns, $command, @args) = _process_args(@_); + + # If we have arguments, we really want to call systemx, + # so we do so. + + if (@args) { + return systemx($valid_returns, $command, @args); + } + + if (WINDOWS) { + my $pid = _spawn_or_die(&WINDOWS_SHELL->[0], join ' ', @{&WINDOWS_SHELL}, $command); + $pid->Wait(INFINITE); # Wait for process exit. + $pid->GetExitCode($EXITVAL); + return _check_exit($command,$EXITVAL,$valid_returns); + } + + # Without arguments, we're calling system, and checking + # the results. + + # We're throwing our own exception on command not found, so + # we don't need a warning from Perl. + + { + # silence 'Statement unlikely to be reached' warning + no warnings 'exec'; ## no critic + CORE::system($command,@args); + } + + return _process_child_error($?,$command,$valid_returns); +} + +# runx is just like system/run, but *never* invokes the shell. + +sub runx { + _check_taint(@_); + + my ($valid_returns, $command, @args) = _process_args(@_); + + if (WINDOWS) { + our $EXITVAL = -1; + + my $pid = _spawn_or_die($command, Win32::ShellQuote::quote_native($command, @args)); + + $pid->Wait(INFINITE); # Wait for process exit. + $pid->GetExitCode($EXITVAL); + return _check_exit($command,$EXITVAL,$valid_returns); + } + + # If system() fails, we throw our own exception. We don't + # need to have perl complain about it too. + + no warnings; ## no critic + + CORE::system { $command } $command, @args; + + return _process_child_error($?, $command, $valid_returns); +} + +# capture is our way of running a process with backticks/qx semantics + +sub capture { + _check_taint(@_); + + my ($valid_returns, $command, @args) = _process_args(@_); + + if (@args) { + return capturex($valid_returns, $command, @args); + } + + if (WINDOWS) { + # USE_SHELL really means "You may use the shell if you need it." + return _win32_capture(USE_SHELL, $valid_returns, $command); + } + + our $EXITVAL = -1; + + my $wantarray = wantarray(); + + # We'll produce our own warnings on failure to execute. + no warnings 'exec'; ## no critic + + if ($wantarray) { + my @results = qx($command); + _process_child_error($?,$command,$valid_returns); + return @results; + } + + my $results = qx($command); + _process_child_error($?,$command,$valid_returns); + return $results; +} + +# _win32_capture implements the capture and capurex commands on Win32. +# We need to wrap the whole internals of this sub into +# an if (WINDOWS) block to avoid it being compiled on non-Win32 systems. + +sub _win32_capture { + if (not WINDOWS) { + croak sprintf(FAIL_INTERNAL, "_win32_capture called when not under Win32"); + } else { + + my ($use_shell, $valid_returns, $command, @args) = @_; + + my $wantarray = wantarray(); + + # Perl doesn't support multi-arg open under + # Windows. Perl also doesn't provide very good + # feedback when normal backtails fail, either; + # it returns exit status from the shell + # (which is indistinguishable from the command + # running and producing the same exit status). + + # As such, we essentially have to write our own + # backticks. + + # We start by dup'ing STDOUT. + + open(my $saved_stdout, '>&', \*STDOUT) ## no critic + or croak sprintf(FAIL_PLUMBING, "Can't dup STDOUT", $!); + + # We now open up a pipe that will allow us to + # communicate with the new process. + + pipe(my ($read_fh, $write_fh)) + or croak sprintf(FAIL_PLUMBING, "Can't create pipe", $!); + + # Allow CRLF sequences to become "\n", since + # this is what Perl backticks do. + + binmode($read_fh, ':crlf'); + + # Now we re-open our STDOUT to $write_fh... + + open(STDOUT, '>&', $write_fh) ## no critic + or croak sprintf(FAIL_PLUMBING, "Can't redirect STDOUT", $!); + + # If we have args, or we're told not to use the shell, then + # we treat $command as our shell. Otherwise we grub around + # in our command to look for a command to run. + # + # Note that we don't actually *use* the shell (although in + # a future version we might). Being told not to use the shell + # (capturex) means we treat our command as really being a command, + # and not a command line. + + my $exe = @args ? $command : + (! $use_shell) ? $command : + $command =~ m{^"([^"]+)"}x ? $1 : + $command =~ m{(\S+) }x ? $1 : + croak sprintf(FAIL_CMD_BLANK, $command); + + # And now we spawn our new process with inherited + # filehandles. + + my $err; + my $pid = eval { + _spawn_or_die($exe, @args ? Win32::ShellQuote::quote_native($command, @args) : $command); + } + or do { + $err = $@; + }; + + # Regardless of whether our command ran, we must restore STDOUT. + # RT #48319 + open(STDOUT, '>&', $saved_stdout) ## no critic + or croak sprintf(FAIL_PLUMBING,"Can't restore STDOUT", $!); + + # And now, if there was an actual error , propagate it. + die $err if defined $err; # If there's an error from _spawn_or_die + + # Clean-up the filehandles we no longer need... + + close($write_fh) + or croak sprintf(FAIL_PLUMBING,q{Can't close write end of pipe}, $!); + close($saved_stdout) + or croak sprintf(FAIL_PLUMBING,q{Can't close saved STDOUT}, $!); + + # Read the data from our child... + + my (@results, $result); + + if ($wantarray) { + @results = <$read_fh>; + } else { + $result = join("",<$read_fh>); + } + + # Tidy up our windows process and we're done! + + $pid->Wait(INFINITE); # Wait for process exit. + $pid->GetExitCode($EXITVAL); + + _check_exit($command,$EXITVAL,$valid_returns); + + return $wantarray ? @results : $result; + + } +} + +# capturex() is just like backticks/qx, but never invokes the shell. + +sub capturex { + _check_taint(@_); + + my ($valid_returns, $command, @args) = _process_args(@_); + + our $EXITVAL = -1; + + my $wantarray = wantarray(); + + if (WINDOWS) { + return _win32_capture(NO_SHELL, $valid_returns, $command, @args); + } + + # We can't use a multi-arg piped open here, since 5.6.x + # doesn't like them. Instead we emulate what 5.8.x does, + # which is to create a pipe(), set the close-on-exec flag + # on the child, and the fork/exec. If the exec fails, the + # child writes to the pipe. If the exec succeeds, then + # the pipe closes without data. + + pipe(my ($read_fh, $write_fh)) + or croak sprintf(FAIL_PLUMBING, "Can't create pipe", $!); + + # This next line also does an implicit fork. + my $pid = open(my $pipe, '-|'); ## no critic + + if (not defined $pid) { + croak sprintf(FAIL_START, $command, $!); + } elsif (not $pid) { + # Child process, execs command. + + close($read_fh); + + # TODO: 'no warnings exec' doesn't get rid + # of the 'unlikely to be reached' warnings. + # This is a bug in perl / perldiag / perllexwarn / warnings. + + no warnings; ## no critic + + CORE::exec { $command } $command, @args; + + # Oh no, exec fails! Send the reason why to + # the parent. + + print {$write_fh} int($!); + exit(-1); + } + + { + # In parent process. + + close($write_fh); + + # Parent process, check for child error. + my $error = <$read_fh>; + + # Tidy up our pipes. + close($read_fh); + + # Check for error. + if ($error) { + # Setting $! to our child error number gives + # us nice looking strings when printed. + local $! = $error; + croak sprintf(FAIL_START, $command, $!); + } + } + + # Parent process, we don't care about our pid, but we + # do go and read our pipe. + + if ($wantarray) { + my @results = <$pipe>; + close($pipe); + _process_child_error($?,$command,$valid_returns); + return @results; + } + + # NB: We don't check the return status on close(), since + # on failure it sets $?, which we then inspect for more + # useful information. + + my $results = join("",<$pipe>); + close($pipe); + _process_child_error($?,$command,$valid_returns); + + return $results; + +} + +# Tries really hard to spawn a process under Windows. Returns +# the pid on success, or undef on error. + +sub _spawn_or_die { + + # We need to wrap practically the entire sub in an + # if block to ensure it doesn't get compiled under non-Win32 + # systems. Compiling on these systems would not only be a + # waste of time, but also results in complaints about + # the NORMAL_PRIORITY_CLASS constant. + + if (not WINDOWS) { + croak sprintf(FAIL_INTERNAL, "_spawn_or_die called when not under Win32"); + } else { + my ($orig_exe, $cmdline) = @_; + my $pid; + + my $exe = $orig_exe; + + # If our command doesn't have an extension, add one. + $exe .= $Config{_exe} if ($exe !~ m{\.}); + + Win32::Process::Create( + $pid, $exe, $cmdline, 1, NORMAL_PRIORITY_CLASS, "." + ) and return $pid; + + my @path = split(/;/,$ENV{PATH}); + + foreach my $dir (@path) { + my $fullpath = File::Spec->catfile($dir,$exe); + + # We're using -x here on the assumption that stat() + # is faster than spawn, so trying to spawn a process + # for each path element will be unacceptably + # inefficient. + + if (-x $fullpath) { + Win32::Process::Create( + $pid, $fullpath, $cmdline, 1, + NORMAL_PRIORITY_CLASS, "." + ) and return $pid; + } + } + + croak sprintf(FAIL_START, $orig_exe, $^E); + } +} + +# Complain on tainted arguments or environment. +# ASSUME_TAINTED is true for 5.6.x, since it's missing ${^TAINT} + +sub _check_taint { + return if not (ASSUME_TAINTED or ${^TAINT}); + my $caller = (caller(1))[3]; + foreach my $var (@_) { + if (tainted $var) { + croak sprintf(FAIL_TAINT, $caller, $var); + } + } + foreach my $var (@Check_tainted_env) { + if (tainted $ENV{$var} ) { + croak sprintf(FAIL_TAINT_ENV, $caller, $var); + } + } + + return; + +} + +# This subroutine performs the difficult task of interpreting +# $?. It's not intended to be called directly, as it will +# croak on errors, and its implementation and interface may +# change in the future. + +sub _process_child_error { + my ($child_error, $command, $valid_returns) = @_; + + $EXITVAL = -1; + + my $coredump = WCOREDUMP($child_error); + + # There's a bug in perl 5.8.9 and 5.10.0 where if the system + # does not provide a native WCOREDUMP, then $? will + # never contain coredump information. This code + # checks to see if we have the bug, and works around + # it if needed. + + if ($] >= 5.008009 and not $NATIVE_WCOREDUMP) { + $coredump ||= WCOREDUMP( ${^CHILD_ERROR_NATIVE} ); + } + + if ($child_error == -1) { + croak sprintf(FAIL_START, $command, $!); + + } elsif ( WIFEXITED( $child_error ) ) { + $EXITVAL = WEXITSTATUS( $child_error ); + + return _check_exit($command,$EXITVAL,$valid_returns); + + } elsif ( WIFSIGNALED( $child_error ) ) { + my $signal_no = WTERMSIG( $child_error ); + my $signal_name = $Signal_from_number[$signal_no] || "UNKNOWN"; + + croak sprintf FAIL_SIGNAL, $command, $signal_name, $signal_no, ($coredump ? " and dumped core" : ""); + + + } + + croak sprintf(FAIL_INTERNAL, qq{'$command' ran without exit value or signal}); + +} + +# A simple subroutine for checking exit values. Results in better +# assurance of consistent error messages, and better forward support +# for new features in I::S::S. + +sub _check_exit { + my ($command, $exitval, $valid_returns) = @_; + + # If we have a single-value list consisting of the EXIT_ANY + # value, then we're happy with whatever exit value we're given. + if (@$valid_returns == 1 and $valid_returns->[0] == EXIT_ANY_CONST) { + return $exitval; + } + + if (not defined first { $_ == $exitval } @$valid_returns) { + croak sprintf FAIL_BADEXIT, $command, $exitval; + } + return $exitval; +} + + +# This subroutine simply determines a list of valid returns, the command +# name, and any arguments that we need to pass to it. + +sub _process_args { + my $valid_returns = [ 0 ]; + my $caller = (caller(1))[3]; + + if (not @_) { + croak "$caller called with no arguments"; + } + + if (ref $_[0] eq "ARRAY") { + $valid_returns = shift(@_); + } + + if (not @_) { + croak "$caller called with no command"; + } + + my $command = shift(@_); + + if (not defined $command) { + croak sprintf( FAIL_UNDEF, $caller ); + } + + return ($valid_returns,$command,@_); + +} + +1; + +__END__ + +=head1 NAME + +IPC::System::Simple - Run commands simply, with detailed diagnostics + +=head1 SYNOPSIS + + use IPC::System::Simple qw(system systemx capture capturex); + + system("some_command"); # Command succeeds or dies! + + system("some_command",@args); # Succeeds or dies, avoids shell if @args + + systemx("some_command",@args); # Succeeds or dies, NEVER uses the shell + + + # Capture the output of a command (just like backticks). Dies on error. + my $output = capture("some_command"); + + # Just like backticks in list context. Dies on error. + my @output = capture("some_command"); + + # As above, but avoids the shell if @args is non-empty + my $output = capture("some_command", @args); + + # As above, but NEVER invokes the shell. + my $output = capturex("some_command", @args); + my @output = capturex("some_command", @args); + +=head1 DESCRIPTION + +Calling Perl's in-built C function is easy, +determining if it was successful is I. Let's face it, +C<$?> isn't the nicest variable in the world to play with, and +even if you I check it, producing a well-formatted error +string takes a lot of work. + +C takes the hard work out of calling +external commands. In fact, if you want to be really lazy, +you can just write: + + use IPC::System::Simple qw(system); + +and all of your C commands will either succeed (run to +completion and return a zero exit value), or die with rich diagnostic +messages. + +The C module also provides a simple replacement +to Perl's backticks operator. Simply write: + + use IPC::System::Simple qw(capture); + +and then use the L command just like you'd use backticks. +If there's an error, it will die with a detailed description of what +went wrong. Better still, you can even use C to run the +equivalent of backticks, but without the shell: + + use IPC::System::Simple qw(capturex); + + my $result = capturex($command, @args); + +If you want more power than the basic interface, including the +ability to specify which exit values are acceptable, trap errors, +or process diagnostics, then read on! + +=head1 ADVANCED SYNOPSIS + + use IPC::System::Simple qw( + capture capturex system systemx run runx $EXITVAL EXIT_ANY + ); + + # Run a command, throwing exception on failure + + run("some_command"); + + runx("some_command",@args); # Run a command, avoiding the shell + + # Do the same thing, but with the drop-in system replacement. + + system("some_command"); + + systemx("some_command", @args); + + # Run a command which must return 0..5, avoid the shell, and get the + # exit value (we could also look at $EXITVAL) + + my $exit_value = runx([0..5], "some_command", @args); + + # The same, but any exit value will do. + + my $exit_value = runx(EXIT_ANY, "some_command", @args); + + # Capture output into $result and throw exception on failure + + my $result = capture("some_command"); + + # Check exit value from captured command + + print "some_command exited with status $EXITVAL\n"; + + # Captures into @lines, splitting on $/ + my @lines = capture("some_command"); + + # Run a command which must return 0..5, capture the output into + # @lines, and avoid the shell. + + my @lines = capturex([0..5], "some_command", @args); + +=head1 ADVANCED USAGE + +=head2 run() and system() + +C provides a subroutine called +C, that executes a command using the same semantics as +Perl's built-in C: + + use IPC::System::Simple qw(run); + + run("cat *.txt"); # Execute command via the shell + run("cat","/etc/motd"); # Execute command without shell + +The primary difference between Perl's in-built system and +the C command is that C will throw an exception on +failure, and allows a list of acceptable exit values to be set. +See L for further information. + +In fact, you can even have C replace the +default C function for your package so it has the +same behaviour: + + use IPC::System::Simple qw(system); + + system("cat *.txt"); # system now succeeds or dies! + +C and C are aliases to each other. + +See also L for variants of +C and C that never invoke the shell, even with +a single argument. + +=head2 capture() + +A second subroutine, named C executes a command with +the same semantics as Perl's built-in backticks (and C): + + use IPC::System::Simple qw(capture); + + # Capture text while invoking the shell. + my $file = capture("cat /etc/motd"); + my @lines = capture("cat /etc/passwd"); + +However unlike regular backticks, which always use the shell, C +will bypass the shell when called with multiple arguments: + + # Capture text while avoiding the shell. + my $file = capture("cat", "/etc/motd"); + my @lines = capture("cat", "/etc/passwd"); + +See also L for a variant of +C that never invokes the shell, even with a single +argument. + +=head2 runx(), systemx() and capturex() + +The C, C and C commands are identical +to the multi-argument forms of C, C and C +respectively, but I invoke the shell, even when called with a +single argument. These forms are particularly useful when a command's +argument list I be empty, for example: + + systemx($cmd, @args); + +The use of C here guarantees that the shell will I +be invoked, even if C<@args> is empty. + +=head2 Exception handling + +In the case where the command returns an unexpected status, both C and +C will throw an exception, which if not caught will terminate your +program with an error. + +Capturing the exception is easy: + + eval { + run("cat *.txt"); + }; + + if ($@) { + print "Something went wrong - $@\n"; + } + +See the diagnostics section below for more details. + +=head3 Exception cases + +C considers the following to be unexpected, +and worthy of exception: + +=over 4 + +=item * + +Failing to start entirely (eg, command not found, permission denied). + +=item * + +Returning an exit value other than zero (but see below). + +=item * + +Being killed by a signal. + +=item * + +Being passed tainted data (in taint mode). + +=back + +=head2 Exit values + +Traditionally, system commands return a zero status for success and a +non-zero status for failure. C will default to throwing +an exception if a non-zero exit value is returned. + +You may specify a range of values which are considered acceptable exit +values by passing an I as the first argument. The +special constant C can be used to allow I exit value +to be returned. + + use IPC::System::Simple qw(run system capture EXIT_ANY); + + run( [0..5], "cat *.txt"); # Exit values 0-5 are OK + + system( [0..5], "cat *.txt"); # This works the same way + + my @lines = capture( EXIT_ANY, "cat *.txt"); # Any exit is fine. + +The C and replacement C subroutines returns the exit +value of the process: + + my $exit_value = run( [0..5], "cat *.txt"); + + # OR: + + my $exit_value = system( [0..5] "cat *.txt"); + + print "Program exited with value $exit_value\n"; + +=head3 $EXITVAL + +The exit value of any command executed by C +can always be retrieved from the C<$IPC::System::Simple::EXITVAL> +variable: + +This is particularly useful when inspecting results from C, +which returns the captured text from the command. + + use IPC::System::Simple qw(capture $EXITVAL EXIT_ANY); + + my @enemies_defeated = capture(EXIT_ANY, "defeat_evil", "/dev/mordor"); + + print "Program exited with value $EXITVAL\n"; + +C<$EXITVAL> will be set to C<-1> if the command did not exit normally (eg, +being terminated by a signal) or did not start. In this situation an +exception will also be thrown. + +=head2 WINDOWS-SPECIFIC NOTES + +The C subroutine make available the full 32-bit exit value on +Win32 systems. This has been true since C v0.06 +when called with multiple arguments, and since v1.25 when called with +a single argument. This is different from the previous versions of +C and from Perl's in-build C function, +which can only handle 8-bit return values. + +The C subroutine always returns the 32-bit exit value under +Windows. The C subroutine also never uses the shell, +even when passed a single argument. + +The C subroutine always uses a shell when passed a single +argument. On NT systems, it uses C in the system root, and on +non-NT systems it uses C in the system root. + +As of C v1.25, the C and C +subroutines, as well as multiple-argument calls to the C and +C subroutines, have their arguments properly quoted, so that +arugments with spaces and the like work properly. Unfortunately, this +breaks any attempt to invoke the shell itself. If you really need to +execute C or C, use the single-argument form. +For single-argument calls to C and C, the argument must +be properly shell-quoted in advance of the call. + +Versions of C before v0.09 would not search +the C environment variable when the multi-argument form of +C was called. Versions from v0.09 onwards correctly search +the path provided the command is provided including the extension +(eg, C rather than just C, or C rather +than just C). If no extension is provided, C<.exe> is +assumed. + +Signals are not supported on Windows systems. Sending a signal +to a Windows process will usually cause it to exit with the signal +number used. + +=head1 DIAGNOSTICS + +=over 4 + +=item "%s" failed to start: "%s" + +The command specified did not even start. It may not exist, or +you may not have permission to use it. The reason it could not +start (as determined from C<$!>) will be provided. + +=item "%s" unexpectedly returned exit value %d + +The command ran successfully, but returned an exit value we did +not expect. The value returned is reported. + +=item "%s" died to signal "%s" (%d) %s + +The command was killed by a signal. The name of the signal +will be reported, or C if it cannot be determined. The +signal number is always reported. If we detected that the +process dumped core, then the string C is +appended. + +=item IPC::System::Simple::%s called with no arguments + +You attempted to call C or C but did not provide any +arguments at all. At the very lease you need to supply a command +to run. + +=item IPC::System::Simple::%s called with no command + +You called C or C with a list of acceptable exit values, +but no actual command. + +=item IPC::System::Simple::%s called with tainted argument "%s" + +You called C or C with tainted (untrusted) arguments, which is +almost certainly a bad idea. To untaint your arguments you'll need to pass +your data through a regular expression and use the resulting match variables. +See L for more information. + +=item IPC::System::Simple::%s called with tainted environment $ENV{%s} + +You called C or C but part of your environment was tainted +(untrusted). You should either delete the named environment +variable before calling C, or set it to an untainted value +(usually one set inside your program). See +L for more information. + +=item Error in IPC::System::Simple plumbing: "%s" - "%s" + +Implementing the C command involves dark and terrible magicks +involving pipes, and one of them has sprung a leak. This could be due to a +lack of file descriptors, although there are other possibilities. + +If you are able to reproduce this error, you are encouraged +to submit a bug report according to the L section below. + +=item Internal error in IPC::System::Simple: "%s" + +You've found a bug in C. Please check to +see if an updated version of C is available. +If not, please file a bug report according to the L section +below. + +=item IPC::System::Simple::%s called with undefined command + +You've passed the undefined value as a command to be executed. +While this is a very Zen-like action, it's not supported by +Perl's current implementation. + +=back + +=head1 DEPENDENCIES + +This module depends upon L when used on Win32 +system. C is bundled as a core module in ActivePerl 5.6 +and above. + +There are no non-core dependencies on non-Win32 systems. + +=head1 COMPARISON TO OTHER APIs + +Perl provides a range of in-built functions for handling external +commands, and CPAN provides even more. The C +differentiates itself from other options by providing: + +=over 4 + +=item Extremely detailed diagnostics + +The diagnostics produced by C are designed +to provide as much information as possible. Rather than requiring +the developer to inspect C<$?>, C does the +hard work for you. + +If an odd exit status is provided, you're informed of what it is. If a +signal kills your process, you are informed of both its name and number. +If tainted data or environment prevents your command from running, you +are informed of exactly which data or environmental variable is +tainted. + +=item Exceptions on failure + +C takes an aggressive approach to error handling. +Rather than allow commands to fail silently, exceptions are thrown +when unexpected results are seen. This allows for easy development +using a try/catch style, and avoids the possibility of accidentally +continuing after a failed command. + +=item Easy access to exit status + +The C, C and C commands all set C<$EXITVAL>, +making it easy to determine the exit status of a command. +Additionally, the C and C interfaces return the exit +status. + +=item Consistent interfaces + +When called with multiple arguments, the C, C and +C interfaces I invoke the shell. This differs +from the in-built Perl C command which may invoke the +shell under Windows when called with multiple arguments. It +differs from the in-built Perl backticks operator which always +invokes the shell. + +=back + +=head1 BUGS + +When C is exported, the exotic form C +is not supported. Attemping to use the exotic form is a syntax +error. This affects the calling package I. Use C +if you need it, or consider using the L module to replace +C with lexical scope. + +Core dumps are only checked for when a process dies due to a +signal. It is not believed there are any systems where processes +can dump core without dying to a signal. + +C status is not checked, as perl never spawns processes +with the C option. + +Signals are not supported under Win32 systems, since they don't +work at all like Unix signals. Win32 signals cause commands to +exit with a given exit value, which this modules I capture. + +=head2 Reporting bugs + +Before reporting a bug, please check to ensure you are using the +most recent version of C. Your problem may +have already been fixed in a new release. + +You can find the C bug-tracker at +L . +Please check to see if your bug has already been reported; if +in doubt, report yours anyway. + +Submitting a patch and/or failing test case will greatly expedite +the fixing of bugs. + +=head1 FEEDBACK + +If you find this module useful, please consider rating it on the +CPAN Ratings service at +L . + +The module author loves to hear how C has made +your life better (or worse). Feedback can be sent to +Epjf@perltraining.com.auE. + +=head1 SEE ALSO + +L uses C to provide succeed-or-die +replacements to C (and other built-ins) with lexical scope. + +L, L, L, L, L, +L, L + +=head1 AUTHOR + +Paul Fenwick Epjf@cpan.orgE + +=head1 COPYRIGHT AND LICENSE + +Copyright (C) 2006-2008 by Paul Fenwick + +This library is free software; you can redistribute it and/or modify +it under the same terms as Perl itself, either Perl version 5.6.0 or, +at your option, any later version of Perl 5 you may have available. + +=for Pod::Coverage WCOREDUMP + +=cut diff --git a/tests/lib/Path/Tiny.pm b/tests/lib/Path/Tiny.pm new file mode 100644 index 0000000..b8c6804 --- /dev/null +++ b/tests/lib/Path/Tiny.pm @@ -0,0 +1,3880 @@ +use 5.008001; +use strict; +use warnings; + +package Path::Tiny; +# ABSTRACT: File path utility + +our $VERSION = '0.144'; + +# Dependencies +use Config; +use Exporter 5.57 (qw/import/); +use File::Spec 0.86 (); # shipped with 5.8.1 +use Carp (); + +our @EXPORT = qw/path/; +our @EXPORT_OK = qw/cwd rootdir tempfile tempdir/; + +use constant { + PATH => 0, + CANON => 1, + VOL => 2, + DIR => 3, + FILE => 4, + TEMP => 5, + IS_WIN32 => ( $^O eq 'MSWin32' ), +}; + +use overload ( + q{""} => 'stringify', + bool => sub () { 1 }, + fallback => 1, +); + +# FREEZE/THAW per Sereal/CBOR/Types::Serialiser protocol +sub THAW { return path( $_[2] ) } +{ no warnings 'once'; *TO_JSON = *FREEZE = \&stringify }; + +my $HAS_UU; # has Unicode::UTF8; lazily populated + +sub _check_UU { + local $SIG{__DIE__}; # prevent outer handler from being called + !!eval { + require Unicode::UTF8; + Unicode::UTF8->VERSION(0.58); + 1; + }; +} + +my $HAS_PU; # has PerlIO::utf8_strict; lazily populated + +sub _check_PU { + local $SIG{__DIE__}; # prevent outer handler from being called + !!eval { + # MUST preload Encode or $SIG{__DIE__} localization fails + # on some Perl 5.8.8 (maybe other 5.8.*) compiled with -O2. + require Encode; + require PerlIO::utf8_strict; + PerlIO::utf8_strict->VERSION(0.003); + 1; + }; +} + +my $HAS_FLOCK = $Config{d_flock} || $Config{d_fcntl_can_lock} || $Config{d_lockf}; + +# notions of "root" directories differ on Win32: \\server\dir\ or C:\ or \ +my $SLASH = qr{[\\/]}; +my $NOTSLASH = qr{[^\\/]}; +my $DRV_VOL = qr{[a-z]:}i; +my $UNC_VOL = qr{$SLASH $SLASH $NOTSLASH+ $SLASH $NOTSLASH+}x; +my $WIN32_ROOT = qr{(?: $UNC_VOL $SLASH | $DRV_VOL $SLASH | $SLASH )}x; + +sub _win32_vol { + my ( $path, $drv ) = @_; + require Cwd; + my $dcwd = eval { Cwd::getdcwd($drv) }; # C: -> C:\some\cwd + # getdcwd on non-existent drive returns empty string + # so just use the original drive Z: -> Z: + $dcwd = "$drv" unless defined $dcwd && length $dcwd; + # normalize dwcd to end with a slash: might be C:\some\cwd or D:\ or Z: + $dcwd =~ s{$SLASH?\z}{/}; + # make the path absolute with dcwd + $path =~ s{^$DRV_VOL}{$dcwd}; + return $path; +} + +# This is a string test for before we have the object; see is_rootdir for well-formed +# object test +sub _is_root { + return IS_WIN32() ? ( $_[0] =~ /^$WIN32_ROOT\z/ ) : ( $_[0] eq '/' ); +} + +BEGIN { + *_same = IS_WIN32() ? sub { lc( $_[0] ) eq lc( $_[1] ) } : sub { $_[0] eq $_[1] }; +} + +# mode bits encoded for chmod in symbolic mode +my %MODEBITS = ( om => 0007, gm => 0070, um => 0700 ); ## no critic +{ my $m = 0; $MODEBITS{$_} = ( 1 << $m++ ) for qw/ox ow or gx gw gr ux uw ur/ }; + +sub _symbolic_chmod { + my ( $mode, $symbolic ) = @_; + for my $clause ( split /,\s*/, $symbolic ) { + if ( $clause =~ m{\A([augo]+)([=+-])([rwx]+)\z} ) { + my ( $who, $action, $perms ) = ( $1, $2, $3 ); + $who =~ s/a/ugo/g; + for my $w ( split //, $who ) { + my $p = 0; + $p |= $MODEBITS{"$w$_"} for split //, $perms; + if ( $action eq '=' ) { + $mode = ( $mode & ~$MODEBITS{"${w}m"} ) | $p; + } + else { + $mode = $action eq "+" ? ( $mode | $p ) : ( $mode & ~$p ); + } + } + } + else { + Carp::croak("Invalid mode clause '$clause' for chmod()"); + } + } + return $mode; +} + +# flock doesn't work on NFS on BSD or on some filesystems like lustre. +# Since program authors often can't control or detect that, we warn once +# instead of being fatal if we can detect it and people who need it strict +# can fatalize the 'flock' category + +#<<< No perltidy +{ package flock; use warnings::register } +#>>> + +my $WARNED_NO_FLOCK = 0; + +sub _throw { + my ( $self, $function, $file, $msg ) = @_; + if ( $function =~ /^flock/ + && $! =~ /operation not supported|function not implemented/i + && !warnings::fatal_enabled('flock') ) + { + if ( !$WARNED_NO_FLOCK ) { + warnings::warn( flock => "Flock not available: '$!': continuing in unsafe mode" ); + $WARNED_NO_FLOCK++; + } + } + else { + $msg = $! unless defined $msg; + Path::Tiny::Error->throw( $function, ( defined $file ? $file : $self->[PATH] ), + $msg ); + } + return; +} + +# cheapo option validation +sub _get_args { + my ( $raw, @valid ) = @_; + if ( defined($raw) && ref($raw) ne 'HASH' ) { + my ( undef, undef, undef, $called_as ) = caller(1); + $called_as =~ s{^.*::}{}; + Carp::croak("Options for $called_as must be a hash reference"); + } + my $cooked = {}; + for my $k (@valid) { + $cooked->{$k} = delete $raw->{$k} if exists $raw->{$k}; + } + if ( keys %$raw ) { + my ( undef, undef, undef, $called_as ) = caller(1); + $called_as =~ s{^.*::}{}; + Carp::croak( "Invalid option(s) for $called_as: " . join( ", ", keys %$raw ) ); + } + return $cooked; +} + +#--------------------------------------------------------------------------# +# Constructors +#--------------------------------------------------------------------------# + +#pod =construct path +#pod +#pod $path = path("foo/bar"); +#pod $path = path("/tmp", "file.txt"); # list +#pod $path = path("."); # cwd +#pod +#pod Constructs a C object. It doesn't matter if you give a file or +#pod directory path. It's still up to you to call directory-like methods only on +#pod directories and file-like methods only on files. This function is exported +#pod automatically by default. +#pod +#pod The first argument must be defined and have non-zero length or an exception +#pod will be thrown. This prevents subtle, dangerous errors with code like +#pod C<< path( maybe_undef() )->remove_tree >>. +#pod +#pod B: If and only if the B character of the B argument +#pod to C is a tilde ('~'), then tilde replacement will be applied to the +#pod first path segment. A single tilde will be replaced with C and a +#pod tilde followed by a username will be replaced with output of +#pod C. B. +#pod See L for more. +#pod +#pod On Windows, if the path consists of a drive identifier without a path component +#pod (C or C), it will be expanded to the absolute path of the current +#pod directory on that volume using C. +#pod +#pod If called with a single C argument, the original is returned unless +#pod the original is holding a temporary file or directory reference in which case a +#pod stringified copy is made. +#pod +#pod $path = path("foo/bar"); +#pod $temp = Path::Tiny->tempfile; +#pod +#pod $p2 = path($path); # like $p2 = $path +#pod $t2 = path($temp); # like $t2 = path( "$temp" ) +#pod +#pod This optimizes copies without proliferating references unexpectedly if a copy is +#pod made by code outside your control. +#pod +#pod Current API available since 0.017. +#pod +#pod =cut + +sub path { + my $path = shift; + Carp::croak("Path::Tiny paths require defined, positive-length parts") + unless 1 + @_ == grep { defined && length } $path, @_; + + # non-temp Path::Tiny objects are effectively immutable and can be reused + if ( !@_ && ref($path) eq __PACKAGE__ && !$path->[TEMP] ) { + return $path; + } + + # stringify objects + $path = "$path"; + + # do any tilde expansions + my ($tilde) = $path =~ m{^(~[^/]*)}; + if ( defined $tilde ) { + # Escape File::Glob metacharacters + (my $escaped = $tilde) =~ s/([\[\{\*\?\\])/\\$1/g; + require File::Glob; + my ($homedir) = File::Glob::bsd_glob($escaped); + if (defined $homedir && ! $File::Glob::ERROR) { + $homedir =~ tr[\\][/] if IS_WIN32(); + $path =~ s{^\Q$tilde\E}{$homedir}; + } + } + + unshift @_, $path; + goto &_pathify; +} + +# _path is like path but without tilde expansion +sub _path { + my $path = shift; + Carp::croak("Path::Tiny paths require defined, positive-length parts") + unless 1 + @_ == grep { defined && length } $path, @_; + + # non-temp Path::Tiny objects are effectively immutable and can be reused + if ( !@_ && ref($path) eq __PACKAGE__ && !$path->[TEMP] ) { + return $path; + } + + # stringify objects + $path = "$path"; + + unshift @_, $path; + goto &_pathify; +} + +# _pathify expects one or more string arguments, then joins and canonicalizes +# them into an object. +sub _pathify { + my $path = shift; + + # expand relative volume paths on windows; put trailing slash on UNC root + if ( IS_WIN32() ) { + $path = _win32_vol( $path, $1 ) if $path =~ m{^($DRV_VOL)(?:$NOTSLASH|\z)}; + $path .= "/" if $path =~ m{^$UNC_VOL\z}; + } + + # concatenations stringifies objects, too + if (@_) { + $path .= ( _is_root($path) ? "" : "/" ) . join( "/", @_ ); + } + + + # canonicalize, but with unix slashes and put back trailing volume slash + my $cpath = $path = File::Spec->canonpath($path); + $path =~ tr[\\][/] if IS_WIN32(); + $path = "/" if $path eq '/..'; # for old File::Spec + $path .= "/" if IS_WIN32() && $path =~ m{^$UNC_VOL\z}; + + # root paths must always have a trailing slash, but other paths must not + if ( _is_root($path) ) { + $path =~ s{/?\z}{/}; + } + else { + $path =~ s{/\z}{}; + } + + bless [ $path, $cpath ], __PACKAGE__; +} + +#pod =construct new +#pod +#pod $path = Path::Tiny->new("foo/bar"); +#pod +#pod This is just like C, but with method call overhead. (Why would you +#pod do that?) +#pod +#pod Current API available since 0.001. +#pod +#pod =cut + +sub new { shift; path(@_) } + +#pod =construct cwd +#pod +#pod $path = Path::Tiny->cwd; # path( Cwd::getcwd ) +#pod $path = cwd; # optional export +#pod +#pod Gives you the absolute path to the current directory as a C object. +#pod This is slightly faster than C<< path(".")->absolute >>. +#pod +#pod C may be exported on request and used as a function instead of as a +#pod method. +#pod +#pod Current API available since 0.018. +#pod +#pod =cut + +sub cwd { + require Cwd; + return _path( Cwd::getcwd() ); +} + +#pod =construct rootdir +#pod +#pod $path = Path::Tiny->rootdir; # / +#pod $path = rootdir; # optional export +#pod +#pod Gives you C<< File::Spec->rootdir >> as a C object if you're too +#pod picky for C. +#pod +#pod C may be exported on request and used as a function instead of as a +#pod method. +#pod +#pod Current API available since 0.018. +#pod +#pod =cut + +sub rootdir { _path( File::Spec->rootdir ) } + +#pod =construct tempfile, tempdir +#pod +#pod $temp = Path::Tiny->tempfile( @options ); +#pod $temp = Path::Tiny->tempdir( @options ); +#pod $temp = $dirpath->tempfile( @options ); +#pod $temp = $dirpath->tempdir( @options ); +#pod $temp = tempfile( @options ); # optional export +#pod $temp = tempdir( @options ); # optional export +#pod +#pod C passes the options to C<< File::Temp->new >> and returns a +#pod C object with the file name. The C option will be enabled +#pod by default, but you can override that by passing C<< TMPDIR => 0 >> along with +#pod the options. (If you use an absolute C