pax_global_header00006660000000000000000000000064147306047640014525gustar00rootroot0000000000000052 comment=74fb685fb775dd3a22994b46e1163c3d951696f5 shards-0.19.0/000077500000000000000000000000001473060476400131005ustar00rootroot00000000000000shards-0.19.0/.circleci/000077500000000000000000000000001473060476400147335ustar00rootroot00000000000000shards-0.19.0/.circleci/config.yml000066400000000000000000000043301473060476400167230ustar00rootroot00000000000000version: 2.1 orbs: crystal: manastech/crystal@1.0.0 commands: shards-make-test: steps: - run: name: git config command: | git config --global user.email "you@example.com" git config --global user.name "Your Name" git config --global column.ui always - crystal/version - checkout - run: shards install --ignore-crystal-version - run: make - run: make test - run: crystal tool format --check src spec with-brew-cache: parameters: steps: type: steps steps: - restore_cache: keys: - brew-cache-v1-{{ .Branch }} - brew-cache-v1- - steps: <> - save_cache: key: brew-cache-v1-{{ .Branch }}-{{ epoch }} paths: - /usr/local/Homebrew - ~/Library/Caches/Homebrew jobs: build-manpages: docker: - image: asciidoctor/docker-asciidoctor steps: - checkout - run: name: Build manpages command: make manpages - store_artifacts: path: man test: docker: - image: crystallang/crystal:latest environment: USER: shardsuser steps: - run: name: Install mercurial and fossil command: apt-get update && apt-get install mercurial fossil -y - shards-make-test test-on-osx: macos: xcode: 15.0.0 steps: - with-brew-cache: steps: - run: name: Install Crystal, Mercurial, and Fossil command: brew install crystal mercurial fossil - shards-make-test test-on-nightly: docker: - image: crystallang/crystal:nightly environment: USER: shardsuser steps: - run: name: Install mercurial and fossil command: apt-get update && apt-get install mercurial fossil -y - shards-make-test workflows: version: 2 ci: jobs: - build-manpages - test - test-on-osx - test-on-nightly nightly: triggers: - schedule: cron: '0 2 * * *' filters: branches: only: - master jobs: - test-on-nightly shards-0.19.0/.envrc000066400000000000000000000002601473060476400142140ustar00rootroot00000000000000source_url "https://raw.githubusercontent.com/cachix/devenv/95f329d49a8a5289d31e0982652f7058a189bfca/direnvrc" "sha256-d+8cBpDfDBj41inrADaJt+bDWhOktwslgoP5YiGJ1v0=" use devenvshards-0.19.0/.gitattributes000066400000000000000000000004771473060476400160030ustar00rootroot00000000000000## Sources *.cr text eol=lf ## Generated files # produced by `make manpages` man/shards.1 linguist-generated man/shard.yml.5 linguist-generated # produced by `make htmlpages` docs/shards.html linguist-generated docs/shard.yml.html linguist-generated ## Syntax highlighting Makefile.win linguist-language=makefile shards-0.19.0/.github/000077500000000000000000000000001473060476400144405ustar00rootroot00000000000000shards-0.19.0/.github/workflows/000077500000000000000000000000001473060476400164755ustar00rootroot00000000000000shards-0.19.0/.github/workflows/ci.yml000066400000000000000000000031221473060476400176110ustar00rootroot00000000000000name: CI on: push: pull_request: schedule: - cron: "0 6 * * 1" # Every monday 6 AM jobs: test: strategy: fail-fast: false matrix: os: [ubuntu-24.04, macos-13, macos-14] crystal: [latest, nightly] include: - os: windows-2022 crystal: nightly - os: ubuntu-22.04 crystal: 1.0.0 runs-on: ${{ matrix.os }} steps: - name: Configure Git run: | git config --global user.email "you@example.com" git config --global user.name "Your Name" git config --global column.ui always git config --global core.autocrlf false - name: Install Crystal uses: oprypin/install-crystal@v1 with: crystal: ${{ matrix.crystal }} - name: Install Fossil and Mercurial if: ${{ runner.os == 'Linux' }} run: | sudo apt-get update sudo apt-get install fossil mercurial - name: Install Fossil and Mercurial if: ${{ runner.os == 'macOS' }} run: | brew update brew install fossil mercurial - name: Install Fossil and Mercurial if: ${{ runner.os == 'Windows' }} run: choco install fossil hg - name: Download source uses: actions/checkout@v4 - name: Build run: make -f ${{ runner.os == 'Windows' && 'Makefile.win' || 'Makefile' }} - name: Run specs run: make -f ${{ runner.os == 'Windows' && 'Makefile.win' || 'Makefile' }} test - name: Check formatting run: crystal tool format --check src spec shards-0.19.0/.github/workflows/docs.yml000066400000000000000000000013611473060476400201510ustar00rootroot00000000000000name: Docs on: push: paths: - docs/*.adoc - .github/workflows/docs.yml - VERSION - docs.mk pull_request: paths: - docs/*.adoc - .github/workflows/docs.yml - VERSION - docs.mk permissions: {} jobs: docs: runs-on: ubuntu-24.04 steps: - name: Download source uses: actions/checkout@v4 - name: Build manpages uses: Analog-inc/asciidoctor-action@v1.3.2 with: shellcommand: "make -B manpages SOURCE_DATE_EPOCH=\"$(date +%s --utc --date \"$(grep -m1 -o -E 'Date: .*' man/shard.yml.5 | cut -d' ' -f2)\")\"" - name: Ensure no changes run: git diff --exit-code || echo '`make manpages` produced changes. Please rebuild the docs.' shards-0.19.0/.gitignore000066400000000000000000000003651473060476400150740ustar00rootroot00000000000000/.crystal /.shards /lib /bin/shards /bin/shards.dwarf /bin/shards.exe /bin/shards.pdb /spec/.repositories /spec/.shards /spec/unit/.lib /tmp /docs/*.html # Devenv .devenv* devenv.local.nix # direnv .direnv # pre-commit .pre-commit-config.yaml shards-0.19.0/CHANGELOG.md000066400000000000000000000757731473060476400147340ustar00rootroot00000000000000# Changelog ## [0.19.0] (2024-12-18) [0.19.0]: https://github.com/crystal-lang/shards/releases/0.19.0 ### Features - Forward unmodified ARGV to subcommand ([#631], thanks @luislavena) - Add support for Codeberg as a git resolver ([#656], thanks @miry) [#631]: https://github.com/crystal-lang/shards/pull/631 [#656]: https://github.com/crystal-lang/shards/pull/656 ### Bugfixes - Fix `GitResolver#valid_repository?` ([#646], thanks @straight-shoota) [#646]: https://github.com/crystal-lang/shards/pull/646 ### Chores - `crystal tool format` with Crystal 1.15.0-dev ([#647], thanks @straight-shoota) - Replace deprecated `::sleep(Number)` ([#652], thanks @straight-shoota) [#647]: https://github.com/crystal-lang/shards/pull/647 [#652]: https://github.com/crystal-lang/shards/pull/652 ### Refactor - Run `git config` instead of reading `config` file manually ([#639], thanks @straight-shoota) [#639]: https://github.com/crystal-lang/shards/pull/639 ### Documentation - Use SPDX license identifiers for `license` in `shard.yml` ([#641], thanks @leoheitmannruiz) [#641]: https://github.com/crystal-lang/shards/pull/641 ### Infrastructure - `devenv update` ([#661], thanks @straight-shoota) - Release 0.19.0 ([#660], thanks @straight-shoota) - Remove `Vagrantfile` ([#630], thanks @straight-shoota) - Add devenv configuration ([#629], thanks @straight-shoota) - Update GH Actions ([#621], thanks @renovate) - Update Analog-inc/asciidoctor-action action to v1.3.2 ([#636], thanks @renovate) - Update dependency ubuntu to v24 ([#643], thanks @renovate) - Install mercurial via OS package manager ([#645], thanks @straight-shoota) - Support `.exe` file extension in `Makefile` on MSYS2 ([#651], thanks @HertzDevil) - Update CI runners ([#654], thanks @straight-shoota) - `devenv update` ([#653], thanks @straight-shoota) - Add linuguist-vendored annotation for generated files ([#658], thanks @straight-shoota) - [CI] Run docs check in separate workflow with path restriction ([#657], thanks @straight-shoota) - Add description and metdatada to `shard.yml` ([#662], thanks @straight-shoota) [#661]: https://github.com/crystal-lang/shards/pull/661 [#660]: https://github.com/crystal-lang/shards/pull/660 [#630]: https://github.com/crystal-lang/shards/pull/630 [#629]: https://github.com/crystal-lang/shards/pull/629 [#621]: https://github.com/crystal-lang/shards/pull/621 [#636]: https://github.com/crystal-lang/shards/pull/636 [#643]: https://github.com/crystal-lang/shards/pull/643 [#645]: https://github.com/crystal-lang/shards/pull/645 [#651]: https://github.com/crystal-lang/shards/pull/651 [#654]: https://github.com/crystal-lang/shards/pull/654 [#653]: https://github.com/crystal-lang/shards/pull/653 [#658]: https://github.com/crystal-lang/shards/pull/658 [#657]: https://github.com/crystal-lang/shards/pull/657 [#662]: https://github.com/crystal-lang/shards/pull/662 ## [0.18.0] (2024-03-28) [0.18.0]: https://github.com/crystal-lang/shards/releases/0.18.0 ### Features - Support more cache directories on Windows ([#612], thanks @HertzDevil) - Detect symlink creation capability on Windows ([#617], thanks @HertzDevil) - Use `Colorize.on_tty_only!` ([#620], thanks @HertzDevil) ### Documentation - Fix typos ([#607], thanks @kojix2) ### Specs - Use `FileUtils.rm_rf` instead of shell command in spec ([#616], thanks @HertzDevil) ### Infrastructure - Adjust changelog format to follow that of crystal ([#606], thanks @straight-shoota) - Add Windows binary paths to `.gitignore` ([#613], thanks @HertzDevil) - Add `.gitattributes` ([#614], thanks @HertzDevil) - Add CI job to test against Crystal 1.0 ([#618], thanks @HertzDevil) - Configure Renovate ([#564], thanks @renovate) - Refactor Makefile phony declaration ([#610], thanks @straight-shoota) - Add `make help` recipe ([#609], thanks @straight-shoota) - Add `Makefile.win` ([#615], thanks @HertzDevil) - Add JSON schema for `shard.yml` ([#623], thanks @nobodywasishere) - Fix Makefile incorrect peer target usage ([#608], thanks @straight-shoota) [#564]: https://github.com/crystal-lang/shards/pull/564 [#606]: https://github.com/crystal-lang/shards/pull/606 [#607]: https://github.com/crystal-lang/shards/pull/607 [#608]: https://github.com/crystal-lang/shards/pull/608 [#609]: https://github.com/crystal-lang/shards/pull/609 [#610]: https://github.com/crystal-lang/shards/pull/610 [#612]: https://github.com/crystal-lang/shards/pull/612 [#613]: https://github.com/crystal-lang/shards/pull/613 [#614]: https://github.com/crystal-lang/shards/pull/614 [#615]: https://github.com/crystal-lang/shards/pull/615 [#616]: https://github.com/crystal-lang/shards/pull/616 [#617]: https://github.com/crystal-lang/shards/pull/617 [#618]: https://github.com/crystal-lang/shards/pull/618 [#620]: https://github.com/crystal-lang/shards/pull/620 [#623]: https://github.com/crystal-lang/shards/pull/623 ## [0.17.4] (2023-12-22) [0.17.4]: https://github.com/crystal-lang/shards/releases/0.17.4 ### Bugfixes - Do not try to override existing lib path in dependency ([#599](https://github.com/crystal-lang/shards/pull/599), thanks @straight-shoota) - Fix install non-`.exe` executables on Windows ([#593](https://github.com/crystal-lang/shards/pull/593), thanks @straight-shoota) ### Specs - Add tags to resolver specs ([#589](https://github.com/crystal-lang/shards/pull/589), thanks @straight-shoota) ### Documentation - Clarify documentation of `--local` flag ([#587](https://github.com/crystal-lang/shards/pull/587), thanks @straight-shoota) ### Infrastructure - *(ci)* Ensure manpages are generated with no diff ([#594](https://github.com/crystal-lang/shards/pull/594), thanks @straight-shoota) - *(ci)* Upgrade xcode version on circleci ([#603](https://github.com/crystal-lang/shards/pull/603), thanks @straight-shoota) - *(ci)* Pin GHA runner versions ([#604](https://github.com/crystal-lang/shards/pull/604), thanks @straight-shoota) - *(ci)* Pin `Analog-inc/asciidoctor-action` version ([#602](https://github.com/crystal-lang/shards/pull/602), thanks @straight-shoota) ## [0.17.3] (2023-04-07) [0.17.3]: https://github.com/crystal-lang/shards/releases/0.17.3 - Fix swallowing original error message in `git_retry` ([#573](https://github.com/crystal-lang/shards/pull/573), thanks @straight-shoota) - `crystal tool format` with 1.8-dev ([#575](https://github.com/crystal-lang/shards/pull/575), thanks @straight-shoota) - Docs: Tilde version operator improvements ([#571](https://github.com/crystal-lang/shards/pull/571), thanks @Blacksmoke16) - Fix avoid swallowing error message if git command failed ([#569](https://github.com/crystal-lang/shards/pull/569), thanks @straight-shoota) ## [0.17.2] (2022-12-28) [0.17.2]: https://github.com/crystal-lang/shards/releases/0.17.2 - Improve error message when symlink failed on Windows ([#565](https://github.com/crystal-lang/shards/pull/565), thanks @straight-shoota) - Inherit the standard input descriptor ([#561](https://github.com/crystal-lang/shards/pull/561), thanks @hovsater) ## [0.17.1] (2022-09-30) [0.17.1]: https://github.com/crystal-lang/shards/releases/0.17.1 - Fix: Don't raise an exception if install_path doesn't exist ([#557](https://github.com/crystal-lang/shards/pull/557), thanks @mjoerussell) - Fix Fossil resolver when multiple dependencies are coming from the same website ([#558](https://github.com/crystal-lang/shards/pull/558), thanks @MistressRemilia) - Adjust parameter name for `Resolver#install_sources` ([#559](https://github.com/crystal-lang/shards/pull/559), thanks @straight-shoota) ## [0.17.0] (2022-03-24) [0.17.0]: https://github.com/crystal-lang/shards/releases/0.17.0 - Add `make build` recipe ([#533](https://github.com/crystal-lang/shards/pull/533), thanks @straight-shoota) - Fix unexpected token compiler error match ([#532](https://github.com/crystal-lang/shards/pull/532), thanks @straight-shoota) - Honour `CRYSTAL` env var ([#534](https://github.com/crystal-lang/shards/pull/534), thanks @straight-shoota) - No longer depend of external git user config ([#536](https://github.com/crystal-lang/shards/pull/536), thanks @luislavena) - [CI] Update circleci xcode 13.2.1 ([#537](https://github.com/crystal-lang/shards/pull/537), thanks @straight-shoota) - Output `STDERR` from the building process ([#540](https://github.com/crystal-lang/shards/pull/540), thanks @beta-ziliani) - Fix grammar problems ([#543](https://github.com/crystal-lang/shards/pull/543), thanks @dinko-pehar) - Add fossil resolver ([#530](https://github.com/crystal-lang/shards/pull/530), thanks @MistressRemilia) - Add expanded local path to `shard.yml` error message in `PathResolver` ([#541](https://github.com/crystal-lang/shards/pull/541), thanks @straight-shoota) - Avoid user defined git template in resolver ([#528](https://github.com/crystal-lang/shards/pull/528), thanks @lzap) - Add run command ([#546](https://github.com/crystal-lang/shards/pull/546), thanks @luislavena) - Re-enabled `~` support in path resolver ([#538](https://github.com/crystal-lang/shards/pull/538), thanks @masukomi) - Add `--jobs` flag (parallel git fetch) ([#539](https://github.com/crystal-lang/shards/pull/539), thanks @m-o-e) ## [0.16.0] (2021-10-06) [0.16.0]: https://github.com/crystal-lang/shards/releases/0.16.0 ### Fixes - Fix error message for invalid shard.yml ([#516](https://github.com/crystal-lang/shards/pull/516), thanks @straight-shoota) - [Makefile] Fix shard.lock recipe ([#515](https://github.com/crystal-lang/shards/pull/515), thanks @straight-shoota) - Fix pass no-color and verbose flags to crystal build ([#517](https://github.com/crystal-lang/shards/pull/517), thanks @straight-shoota) ### Features - Resolver for Mercurial repositories ([#458](https://github.com/crystal-lang/shards/pull/458), thanks @f-fr) - Update manpages with mercurial information ([#526](https://github.com/crystal-lang/shards/pull/526), thanks @straight-shoota) - Add `!=` operator for version resolve ([#520](https://github.com/crystal-lang/shards/pull/520), thanks @syeopite) - Compress manpages on install ([#524](https://github.com/crystal-lang/shards/pull/524), thanks @straight-shoota) ## [0.15.0] (2021-06-29) [0.15.0]: https://github.com/crystal-lang/shards/releases/0.15.0 ### Fixes - Let `shards build` error if no targets defined ([#490](https://github.com/crystal-lang/shards/pull/490), thanks @straight-shoota) - Fix to allow empty `shard.override.yml` ([#495](https://github.com/crystal-lang/shards/pull/495), thanks @straight-shoota) - Stop expecting master to be the default branch for git ([#503](https://github.com/crystal-lang/shards/pull/503), thanks @szabgab) ### Features - Add documentation for `shard.override.yml` ([#494](https://github.com/crystal-lang/shards/pull/494), thanks @straight-shoota) - Warn only crystal version ([#496](https://github.com/crystal-lang/shards/pull/496), thanks @beta-ziliani, @bcardiff) - Don't default the Crystal version to `<1.0.0`, use only the lower bound ([#493](https://github.com/crystal-lang/shards/pull/493), thanks @oprypin) - Add `--skip-executables` ([#506](https://github.com/crystal-lang/shards/pull/506), thanks @straight-shoota) ### Others - Escape automatic ligatures in AsciiDoc ([#489](https://github.com/crystal-lang/shards/pull/489), thanks @elebow) - Fix links in README ([#500](https://github.com/crystal-lang/shards/pull/500), [#483](https://github.com/crystal-lang/shards/pull/483), thanks @szabgab, @kimburgess) - Correct list identation in shard.yml.adoc ([#492](https://github.com/crystal-lang/shards/pull/492/files), thanks @elebow) - Add getting started section to README ([#513](https://github.com/crystal-lang/shards/pull/513), thanks @straight-shoota) ## [0.14.1] (2021-03-10) [0.14.1]: https://github.com/crystal-lang/shards/releases/0.14.1 ### Fixes - Fix broken `SOURCE_DATE_EPOCH` in `docs.mk`. ([#479](https://github.com/crystal-lang/shards/pull/479), thanks @straight-shoota) ## [0.14.0] (2021-02-23) [0.14.0]: https://github.com/crystal-lang/shards/releases/0.14.0 ### Fixes - Improve error message when locked version is missing in source. ([#466](https://github.com/crystal-lang/shards/pull/466), thanks @straight-shoota) - Fix touch install_path to not accidentally create file. ([#478](https://github.com/crystal-lang/shards/pull/478), thanks @straight-shoota) ### Features - Add `--frozen` and `--without-development` CLI flags. ([#473](https://github.com/crystal-lang/shards/pull/473), thanks @straight-shoota) - Add `--skip-postinstall` cli option to install and update. ([#475](https://github.com/crystal-lang/shards/pull/475), thanks @bcardiff) - Treat github sources as case insensitive. ([#471](https://github.com/crystal-lang/shards/pull/471), thanks @stakach) ### Others - Rewrite manpages in Asciidoc. ([#262](https://github.com/crystal-lang/shards/pull/262), thanks @straight-shoota) - CI improvements and housekeeping. ([#454](https://github.com/crystal-lang/shards/pull/454), [#464](https://github.com/crystal-lang/shards/pull/464), thanks @j8r, @Sija) - Bump crystal-molinillo to 0.2.0. ([#476](https://github.com/crystal-lang/shards/pull/476), thanks @bcardiff) ## [0.13.0] (2021-01-21) [0.13.0]: https://github.com/crystal-lang/shards/releases/0.13.0 ### Fixes - Fix outdated command for dependencies with no releases. ([#455](https://github.com/crystal-lang/shards/pull/455), thanks @straight-shoota) - Fix outdated command with non-release installed. ([#456](https://github.com/crystal-lang/shards/pull/456), thanks @straight-shoota) - Write lockfile even when there are no dependencies. ([#453](https://github.com/crystal-lang/shards/pull/453), thanks @straight-shoota) - Touch install_path and lockfile to express dependency. ([#444](https://github.com/crystal-lang/shards/pull/444), thanks @straight-shoota) - Improve git reliability by retrying on failures. ([#450](https://github.com/crystal-lang/shards/pull/450), thanks @fudanchii) - Allow empty scalar for mappings/sequences. ([#451](https://github.com/crystal-lang/shards/pull/451), thanks @straight-shoota) - Fix working directory in `capture`. ([#457](https://github.com/crystal-lang/shards/pull/457), thanks @f-fr) ### Features - Add a fallback to alternate shards commands. ([#202](https://github.com/crystal-lang/shards/pull/202), thanks @Willamin) ### Others - Use git's `checkout` feature directly to write out repo files. ([#435](https://github.com/crystal-lang/shards/pull/435), thanks @oprypin) - Use `Process.quote` instead of the old platform-specific helper. ([#437](https://github.com/crystal-lang/shards/pull/437), thanks @oprypin) - Don't use POSIX-specific shell constructs. ([#436](https://github.com/crystal-lang/shards/pull/436), thanks @oprypin) - Don't use compile-time shell commands to determine build timestamp. ([#438](https://github.com/crystal-lang/shards/pull/438), thanks @oprypin) - Expand Windows support + fix all specs. ([#447](https://github.com/crystal-lang/shards/pull/447), thanks @oprypin) - Add continuous testing (including Windows) using GitHub Actions. ([#448](https://github.com/crystal-lang/shards/pull/448), thanks @oprypin) - Cleanup unused code. ([#460](https://github.com/crystal-lang/shards/pull/460), thanks @f-fr) - Fix outdated content in the `README.md` and `SPEC.md`. ([#434](https://github.com/crystal-lang/shards/pull/434), [#461](https://github.com/crystal-lang/shards/pull/461), [#462](https://github.com/crystal-lang/shards/pull/462), thanks @kojix2, @straight-shoota, @KimBurgess) ## [0.12.0] (2020-08-05) [0.12.0]: https://github.com/crystal-lang/shards/releases/0.12.0 ### Fixes - Disable interactive credential prompt for git resolver. ([#411](https://github.com/crystal-lang/shards/pull/411), thanks @straight-shoota) - Display dependency name on parsing errors of `shard.yml`. ([#408](https://github.com/crystal-lang/shards/pull/408), thanks @straight-shoota) - Handle ambiguous dependencies and update `shard.lock` if source of dependency change. ([#419](https://github.com/crystal-lang/shards/pull/419), [#429](https://github.com/crystal-lang/shards/pull/429), thanks @bcardiff) - Reinstall when resolver changes. ([#425](https://github.com/crystal-lang/shards/pull/425), thanks @waj) ### Features - Shards overrides. ([#422](https://github.com/crystal-lang/shards/pull/422), [#429](https://github.com/crystal-lang/shards/pull/429), thanks @bcardiff) - Add `--ignore-crystal-version` related suggestion and warnings to guide user. ([#418](https://github.com/crystal-lang/shards/pull/418), thanks @bcardiff) - Allow shards to read `SHARDS_OPTS` for addition command options. ([#417](https://github.com/crystal-lang/shards/pull/417), [#420](https://github.com/crystal-lang/shards/pull/420), thanks @bcardiff) - Add convenient makefile arguments for packaging. ([#414](https://github.com/crystal-lang/shards/pull/414), thanks @bcardiff) ### Others - Bump required Crystal to 0.35. ([#424](https://github.com/crystal-lang/shards/pull/424), thanks @bcardiff) - Refactor: Move install responsibilities from `Resolver` to `Package`. ([#426](https://github.com/crystal-lang/shards/pull/426), thanks @waj) - Refactor: Use `Package` for locks and installed shards. ([#428](https://github.com/crystal-lang/shards/pull/428), thanks @waj) - Spec: Add `stdout` and `stderr` to `FailedCommand` message. ([#410](https://github.com/crystal-lang/shards/pull/410), thanks @straight-shoota) - Spec: Fix failure under 32-bit Linux. ([#416](https://github.com/crystal-lang/shards/pull/416), thanks @lugia-kun) - Fix builds. ([#421](https://github.com/crystal-lang/shards/pull/421), [#423](https://github.com/crystal-lang/shards/pull/423), thanks @bcardiff) ## [0.11.1] (2020-06-08) [0.11.1]: https://github.com/crystal-lang/shards/releases/0.11.1 ### Fixes - Support `crystal: x.y` values (without patch). ([#404](https://github.com/crystal-lang/shards/pull/404), thanks @bcardiff) ## [0.11.0] (2020-06-05) [0.11.0]: https://github.com/crystal-lang/shards/releases/0.11.0 ### Features - **(breaking-change)** Use `crystal:` property to filter candidates version. ([#395](https://github.com/crystal-lang/shards/pull/395), thanks @waj, @bcardiff) - Introduce `shard.lock` 2.0 format, run `shards install` to migrate. ([#349](https://github.com/crystal-lang/shards/pull/349), [#400](https://github.com/crystal-lang/shards/pull/400), thanks @waj) - Support intersection in requirements `version: >= 1.0.0, < 2.0`. ([#394](https://github.com/crystal-lang/shards/pull/394), thanks @waj) - Install dependencies in reverse topological order. ([#369](https://github.com/crystal-lang/shards/pull/369), thanks @waj) - Use less bright colors for output. ([#373](https://github.com/crystal-lang/shards/pull/373), thanks @waj) - Add error on duplicate arguments in `shard.yml`. ([#387](https://github.com/crystal-lang/shards/pull/387), thanks @straight-shoota) - Replace `.sha1` files with a single `.shards.info`. ([#349](https://github.com/crystal-lang/shards/pull/349), [#366](https://github.com/crystal-lang/shards/pull/366), [#368](https://github.com/crystal-lang/shards/pull/368), [#401](https://github.com/crystal-lang/shards/pull/401), thanks @waj) ### Fixes - Improve `GitRef` dependencies and locks. ([#388](https://github.com/crystal-lang/shards/pull/388), [#389](https://github.com/crystal-lang/shards/pull/389), thanks @waj, @straight-shoota) - Fix crash when a shard version didn't contain a `shard.yml`. ([#362](https://github.com/crystal-lang/shards/pull/362), thanks @waj) - Avoid `shard.lock` being overwritten when dependencies are up to date. ([#370](https://github.com/crystal-lang/shards/pull/370), thanks @waj) - Detect version mismatches between `shard.yml` and git tags . ([#341](https://github.com/crystal-lang/shards/pull/341), thanks @RX14) ### Others - Add compatibility with Crystal 0.35. Drop compatibility with < 0.34. ([#379](https://github.com/crystal-lang/shards/pull/379), [#391](https://github.com/crystal-lang/shards/pull/391), [#397](https://github.com/crystal-lang/shards/pull/397), thanks @waj, @bcardiff) - Explicitly state build_options in help output. ([#364](https://github.com/crystal-lang/shards/pull/364), thanks @Darwinnn) - Use YAML parser for `Dependency` and `Target`. ([#306](https://github.com/crystal-lang/shards/pull/306), thanks @straight-shoota) - Add lib to Makefile. ([#344](https://github.com/crystal-lang/shards/pull/344), [#380](https://github.com/crystal-lang/shards/pull/380), thanks @straight-shoota, @waj) - Allow Makefile envvars to be overwritten from a command line. ([#378](https://github.com/crystal-lang/shards/pull/378), thanks @anatol) - Rework of dependency and requirements. ([#354](https://github.com/crystal-lang/shards/pull/354), [#358](https://github.com/crystal-lang/shards/pull/358), thanks @waj) - Add spec to check when there is a version mismatch. ([#361](https://github.com/crystal-lang/shards/pull/361), thanks @waj) - Make sure tags in specs aren't signed. ([#382](https://github.com/crystal-lang/shards/pull/382), thanks @repomaa) - Code clean-up. ([#356](https://github.com/crystal-lang/shards/pull/356), [#375](https://github.com/crystal-lang/shards/pull/375), thanks @straight-shoota) ## [0.10.0] (2020-04-01) [0.10.0]: https://github.com/crystal-lang/shards/releases/0.10.0 ### Features - Use [crystal-molinillo](https://github.com/crystal-lang/crystal-molinillo) to resolve dependencies, drop the SAT solver. [#322](https://github.com/crystal-lang/shards/pull/322), [#329](https://github.com/crystal-lang/shards/pull/329), [#336](https://github.com/crystal-lang/shards/pull/336). - Automatic unlock on install and update. [#337](https://github.com/crystal-lang/shards/pull/337) - Show the shard's name when running scripts. [#326](https://github.com/crystal-lang/shards/pull/326) - Support shard renames. [#327](https://github.com/crystal-lang/shards/pull/327) - Add SPEC for repository, homepage, documentation properties. [#265](https://github.com/crystal-lang/shards/pull/265) ### Fixes - Allow changes in the source protocol without triggering an actual change in the source. [#315](https://github.com/crystal-lang/shards/pull/315) - Make shards reproducible via `SOURCE_DATE_EPOCH` environment variable. [#314](https://github.com/crystal-lang/shards/pull/314) - Check non hidden files are not pruned. [#330](https://github.com/crystal-lang/shards/pull/330) - Validation of changes in production mode for dependencies referenced by commit. [#340](https://github.com/crystal-lang/shards/pull/340) ### Others - Upgrade to Crystal 0.34.0. [#296](https://github.com/crystal-lang/shards/pull/296), [#331](https://github.com/crystal-lang/shards/pull/331), [#335](https://github.com/crystal-lang/shards/pull/335) - Replace [minitest](https://github.com/ysbaddaden/minitest.cr) in favor of std-lib spec. [#334](https://github.com/crystal-lang/shards/pull/334) - CI improvements and housekeeping. [#333](https://github.com/crystal-lang/shards/pull/333), [#317](https://github.com/crystal-lang/shards/pull/317), [#323](https://github.com/crystal-lang/shards/pull/323), [#328](https://github.com/crystal-lang/shards/pull/328) ## [0.9.0] (2019-06-13) [0.9.0]: https://github.com/crystal-lang/shards/releases/0.9.0 ### Fixes - Allow resolving pre-release when installing git refs; - Report all available versions (Git resolver); - Don't prune everything in `lib` directory. ## [0.9.0.rc2] (2019-05-07) [0.9.0.rc2]: https://github.com/crystal-lang/shards/releases/0.9.0.rc2 ### Fixes - Exit with non-zero status on dependency resolve error; - Install dependency at HEAD when no version tags are defined; - Install executables using `shard.yml` at commit (not version). ## [0.9.0.rc1] (2019-01-11) [0.9.0.rc1]: https://github.com/crystal-lang/shards/releases/0.9.0.rc1 Breaking changes: - Dependency solver was overhauled; - Git tag refs that match a version number are now an actual version (i.e. `tag: v1.0.0` is converted to `version: 1.0.0`). ### Features - Update specified shards only, trying to keep other shards to their locked version if possible; - Add `--local` argument to use the cache as-is, allowing to skip git fetches when you know the cache is up-to-date; - Add the *outdated* command to list dependencies that could be updated (matching constraints) as well as their latest version; including pre-release versions on demand. - Add the *lock* command that behaves like the *install* and *update* commands but that only creates a lockfile, and doesn't install anything. ### Fixes - Transitive dependencies are now available to all installed shards, allowing postinstall scripts to compile any Crystal application; - Don't consider metadata when considering a pre-release version number. ## [0.9.0.beta] (2019-01-11) [0.9.0.beta]: https://github.com/crystal-lang/shards/releases/0.9.0.beta Breaking changes: - A `shard.yml` spec is now required in libraries. - Drop support for obsolete Projectfile. ### Features - Experimental support for prereleases. Add a letter to a version number to declare a pre-release. For example `1.2.3.alpha` or `1.0.0-rc1`. - Ignore semver metadata (+abc). ### Fixes - Approximate operator used to match invalid version numbers (e.g. `~> 0.1.0` wrongly matched `0.10.0`). - Unbalanced version numbers, such as `1.0.0` and `1.0.0.1` are now correctly ordered and compared as `1.0.0.1 > 1.0.0`. - Force the 'v' prefix in version tags. - `install -t` isn't supported on macOS. ## [0.8.1] (2018-06-17) [0.8.1]: https://github.com/crystal-lang/shards/releases/0.8.1 ### Fixes - Git repositories cloned with v0.8.0 can't fetch new remote refs anymore, which totally broke the `update` command. - The Path resolver incorrectly handled invalid symlinks. ## [0.8.0] (2018-06-05 [REVOKED]) [0.8.0]: https://github.com/crystal-lang/shards/releases/0.8.0 ### Features - Install shard executables inside project bin folder on shard install. See #126. ### Changes - Global cache for cloned Git repositories, aside crystal cache (e.g. `~/.cache/shards`). Customizable with `SHARDS_CACHE_PATH`. - Clone bare Git repositories instead of creating mirrors (fetch should be faster, and less space required on disk). - Man pages are now in the `man` folder. - Allow loose shard versioning, accepting semver-like versions and alternatives such as calver. ### Fixes - Compatibility with Crystal 0.25. ## [0.7.2] (2017-11-16) [0.7.2]: https://github.com/crystal-lang/shards/releases/0.7.2 ### Features - Version command to print-out the project's version, see #147 ### Fixes - Don't consider a Git refs to be a version number, see #169 - Use installed spec for executing scripts, see #143 - Don't expect `shard.lock` when `shard.yml` has no dependencies, see #145 - Compatibility with Crystal 0.24.0 (unreleased) - Harmonize error messages - Correct shard.yml parse error line:column reporting ## [0.7.1] (2016-11-24) [0.7.1]: https://github.com/crystal-lang/shards/releases/0.7.1 ### Fixes - correctly updates or keeps dependencies, see #107, #141 - upgrades minitest dependency so test do run ## [0.7.0] (2016-11-18) [0.7.0]: https://github.com/crystal-lang/shards/releases/0.7.0 ### Features - Build command for `targets` entry in SPEC - New Crystal search path algorithm (see breaking changes below) - Informational `crystal` entry in SPEC - Informational `libraries` entry in SPEC - Shorthand for gitlab.com dependencies Breaking Changes: - Dependencies are installed in the `lib` directory - Dependencies are now fully installed, instead of merely the `src` folder - `postinstall` scripts are now executed from the root of the dependency, not the `src` directory ### Fixes - crash when dependency keys were unordered - `tar` command usage on OpenBSD - correctly report git errors - the update command created a lockfile for empty dependencies ## [0.6.4] (2016-11-18) [0.6.4]: https://github.com/crystal-lang/shards/releases/0.6.4 ### Fixes - Compatibility with Crystal 0.19.0 ## [0.6.3] (2016-05-05) [0.6.3]: https://github.com/crystal-lang/shards/releases/0.6.3 ### Fixes - Compatibility with Crystal > 0.15.0 - Relative paths for path dependencies, see #99 ## [0.6.2] (2016-03-07) [0.6.2]: https://github.com/crystal-lang/shards/releases/0.6.2 ### Fixes - Don't crash when git binary is missing. ## [0.6.1] (2016-02-16) [0.6.1]: https://github.com/crystal-lang/shards/releases/0.6.1 ### Fixes - Compatibility with Crystal > 0.11.1 ## [0.6.0] (2016-01-23) [0.6.0]: https://github.com/crystal-lang/shards/releases/0.6.0 ### Features - prune command to remove extraneous libs - init command to create an initial shard.yml ### Fixes - print details when postinstall script fails, see #84 - path resolver didn't verify the path actually existed, see #77 - recursion when shard name doesn't match dependency name, see #72 ## [0.5.4] (2015-12-23) [0.5.4]: https://github.com/crystal-lang/shards/releases/0.5.4 ### Fixes - Compatibility with Crystal > 0.9.1 ## [0.5.3] (2015-10-23) [0.5.3]: https://github.com/crystal-lang/shards/releases/0.5.3 ### Fixes - Git resolver didn't install the locked commit when using branch, tag or commit or just failed to install the dependency, see #65 and #67 ## [0.5.2] (2015-10-02) [0.5.2]: https://github.com/crystal-lang/shards/releases/0.5.2 ### Fixes - compilation on Crystal 0.9.0 ## [0.5.1] (2015-10-02) [0.5.1]: https://github.com/crystal-lang/shards/releases/0.5.1 ### Fixes - always generate a `shard.yml` when installing legacy dependencies, see #60 - only create `libs` and `.shards` folders when required, see #61 ## [0.5.0] (2015-09-28) [0.5.0]: https://github.com/crystal-lang/shards/releases/0.5.0 Breaking Change: - renamed `--no-colors` option as `--no-color` to match crystal ### Features - nice error messages for invalid `shard.yml` files ### Enhancements - upgraded to Crystal 0.8.0 - custom YAML parser for shard.yml compliant to the spec - binary releases for OS X and Linux 32 bits ### Fixes - install command fails to install dependencies on fresh projects - check command breaks whenever a dependency is missing - manager doesn't resolve dependencies of development dependencies recursively - support for Git < 1.7.11 (eg: Ubuntu Precise and Debian Wheezy) - don't generate lockfile for projects without dependencies - don't fail when loading empty Projectfile ## [0.4.0] (2015-09-14) [0.4.0]: https://github.com/crystal-lang/shards/releases/0.4.0 ### Features - lock resolved versions for indempotent installs across computers, see #27 - `--production` parameter to skip development dependencies - postintall hook to run a command after installing a dependency, see #19 Breaking Changes: - dropped support for custom dependency groups (but kept `development_dependencies`), see #27 ### Fixes - compatibility with Crystal 0.7.7 ## [0.3.1] (2015-08-16) [0.3.1]: https://github.com/crystal-lang/shards/releases/0.3.1 ### Fixes - don't install dependencies from optional groups recursively - manager didn't install path dependencies anymore ## [0.3.0] (2015-08-03) [0.3.0]: https://github.com/crystal-lang/shards/releases/0.3.0 ### Features - optional groups of dependencies, see #8 - generates default `shard.yml` from Git tags and `Projectfile` dependencies, see #6 ### Fixes - clone repository again when Git remote origin changes, see #4 ## [0.2.0] (2015-06-03) [0.2.0]: https://github.com/crystal-lang/shards/releases/0.2.0 ### Fixes - correctly accesses git versioned `shard.yml` files; - correctly links/extracts the `src` folder as the `libs/` folder for both Git and path resolvers. ## [0.1.0] (2015-05-23) [0.1.0]: https://github.com/crystal-lang/shards/releases/0.1.0 Initial release. shards-0.19.0/LICENSE000066400000000000000000000010561473060476400141070ustar00rootroot00000000000000Copyright 2012-2013 Julien Portalier 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. shards-0.19.0/Makefile000066400000000000000000000076011473060476400145440ustar00rootroot00000000000000.POSIX: # Recipes for this Makefile ## Build shards ## $ make ## Build shards in release mode ## $ make release=1 ## Run tests ## $ make test ## Run tests without fossil tests ## $ make test skip_fossil=1 ## Generate docs ## $ make docs ## Install shards ## $ make install ## Uninstall shards ## $ make uninstall ## Build and install shards ## $ make build && sudo make install release ?= ## Compile in release mode debug ?= ## Add symbolic debug info static ?= ## Enable static linking skip_fossil ?= ## Skip fossil tests skip_git ?= ## Skip git tests skip_hg ?= ## Skip hg tests DESTDIR ?= ## Install destination dir PREFIX ?= /usr/local## Install path prefix CRYSTAL ?= crystal SHARDS ?= shards override FLAGS += $(if $(release),--release )$(if $(debug),-d )$(if $(static),--static ) SHARDS_SOURCES = $(shell find src -name '*.cr') MOLINILLO_SOURCES = $(shell find lib/molinillo -name '*.cr' 2> /dev/null) SOURCES = $(SHARDS_SOURCES) $(MOLINILLO_SOURCES) TEMPLATES = src/templates/*.ecr SHARDS_CONFIG_BUILD_COMMIT := $(shell git rev-parse --short HEAD 2> /dev/null) SHARDS_VERSION := $(shell cat VERSION) SOURCE_DATE_EPOCH := $(shell (git show -s --format=%ct HEAD || stat -c "%Y" Makefile || stat -f "%m" Makefile) 2> /dev/null) EXPORTS := SHARDS_CONFIG_BUILD_COMMIT="$(SHARDS_CONFIG_BUILD_COMMIT)" SOURCE_DATE_EPOCH="$(SOURCE_DATE_EPOCH)" BINDIR ?= $(DESTDIR)$(PREFIX)/bin MANDIR ?= $(DESTDIR)$(PREFIX)/share/man INSTALL ?= /usr/bin/install MOLINILLO_VERSION = $(shell $(CRYSTAL) eval 'require "yaml"; puts YAML.parse(File.read("shard.lock"))["shards"]["molinillo"]["version"]') MOLINILLO_URL = "https://github.com/crystal-lang/crystal-molinillo/archive/v$(MOLINILLO_VERSION).tar.gz" # MSYS2 support (native Windows should use `Makefile.win` instead) ifeq ($(OS),Windows_NT) EXE := .exe WINDOWS := 1 else EXE := WINDOWS := endif .PHONY: all all: build include docs.mk .PHONY: build build: bin/shards$(EXE) .PHONY: clean clean: ## Remove build artifacts clean: clean_docs rm -f bin/shards$(EXE) bin/shards$(EXE): $(SOURCES) $(TEMPLATES) lib @mkdir -p bin $(EXPORTS) $(CRYSTAL) build $(FLAGS) src/shards.cr -o "$@" .PHONY: install install: ## Install shards install: bin/shards$(EXE) man/shards.1.gz man/shard.yml.5.gz $(INSTALL) -m 0755 -d "$(BINDIR)" "$(MANDIR)/man1" "$(MANDIR)/man5" $(INSTALL) -m 0755 bin/shards$(EXE) "$(BINDIR)" $(INSTALL) -m 0644 man/shards.1.gz "$(MANDIR)/man1" $(INSTALL) -m 0644 man/shard.yml.5.gz "$(MANDIR)/man5" .PHONY: uninstall uninstall: ## Uninstall shards uninstall: rm -f "$(BINDIR)/shards" rm -f "$(MANDIR)/man1/shards.1.gz" rm -f "$(MANDIR)/man5/shard.yml.5.gz" .PHONY: test test: ## Run all tests test: test_unit test_integration .PHONY: test_unit test_unit: ## Run unit tests test_unit: lib $(CRYSTAL) spec ./spec/unit/ $(if $(skip_fossil),--tag ~fossil) $(if $(skip_git),--tag ~git) $(if $(skip_hg),--tag ~hg) .PHONY: test_integration test_integration: ## Run integration tests test_integration: bin/shards$(EXE) $(CRYSTAL) spec ./spec/integration/ lib: shard.lock mkdir -p lib/molinillo $(SHARDS) install || (curl -L $(MOLINILLO_URL) | tar -xzf - -C lib/molinillo --strip-components=1) shard.lock: shard.yml [ $(SHARDS) = false ] || $(SHARDS) update man/%.gz: man/% gzip -c -9 $< > $@ .PHONY: help help: ## Show this help @echo @printf '\033[34mtargets:\033[0m\n' @grep -hE '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) |\ sort |\ awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}' @echo @printf '\033[34moptional variables:\033[0m\n' @grep -hE '^[a-zA-Z_-]+ \?=.*?## .*$$' $(MAKEFILE_LIST) |\ sort |\ awk 'BEGIN {FS = " \\?=.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}' @echo @printf '\033[34mrecipes:\033[0m\n' @grep -hE '^##.*$$' $(MAKEFILE_LIST) |\ awk 'BEGIN {FS = "## "}; /^## [a-zA-Z_-]/ {printf " \033[36m%s\033[0m\n", $$2}; /^## / {printf " %s\n", $$2}' shards-0.19.0/Makefile.win000066400000000000000000000077411473060476400153450ustar00rootroot00000000000000.POSIX: # Recipes for this Makefile ## Build shards ## $ make -f Makefile.win ## Build shards in release mode ## $ make -f Makefile.win release=1 ## Run tests ## $ make -f Makefile.win test ## Run tests without fossil tests ## $ make -f Makefile.win test skip_fossil=1 ## Install shards ## $ make -f Makefile.win install ## Uninstall shards ## $ make -f Makefile.win uninstall ## Build and install shards ## $ make -f Makefile.win build && sudo make -f Makefile.win install release ?= ## Compile in release mode debug ?= ## Add symbolic debug info static ?= ## Enable static linking skip_fossil ?= ## Skip fossil tests skip_git ?= ## Skip git tests skip_hg ?= ## Skip hg tests MAKEFLAGS += --no-builtin-rules .SUFFIXES: SHELL := cmd.exe CXX := cl.exe GLOB = $(shell dir $1 /B /S 2>NUL) MKDIR = if not exist $1 mkdir $1 MV = move /Y $1 $2 RM = if exist $1 del /F /Q $1 CRYSTAL ?= crystal.exe SHARDS ?= shards.exe override FLAGS += $(if $(release),--release )$(if $(debug),-d )$(if $(static),--static ) SHARDS_SOURCES = $(call GLOB,src\\*.cr) MOLINILLO_SOURCES = $(call GLOB,lib\\molinillo\\src\\*.cr) SOURCES = $(SHARDS_SOURCES) $(MOLINILLO_SOURCES) TEMPLATES = $(call GLOB,src\\templates\\*.ecr) SHARDS_CONFIG_BUILD_COMMIT := $(shell git rev-parse --short HEAD) SOURCE_DATE_EPOCH := $(shell git show -s --format=%ct HEAD) export_vars = $(eval export SHARDS_CONFIG_BUILD_COMMIT SOURCE_DATE_EPOCH) MOLINILLO_VERSION = $(shell $(CRYSTAL) eval 'require "yaml"; puts YAML.parse(File.read("shard.lock"))["shards"]["molinillo"]["version"]') MOLINILLO_URL = "https://github.com/crystal-lang/crystal-molinillo/archive/v$(MOLINILLO_VERSION).tar.gz" prefix ?= $(or $(ProgramW6432),$(ProgramFiles))\crystal## Install path prefix BINDIR ?= $(prefix) .PHONY: all all: build .PHONY: build build: bin\shards.exe .PHONY: clean clean: ## Remove build artifacts clean: $(call RM,"bin\shards.exe") bin\shards.exe: $(SOURCES) $(TEMPLATES) lib @$(call MKDIR,"bin") $(call export_vars) $(CRYSTAL) build $(FLAGS) -o bin\shards.exe src\shards.cr .PHONY: install install: ## Install shards install: bin\shards.exe $(call MKDIR,"$(BINDIR)") $(call INSTALL,"bin\shards.exe","$(BINDIR)\shards.exe") $(call INSTALL,"bin\shards.pdb","$(BINDIR)\shards.pdb") .PHONY: uninstall uninstall: ## Uninstall shards uninstall: $(call RM,"$(BINDIR)\shards.exe") $(call RM,"$(BINDIR)\shards.pdb") .PHONY: test test: ## Run all tests test: test_unit test_integration .PHONY: test_unit test_unit: ## Run unit tests test_unit: lib $(CRYSTAL) spec $(if $(skip_fossil),--tag ~fossil )$(if $(skip_git),--tag ~git )$(if $(skip_hg),--tag ~hg ).\spec\unit .PHONY: test_integration test_integration: ## Run integration tests test_integration: bin\shards.exe $(CRYSTAL) spec .\spec\integration lib: shard.lock $(call MKDIR,"lib\molinillo") $(SHARDS) install || (curl -L $(MOLINILLO_URL) | tar -xzf - -C lib\molinillo --strip-components=1) shard.lock: shard.yml if not "$(SHARDS)" == "false" $(SHARDS) update .PHONY: help help: ## Show this help @setlocal EnableDelayedExpansion &\ echo. &\ echo targets: &\ (for /F "usebackq tokens=1* delims=:" %%g in ($(MAKEFILE_LIST)) do (\ if not "%%h" == "" (\ set "_line=%%g " &\ set "_rest=%%h" &\ set "_comment=!_rest:* ## =!" &\ if not "!_comment!" == "!_rest!"\ if "!_line:_rest=!" == "!_line!"\ echo !_line:~0,17!!_comment!\ )\ )) &\ echo. &\ echo optional variables: &\ (for /F "usebackq tokens=1,3 delims=?#" %%g in ($(MAKEFILE_LIST)) do (\ if not "%%h" == "" (\ set "_var=%%g " &\ echo !_var:~0,15! %%h\ )\ )) &\ echo. &\ echo recipes: &\ (for /F "usebackq tokens=* delims=" %%g in ($(MAKEFILE_LIST)) do (\ set "_line=%%g" &\ if "!_line:~0,7!" == "## $$ " (\ echo !_name! &\ echo !_line:~2!\ ) else if "!_line:~0,3!" == "## "\ set "_name= !_line:~3!"\ )) shards-0.19.0/README.md000066400000000000000000000102431473060476400143570ustar00rootroot00000000000000# Shards [![CI](https://github.com/crystal-lang/shards/workflows/CI/badge.svg)](https://github.com/crystal-lang/shards/actions?query=workflow%3ACI+event%3Apush+branch%3Amaster) Dependency manager for the [Crystal language](https://crystal-lang.org). ## Usage Crystal applications and libraries are expected to have a `shard.yml` file at their root looking like this: ```yaml name: shards version: 0.1.0 dependencies: openssl: github: datanoise/openssl.cr branch: master development_dependencies: minitest: git: https://github.com/ysbaddaden/minitest.cr.git version: ~> 0.3.1 license: MIT ``` When libraries are installed from Git repositories, the repository is expected to have version tags following a [semver](http://semver.org/)-like format, prefixed with a `v`. Examples: `v1.2.3`, `v2.0.0-rc1` or `v2017.04.1`. Please see the [SPEC](docs/shard.yml.adoc) for more details about the `shard.yml` format. ## Install Shards is usually distributed with Crystal itself (e.g. Homebrew and Debian packages). Alternatively, a `shards` package may be available for your system. You can download a source tarball from the same page (or clone the repository) then run `make release=1`and copy `bin/shards` into your `PATH`. For example `/usr/local/bin`. You are now ready to create a `shard.yml` for your projects (see details in [SPEC](docs/shard.yml.adoc)). You can type `shards init` to have an example `shard.yml` file created for your project. Run `shards install` to install your dependencies, which will lock your dependencies into a `shard.lock` file. You should check both `shard.yml` and `shard.lock` into version control, so further `shards install` will always install locked versions, achieving reproducible installations across computers. Run `shards --help` to list other commands with their options. Happy Hacking! ## Developers ### Requirements These requirements are only necessary for compiling Shards. * Crystal Please refer to for instructions for your operating system. * `molinillo` The shard `molinillo` needs to be in the Crystal path. It is available at You can install it either with a pre-existing `shards` binary (running `shards install`) or just check out the repository at `lib/crystal-molinillo` (`make lib`). * libyaml On Debian/Ubuntu Linux you may install the `libyaml-dev` package. On Mac OS X you may install it using homebrew with `brew install libyaml` then make sure to have `/usr/local/lib` in your `LIBRARY_PATH` environment variable (eg: `export LIBRARY_PATH="/usr/local/lib:$LIBRARY_PATH"`). Please adjust the path per your Homebrew installation. * [asciidoctor](https://asciidoctor.org/) Needed for building manpages. ### Getting started It is strongly recommended to use `make` for building shards and developing it. The [`Makefile`](./Makefile) contains recipes for compiling and testing. Building with `make` also ensures the source dependency `molinillo` is installed. You don't need to take care of this yourself. Run `make bin/shards` to build the binary. * `release=1` for a release build (applies optimizations) * `static=1` for static linking (only works with musl-libc) * `debug=1` for full symbolic debug info Run `make install` to install the binary. Target path can be adjusted with `PREFIX` (default: `PREFIX=/usr/bin`). Run `make test` to run the test suites: * `make test_unit` runs unit tests (`./spec/unit`) * `make test_integration` runs integration tests (`./spec/integration`) on `bin/shards` Run `make docs` to build the manpages. ### Devenv This repository contains a configuration for [devenv.sh](https://devenv.sh) which makes it easy to setup a reproducible environment with all necessary tools for building and testing. - Checkout the repository - Run `devenv shell` to get a shell with development environment A hook for [automatic shell activation](https://devenv.sh/automatic-shell-activation/) is also included. If you have `direnv` installed, the devenv environment loads automatically upon entering the repo folder. ## License Licensed under the Apache License, Version 2.0. See [LICENSE](./LICENSE) for details. shards-0.19.0/SPEC.md000066400000000000000000000001351473060476400141530ustar00rootroot00000000000000The documentation for `shard.yml` has moved to [`docs/shard.yml.adoc`](docs/shard.yml.adoc). shards-0.19.0/VERSION000066400000000000000000000000071473060476400141450ustar00rootroot000000000000000.19.0 shards-0.19.0/bin/000077500000000000000000000000001473060476400136505ustar00rootroot00000000000000shards-0.19.0/bin/.keep000066400000000000000000000000001473060476400145630ustar00rootroot00000000000000shards-0.19.0/devenv.lock000066400000000000000000000052561473060476400152510ustar00rootroot00000000000000{ "nodes": { "devenv": { "locked": { "dir": "src/modules", "lastModified": 1734441494, "owner": "cachix", "repo": "devenv", "rev": "bdc1a2cefdda8f89e31b1a0f3771786ba9e5d052", "type": "github" }, "original": { "dir": "src/modules", "owner": "cachix", "repo": "devenv", "type": "github" } }, "flake-compat": { "flake": false, "locked": { "lastModified": 1733328505, "owner": "edolstra", "repo": "flake-compat", "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", "type": "github" }, "original": { "owner": "edolstra", "repo": "flake-compat", "type": "github" } }, "gitignore": { "inputs": { "nixpkgs": [ "pre-commit-hooks", "nixpkgs" ] }, "locked": { "lastModified": 1709087332, "owner": "hercules-ci", "repo": "gitignore.nix", "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", "type": "github" }, "original": { "owner": "hercules-ci", "repo": "gitignore.nix", "type": "github" } }, "nixpkgs": { "locked": { "lastModified": 1733477122, "owner": "cachix", "repo": "devenv-nixpkgs", "rev": "7bd9e84d0452f6d2e63b6e6da29fe73fac951857", "type": "github" }, "original": { "owner": "cachix", "ref": "rolling", "repo": "devenv-nixpkgs", "type": "github" } }, "nixpkgs-stable": { "locked": { "lastModified": 1734202038, "owner": "NixOS", "repo": "nixpkgs", "rev": "bcba2fbf6963bf6bed3a749f9f4cf5bff4adb96d", "type": "github" }, "original": { "owner": "NixOS", "ref": "nixos-24.05", "repo": "nixpkgs", "type": "github" } }, "pre-commit-hooks": { "inputs": { "flake-compat": "flake-compat", "gitignore": "gitignore", "nixpkgs": [ "nixpkgs" ], "nixpkgs-stable": "nixpkgs-stable" }, "locked": { "lastModified": 1734425854, "owner": "cachix", "repo": "pre-commit-hooks.nix", "rev": "0ddd26d0925f618c3a5d85a4fa5eb1e23a09491d", "type": "github" }, "original": { "owner": "cachix", "repo": "pre-commit-hooks.nix", "type": "github" } }, "root": { "inputs": { "devenv": "devenv", "nixpkgs": "nixpkgs", "pre-commit-hooks": "pre-commit-hooks" } } }, "root": "root", "version": 7 } shards-0.19.0/devenv.nix000066400000000000000000000006311473060476400151070ustar00rootroot00000000000000{ pkgs, lib, config, inputs, ... }: { # https://devenv.sh/packages/ packages = with pkgs; [ # build deps gnumake asciidoctor # test deps git fossil mercurial ]; enterShell = '' crystal --version ''; languages.crystal.enable = true; # https://devenv.sh/pre-commit-hooks/ pre-commit.hooks.shellcheck.enable = true; pre-commit.hooks.crystal.enable = true; } shards-0.19.0/devenv.yaml000066400000000000000000000005251473060476400152550ustar00rootroot00000000000000inputs: nixpkgs: url: github:cachix/devenv-nixpkgs/rolling # If you're using non-OSS software, you can set allowUnfree to true. # allowUnfree: true # If you're willing to use a package that's vulnerable # permittedInsecurePackages: # - "openssl-1.1.1w" # If you have more than one devenv you can merge them #imports: # - ./backend shards-0.19.0/docs.mk000066400000000000000000000017641473060476400143710ustar00rootroot00000000000000ASCIIDOC ?= asciidoctor ASCIIDOC_OPTIONS = -a shards_version=$(SHARDS_VERSION) MAN_FILES := man/shards.1 man/shard.yml.5 HTML_FILES := docs/shards.html docs/shard.yml.html SHARDS_VERSION := $(shell cat VERSION) SOURCE_DATE_EPOCH := $(shell (git show -s --format=%ct HEAD || stat -c "%Y" Makefile || stat -f "%m" Makefile) 2> /dev/null) .PHONY: docs docs: ## Build documentation docs: manpages .PHONY: manpages manpages: ## Generate manpages from adoc manpages: $(MAN_FILES) .PHONY: htmlpages htmlpages: ## Generate HTML files from adoc htmlpages: $(HTML_FILES) man/%.1: docs/%.adoc SOURCE_DATE_EPOCH=$(SOURCE_DATE_EPOCH) $(ASCIIDOC) $(ASCIIDOC_OPTIONS) $< -b manpage -o $@ man/%.5: docs/%.adoc SOURCE_DATE_EPOCH=$(SOURCE_DATE_EPOCH) $(ASCIIDOC) $(ASCIIDOC_OPTIONS) $< -b manpage -o $@ docs/%.html: docs/%.adoc SOURCE_DATE_EPOCH=$(SOURCE_DATE_EPOCH) $(ASCIIDOC) $(ASCIIDOC_OPTIONS) $< -b html5 -o $@ .PHONY: clean_docs clean_docs: ## Remove documentation data rm -f $(MAN_FILES) rm -rf docs/*.html shards-0.19.0/docs/000077500000000000000000000000001473060476400140305ustar00rootroot00000000000000shards-0.19.0/docs/shard.yml.adoc000066400000000000000000000315601473060476400165660ustar00rootroot00000000000000= shard.yml(5) :date: {localdate} :shards_version: {shards_version} :man manual: File Formats :man source: shards {shards_version} == NAME shard.yml - metadata for projects managed by shards(1) == DESCRIPTION The file _shard.yml_ is a YAML file with metadata about a project managed by shards, known as a *shard*. It must contain at least _name_ and _version_ attributes plus optional additional attributes. Both libraries and applications will benefit from `shard.yml`. The metadata for libraries are expected to have more information (e.g., list of authors, description, license) than applications that may only have a name, version and dependencies. == FORMAT The file must be named _shard.yml_ and be a valid YAML file with UTF-8 encoding. It must not contain duplicate attributes in any mapping. It should use an indent of 2 spaces. It should not use advanced YAML features, only simple mappings, sequences and strings (Failsafe Schema). == REQUIRED ATTRIBUTES *name*:: The name of the project (string, required). + -- - It must be unique. - It must be 50 characters or less. - It should be lowercase (a-z). - It should not contain _crystal_. - It may contain digits (0-9) but not start with one. - It may contain underscores or dashes but not start/end with one. - It must not have consecutive underscores or dashes. -- + Examples: _minitest_, _mysql2_, _battery-horse_. *version*:: The version number of the project (string, required). + -- - It must contain digits. - It may contain dots and dashes but not consecutive ones. - It may contain a letter to make it a 'prerelease'. -- + Examples: _1.2.3_, _2.0.0.1_, _1.0.0.alpha_ _2.0.0-rc1_ or _2016.09_. + While Shards doesn't enforce it, following a rational versioning scheme like http://semver.org/[Semantic Versioning] or http://calver.org/[Calendar Versioning] is highly recommended. == OPTIONAL ATTRIBUTES *authors*:: A list of authors, along with their contact email (optional) (sequence of string). + -- - Each author must have a name. - Each author may have an email address, within angle bracket (_<_ and _>_) chars. -- + *Example:* + [source,yaml] ---- authors: - Ary - Julien Portalier ---- *crystal*:: A restriction to indicate which are the supported crystal versions. This will usually express a lower and upper-bound constraints (string, recommended) + When resolving dependencies, this information is not used. After dependencies have been determined shards checks all of them are expected to work with the current crystal version. If not, a warning appears for the offending dependencies. The resolved versions are installed and can be used at your own risk. + The valid values are mostly the same as for _dependencies.version_: + -- * A version number prefixed by an operator: _<_, _\<=_, _>_, _>=_, _!=_ or _~>_. * Just _"*"_ if any version will do (this is the default if unspecified). * Multiple requirements can be separated by commas. -- There is a special legacy behavior (its use is discouraged) when just a version number is used as the value: it works exactly the same as a `>=` check: _x.y.z_ is interpreted as _">= x.y.z"_ + You are welcome to also specify the upper bound to be lower than the next (future) major Crystal version, because there's no guarantee that it won't break your library. + *Example:* + [source,yaml] ---- crystal: ">= 0.35, < 2.0" ---- *dependencies*:: A list of required dependencies (mapping). + Each dependency begins with the name of the dependency as a key (string) then a list of attributes (mapping) that depend on the resolver type. + *Example:* + [source,yaml] ---- dependencies: minitest: github: ysbaddaden/minitest.cr version: 0.1.0 ---- *development_dependencies*:: A list of dependencies required to work on the project, but not necessary to build and run the project (mapping). + They will be installed for the main project or library itself. When the library is installed as a dependency for another project the development dependencies will never be installed. + Development dependencies follow the same scheme as dependencies. + *Example:* + [source,yaml] ---- development_dependencies: minitest: github: ysbaddaden/minitest.cr version: ~> 0.1.3 ---- *description*:: A single line description of the project (string, recommended). *documentation*:: The URL to a website providing the project's documentation for online browsing (string). *executables*:: A list of executables to be installed (sequence). + The executables can be of any type or language (e.g., shell, binary, ruby), must exist in the _bin_ folder of the Shard, and have the executable bit set (on POSIX platforms). When installed as a dependency for another project the executables will be copied to the _bin_ folder of that project. + Executables are always installed last, after the _postinstall_ script is run, so libraries can build the executables when they are installed by Shards. Installation can be disabled by passing the flag _--skip-executables_. + *Example:* + [source,yaml] ---- executables: - micrate - icr ---- *homepage*:: The URL of the project's homepage (string). *libraries*:: A list of shared libraries the shard tries to link to (mapping). + This field is purely informational. It serves as a canonical way to discover non Crystal dependencies in shards, both for tools as well as humans. + A shard must only list libraries it directly links to, it must not include libraries that are only referenced by dependencies. It must include all libraries it directly links to, regardless of a dependency doing it too. + It should map from the soname without any extension, path or version, for example _libsqlite3_ for _/usr/lib/libsqlite3.so.0.8.6_, to a version constraint. + The version constraint has the following format: + -- - It may be a version number. - It may be _"*"_ if any version will do. - The version number may be prefixed by an operator: _<_, _\<=_, _>_, _>=_, _!=_ or _~>_. -- + [source,yaml] ---- libraries: libQt5Gui: "*" libQt5Help: "~> 5.7" libQtBus: ">= 4.8" ---- *license*:: An https://spdx.github.io/spdx-spec/v3.0.1/annexes/spdx-license-expressions/[SPDX license expression] or an URL to a license file (string, recommended). + The OSI publishes https://opensource.org/licenses-old/category[a list] of open source licenses and their corresponding SPDX identifiers. + Examples: _Apache-2.0_, _GPL-3.0-or-later_, _Apache-2.0 OR MIT_, _Apache-2.0 WITH Swift-exception_, _https://example.com/LICENSE_. *repository*:: The URL of the project's canonical repository (string, recommended). + The URL should be compatible with typical VCS tools without modifications. _http_/_https_ is preferred over VCS schemes like _git_. It is recommended that this URL is publicly available. + Copies of a shard (such as mirrors, development forks etc.) should point to the same canonical repository address, even if hosted at different locations. + *Example:* + [source,yaml] ---- repository: "https://github.com/crystal-lang/shards" ---- *scripts*:: Script hooks to run. Only _postinstall_ is supported. + Shards may run scripts automatically after certain actions. The scripts themselves are mere shell commands. *postinstall*::: The _postinstall_ hook of a dependency will be run whenever that dependency is installed or upgraded in a project that requires it. This may be used to compile a C library, to build tools to help working on the project, or anything else. + The script will be run from the dependency's installation directory, for example _lib/foo_ for a Shard named _foo_. + *Example:* + [source,yaml] ---- scripts: postinstall: cd src/libfoo && make ---- *targets*:: A list of targets to build (mapping). + Each target begins with the name of the target as a key (string), then a list of attributes (mapping). The target name is the built binary name, created in the _bin_ folder of the project. + *Example:* + [source,yaml] ---- targets: server: main: src/server/cli.cr worker: main: src/worker.cr ---- + The above example will build _bin/server_ from _src/server/cli.cr_ and _bin/worker_ from _src/worker.cr_. *main*::: A path to the source file to compile (string). == DEPENDENCY ATTRIBUTES Each dependency needs at least one attribute that defines the resolver for this dependency. Those can be _path_, _git_, _github_, _gitlab_, _bitbucket_, _codeberg_. *path*:: A local path (string). + The library will be installed as a symlink to the local path. The _version_ attribute isn't required but will be used if present to validate the dependency. *git*:: A Git repository URL (string). + The URL may be https://git-scm.com/docs/git-clone#_git_urls[any protocol] supported by Git, which includes SSH, GIT and HTTPS. + The Git repository will be cloned, the list of versions (and associated _shard.yml_) will be extracted from Git tags (e.g., _v1.2.3_). + One of the other attributes (_version_, _tag_, _branch_ or _commit_) is required. When missing, Shards will install the HEAD refs. + *Example:* _git: git://git.example.org/crystal-library.git_ *github*:: GitHub repository URL as _user/repo_ (string) + Extends the _git_ resolver, and acts exactly like it. + *Example:* _github: ysbaddaden/minitest.cr_ *gitlab*:: GitLab repository URL as _user/repo_ (string). + Extends the _git_ resolver, and acts exactly like it. + Only matches dependencies hosted on _gitlab.com_. For personal GitLab installations, you must use the generic _git_ resolver. + *Example:* _gitlab: thelonlyghost/minitest.cr_ *bitbucket*:: Bitbucket repository URL as _user/repo_ (string). + Extends the _git_ resolver, and acts exactly like it. + *Example:* _bitbucket: tom/library_ *codeberg*:: Codeberg repository URL as _user/repo_ (string). + Extends the _git_ resolver, and acts exactly like it. + *Example:* _codeberg: tom/library_ *hg*:: A Mercurial repository URL (string). + The URL may be https://www.mercurial-scm.org/repo/hg/help/clone[any protocol] supported by Mercurial, which includes SSH and HTTPS. + The Mercurial repository will be cloned, the list of versions (and associated _shard.yml_) will be extracted from Mercurial tags (e.g., _v1.2.3_). + One of the other attributes (_version_, _tag_, _branch_, _bookmark_ or _commit_) is required. When missing, Shards will install the _@_ bookmark or _tip_. + *Example:* _hg: https://hg.example.org/crystal-library_ *fossil*:: A https://www.fossil-scm.org[Fossil] repository URL (string). + The URL may be https://fossil-scm.org/home/help/clone[any protocol] supported by Fossil, which includes SSH and HTTPS. + The Fossil repository will be cloned, the list of versions (and associated _shard.yml_) will be extracted from Fossil tags (e.g., _v1.2.3_). + One of the other attributes (_version_, _tag_, _branch_, or _commit_) is required. When missing, Shards will install _trunk_. + *Example:* _fossil: https://fossil.example.org/crystal-library_ *version*:: A version requirement (string). + -- - It may be an explicit version number. - It may be _"*"_ wildcard if any version will do (this is the default). Shards will then install the latest tagged version (or HEAD if no tagged version available). - The version number may be prefixed by an operator: _<_, _\<=_, _>_, _>=_, _!=_ or _~>_. - Multiple requirements can be separated by commas. -- + Examples: _1.2.3_, _>= 1.0.0_, _>= 1.0.0, < 2.0_ or _~> 2.0_. + Most of the version operators, like _>= 1.0.0_, are self-explanatory, but the _~>_ operator has a special meaning. It specifies a minimum version, but allows the last digit specified to go up, excluding the major release number: -- - _~> 0.3.5_ is identical to _>= 0.3.5 and < 0.4.0_. - _~> 2.0.3_ is identical to _>= 2.0.3 and < 2.1_. - _~> 2.1_ is identical to _>= 2.1 and < 3.0_. - _~> 0.3_ is identical to _>= 0.3 and < 1.0_. - _~> 1_ is identical to _>= 1.0 and < 2.0_. -- NOTE: Even though _2.1.0-dev_ is strictly before _2.1.0_, a version constraint like _~> 2.0.3_ would not install it since only the _.3_ can change but the _2.0_ part is fixed. *branch*:: Install the specified branch of a git dependency, or the named branch of a mercurial or fossil dependency (string). *commit*:: Install the specified commit of a git, mercurial, or fossil dependency (string). *tag*:: Install the specified tag of a git, mercurial, or fossil dependency (string). *bookmark*:: Install the specified bookmark of a mercurial dependency (string). == Example: Here is an example _shard.yml_ for a library named _shards_ at version _1.2.3_ with some dependencies: [source,yaml] ---- name: shards version: 1.2.3 crystal: '>= 0.35.0' authors: - Julien Portalier license: MIT description: | Dependency manager for the Crystal Language dependencies: openssl: github: datanoise/openssl.cr branch: master development_dependencies: minitest: git: https://github.com/ysbaddaden/minitest.cr.git version: "~> 0.1.0" libraries: libgit2: ~> 0.24 scripts: postinstall: make ext targets: shards: main: src/shards.cr ---- == AUTHOR Written by Julien Portalier and the Crystal project. == SEE ALSO *shards*(1) shards-0.19.0/docs/shard.yml.schema.json000066400000000000000000000210431473060476400200630ustar00rootroot00000000000000{ "$schema": "http://json-schema.org/draft-07/schema#", "title": "shard.yml", "description": "Metadata for projects managed by Shards", "type": "object", "properties": { "name": { "type": "string", "title": "project name", "description": "The name of the project", "maxLength": 50, "pattern": "^(?!.*__|.*--|.*crystal|[0-9_-])[a-z0-9_-]+(?_...] [__] [__...] == DESCRIPTION Manages dependencies for Crystal projects and libraries with reproducible installs across computers and systems. == USAGE _shards_ requires the presence of a _shard.yml_ file in the project folder (working directory). This file describes the project and lists dependencies that are required to build it. See *shard.yml*(5) for more information on its format. A default file can be created by running _shards init_. Running _shards install_ resolves and installs the specified dependencies. The installed versions are written into a *shard.lock* file for using the exact same dependency versions when running _shards install_ again. If your shard builds an application, both *shard.yml* and *shard.lock* should be checked into version control to provide reproducible dependency installs. If it is only a library for other shards to depend on, *shard.lock* should _not_ be checked in, only *shard.yml*. It’s good advice to add it to *.gitignore*. == COMMANDS If no _command_ is given, *install* command will be run by default. To see the available options for a particular command, use _--help_ after the command. *build* [__] [__...]:: Builds the specified __ in *bin* path. If no targets are specified, all are built. This command ensures all dependencies are installed, so it is not necessary to run *shards install* before. + All __ following the command are delegated to *crystal build*. *check*:: Verifies that all dependencies are installed and requirements are satisfied. + Exit status: + [horizontal] *0*::: Dependencies are satisfied. *1*::: Dependencies are not satisfied. *init*:: Initializes a default _shard.yml_ in the current folder. *install* [--frozen] [--without-development] [--production] [--skip-postinstall] [--skip-executables] [--jobs=N]:: Resolves and installs dependencies into the _lib_ folder. If not already present, generates a _shard.lock_ file from resolved dependencies, locking version numbers or Git commits. + Reads and enforces locked versions and commits if a _shard.lock_ file is present. The *install* command may fail if a locked version doesn't match a requirement, but may succeed if a new dependency was added, as long as it doesn't generate a conflict, thus generating a new _shard.lock_ file. + -- --frozen:: Strictly installs locked versions from _shard.lock_. Fails if _shard.lock_ is missing. --without-development:: Does not install development dependencies. --production:: same as _--frozen_ and _--without-development_ --skip-postinstall:: Does not run postinstall of dependencies. --skip-executables:: Does not install executables. --jobs:: Number of repository downloads to perform in parallel (default: 8). Currently only for git. -- *list* [--tree]:: Lists the installed dependencies and their versions. + Specifying _--tree_ arranges nested dependencies in a tree, instead of a flattened list. *lock* [--update [...]]:: Resolves dependencies and creates or updates the _shard.lock_ file as per the *install* command, but never installs the dependencies. + Specifying _--update_ follows the same semantics as the *update* command. *outdated* [--pre]:: Lists dependencies that are outdated. + When _--pre_ is specified, pre-release versions are also considered. *prune*:: Removes unused dependencies from _lib_ folder. *update* [...]:: Resolves and updates all dependencies into the _lib_ folder, whatever the locked versions in the _shard.lock_ file. Eventually generates a new _shard.lock_ file. + Specifying _shards_ will update these dependencies only, trying to be as conservative as possible with other dependencies, respecting the locked versions in the _shard.lock_ file. *version* [__]:: Prints the current version of the shard located at _path_ (defaults to current directory). To see the available options for a particular command, use *--help* after a command. == GENERAL OPTIONS --version:: Prints the version of _shards_. -h, --help:: Prints usage synopsis. --no-color:: Disables colored output. --local:: Do not update remote repository cache. Instead, Shards will use the local copies already present in the cache (see *SHARDS_CACHE_PATH*). The command will fail if a dependency is unavailable in the cache. -q, --quiet:: Decreases the log verbosity, printing only warnings and errors. -v, --verbose:: Increases the log verbosity, printing all debug statements. == INSTALLATION Shards is usually distributed with Crystal itself. Alternatively, a separate _shards_ package may be available for your system. To install from source, download or clone https://github.com/crystal-lang/shards[the repository] and run *make CRFLAGS=--release*. The compiled binary is in _bin/shards_ and should be added to *PATH*. == Environment variables SHARDS_OPTS:: Allows general options to be passed in as environment variable. *Example*: _SHARDS_OPTS="--no-color" shards update_ SHARDS_CACHE_PATH:: Defines the cache location. In this folder, shards stores local copies of remote repositories. Defaults to _.cache/shards_ in the home directory (_$XDG_CACHE_HOME_ or _$HOME_) or the current directory. SHARDS_INSTALL_PATH:: Defines the location where dependencies are installed. Defaults to _lib_. SHARDS_BIN_PATH:: Defines the location where executables are installed. Defaults to _bin_. CRYSTAL_VERSION:: Defines the crystal version that dependencies should be resolved against. Defaults to the output of _crystal env CRYSTAL_VERSION_. SHARDS_OVERRIDE:: Defines an alternate location of _shard.override.yml_. == Files shard.yml:: Describes a shard project including its dependencies. See *shard.yml*(5) for documentation. shard.override.yml:: Allows overriding the source and restriction of dependencies. An alternative location can be configured with the env var *SHARDS_OVERRIDE*. + The file contains a YAML document with a single *dependencies* key. It has the same semantics as in *shard.yml*. Dependency configuration takes precedence over the configuration in *shard.yml* or any dependency's *shard.yml*. + Use cases are local working copies, forcing a specific dependency version despite mismatching constraints, fixing a dependency, checking compatibility with unreleased dependency versions. shard.lock:: Lockfile that stores information about the installed versions. + If your shard builds an application, *shard.lock* should be checked into version control to provide reproducible dependency installs. + If it is only a library for other shards to depend on, *shard.lock* should _not_ be checked in, only *shard.yml*. It’s good advice to add it to *.gitignore*. == REPORTING BUGS Report shards bugs to Crystal Language home page: == COPYRIGHT Copyright © {localyear} Julien Portalier. http://www.apache.org/licenses/LICENSE-2.0[License Apache 2.0] This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. == AUTHORS Written by Julien Portalier and the Crystal project. == SEE ALSO *shard.yml*(5) shards-0.19.0/man/000077500000000000000000000000001473060476400136535ustar00rootroot00000000000000shards-0.19.0/man/shard.yml.5000066400000000000000000000420351473060476400156460ustar00rootroot00000000000000'\" t .\" Title: shard.yml .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.20 .\" Date: 2024-12-18 .\" Manual: File Formats .\" Source: shards 0.19.0 .\" Language: English .\" .TH "SHARD.YML" "5" "2024-12-18" "shards 0.19.0" "File Formats" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 .nh .ad l .de URL \fI\\$2\fP <\\$1>\\$3 .. .als MTO URL .if \n[.g] \{\ . mso www.tmac . am URL . ad l . . . am MTO . ad l . . . LINKSTYLE blue R < > .\} .SH "NAME" shard.yml \- metadata for projects managed by shards(1) .SH "DESCRIPTION" .sp The file \fIshard.yml\fP is a YAML file with metadata about a project managed by shards, known as a \fBshard\fP. It must contain at least \fIname\fP and \fIversion\fP attributes plus optional additional attributes. .sp Both libraries and applications will benefit from \f(CRshard.yml\fP. .sp The metadata for libraries are expected to have more information (e.g., list of authors, description, license) than applications that may only have a name, version and dependencies. .SH "FORMAT" .sp The file must be named \fIshard.yml\fP and be a valid YAML file with UTF\-8 encoding. It must not contain duplicate attributes in any mapping. It should use an indent of 2 spaces. It should not use advanced YAML features, only simple mappings, sequences and strings (Failsafe Schema). .SH "REQUIRED ATTRIBUTES" .sp \fBname\fP .RS 4 The name of the project (string, required). .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} It must be unique. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} It must be 50 characters or less. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} It should be lowercase (a\-z). .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} It should not contain \fIcrystal\fP. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} It may contain digits (0\-9) but not start with one. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} It may contain underscores or dashes but not start/end with one. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} It must not have consecutive underscores or dashes. .RE .sp Examples: \fIminitest\fP, \fImysql2\fP, \fIbattery\-horse\fP. .RE .sp \fBversion\fP .RS 4 The version number of the project (string, required). .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} It must contain digits. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} It may contain dots and dashes but not consecutive ones. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} It may contain a letter to make it a \*(Aqprerelease\*(Aq. .RE .sp Examples: \fI1.2.3\fP, \fI2.0.0.1\fP, \fI1.0.0.alpha\fP \fI2.0.0\-rc1\fP or \fI2016.09\fP. .sp While Shards doesn\(cqt enforce it, following a rational versioning scheme like .URL "http://semver.org/" "Semantic Versioning" "" or .URL "http://calver.org/" "Calendar Versioning" is highly recommended. .RE .SH "OPTIONAL ATTRIBUTES" .sp \fBauthors\fP .RS 4 A list of authors, along with their contact email (optional) (sequence of string). .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} Each author must have a name. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} Each author may have an email address, within angle bracket (\fI<\fP and \fI>\fP) chars. .RE .sp \fBExample:\fP .sp .if n .RS 4 .nf .fam C authors: \- Ary \- Julien Portalier .fam .fi .if n .RE .RE .sp \fBcrystal\fP .RS 4 A restriction to indicate which are the supported crystal versions. This will usually express a lower and upper\-bound constraints (string, recommended) .sp When resolving dependencies, this information is not used. After dependencies have been determined shards checks all of them are expected to work with the current crystal version. If not, a warning appears for the offending dependencies. The resolved versions are installed and can be used at your own risk. .sp The valid values are mostly the same as for \fIdependencies.version\fP: .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} A version number prefixed by an operator: \fI<\fP, \fI<=\fP, \fI>\fP, \fI>=\fP, \fI!=\fP or \fI~>\fP. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} Just \fI"*"\fP if any version will do (this is the default if unspecified). .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} Multiple requirements can be separated by commas. .RE .sp There is a special legacy behavior (its use is discouraged) when just a version number is used as the value: it works exactly the same as a \f(CR>=\fP check: \fIx.y.z\fP is interpreted as \fI">= x.y.z"\fP .sp You are welcome to also specify the upper bound to be lower than the next (future) major Crystal version, because there\(cqs no guarantee that it won\(cqt break your library. .sp \fBExample:\fP .sp .if n .RS 4 .nf .fam C crystal: ">= 0.35, < 2.0" .fam .fi .if n .RE .RE .sp \fBdependencies\fP .RS 4 A list of required dependencies (mapping). .sp Each dependency begins with the name of the dependency as a key (string) then a list of attributes (mapping) that depend on the resolver type. .sp \fBExample:\fP .sp .if n .RS 4 .nf .fam C dependencies: minitest: github: ysbaddaden/minitest.cr version: 0.1.0 .fam .fi .if n .RE .RE .sp \fBdevelopment_dependencies\fP .RS 4 A list of dependencies required to work on the project, but not necessary to build and run the project (mapping). .sp They will be installed for the main project or library itself. When the library is installed as a dependency for another project the development dependencies will never be installed. .sp Development dependencies follow the same scheme as dependencies. .sp \fBExample:\fP .sp .if n .RS 4 .nf .fam C development_dependencies: minitest: github: ysbaddaden/minitest.cr version: ~> 0.1.3 .fam .fi .if n .RE .RE .sp \fBdescription\fP .RS 4 A single line description of the project (string, recommended). .RE .sp \fBdocumentation\fP .RS 4 The URL to a website providing the project\(cqs documentation for online browsing (string). .RE .sp \fBexecutables\fP .RS 4 A list of executables to be installed (sequence). .sp The executables can be of any type or language (e.g., shell, binary, ruby), must exist in the \fIbin\fP folder of the Shard, and have the executable bit set (on POSIX platforms). When installed as a dependency for another project the executables will be copied to the \fIbin\fP folder of that project. .sp Executables are always installed last, after the \fIpostinstall\fP script is run, so libraries can build the executables when they are installed by Shards. Installation can be disabled by passing the flag \fI\-\-skip\-executables\fP. .sp \fBExample:\fP .sp .if n .RS 4 .nf .fam C executables: \- micrate \- icr .fam .fi .if n .RE .RE .sp \fBhomepage\fP .RS 4 The URL of the project\(cqs homepage (string). .RE .sp \fBlibraries\fP .RS 4 A list of shared libraries the shard tries to link to (mapping). .sp This field is purely informational. It serves as a canonical way to discover non Crystal dependencies in shards, both for tools as well as humans. .sp A shard must only list libraries it directly links to, it must not include libraries that are only referenced by dependencies. It must include all libraries it directly links to, regardless of a dependency doing it too. .sp It should map from the soname without any extension, path or version, for example \fIlibsqlite3\fP for \fI/usr/lib/libsqlite3.so.0.8.6\fP, to a version constraint. .sp The version constraint has the following format: .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} It may be a version number. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} It may be \fI"*"\fP if any version will do. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} The version number may be prefixed by an operator: \fI<\fP, \fI<=\fP, \fI>\fP, \fI>=\fP, \fI!=\fP or \fI~>\fP. .RE .sp .if n .RS 4 .nf .fam C libraries: libQt5Gui: "*" libQt5Help: "~> 5.7" libQtBus: ">= 4.8" .fam .fi .if n .RE .RE .sp \fBlicense\fP .RS 4 An \c .URL "https://spdx.github.io/spdx\-spec/v3.0.1/annexes/spdx\-license\-expressions/" "SPDX license expression" or an URL to a license file (string, recommended). .sp The OSI publishes \c .URL "https://opensource.org/licenses\-old/category" "a list" "" of open source licenses and their corresponding SPDX identifiers. .sp Examples: \fIApache\-2.0\fP, \fIGPL\-3.0\-or\-later\fP, \fIApache\-2.0 OR MIT\fP, \fIApache\-2.0 WITH Swift\-exception\fP, \fI\c .URL "https://example.com/LICENSE" "" "\fP." .RE .sp \fBrepository\fP .RS 4 The URL of the project\(cqs canonical repository (string, recommended). .sp The URL should be compatible with typical VCS tools without modifications. \fIhttp\fP/\fIhttps\fP is preferred over VCS schemes like \fIgit\fP. It is recommended that this URL is publicly available. .sp Copies of a shard (such as mirrors, development forks etc.) should point to the same canonical repository address, even if hosted at different locations. .sp \fBExample:\fP .sp .if n .RS 4 .nf .fam C repository: "https://github.com/crystal\-lang/shards" .fam .fi .if n .RE .RE .sp \fBscripts\fP .RS 4 Script hooks to run. Only \fIpostinstall\fP is supported. .sp Shards may run scripts automatically after certain actions. The scripts themselves are mere shell commands. .sp \fBpostinstall\fP .RS 4 The \fIpostinstall\fP hook of a dependency will be run whenever that dependency is installed or upgraded in a project that requires it. This may be used to compile a C library, to build tools to help working on the project, or anything else. .sp The script will be run from the dependency\(cqs installation directory, for example \fIlib/foo\fP for a Shard named \fIfoo\fP. .sp \fBExample:\fP .sp .if n .RS 4 .nf .fam C scripts: postinstall: cd src/libfoo && make .fam .fi .if n .RE .RE .RE .sp \fBtargets\fP .RS 4 A list of targets to build (mapping). .sp Each target begins with the name of the target as a key (string), then a list of attributes (mapping). The target name is the built binary name, created in the \fIbin\fP folder of the project. .sp \fBExample:\fP .sp .if n .RS 4 .nf .fam C targets: server: main: src/server/cli.cr worker: main: src/worker.cr .fam .fi .if n .RE .sp The above example will build \fIbin/server\fP from \fIsrc/server/cli.cr\fP and \fIbin/worker\fP from \fIsrc/worker.cr\fP. .sp \fBmain\fP .RS 4 A path to the source file to compile (string). .RE .RE .SH "DEPENDENCY ATTRIBUTES" .sp Each dependency needs at least one attribute that defines the resolver for this dependency. Those can be \fIpath\fP, \fIgit\fP, \fIgithub\fP, \fIgitlab\fP, \fIbitbucket\fP, \fIcodeberg\fP. .sp \fBpath\fP .RS 4 A local path (string). .sp The library will be installed as a symlink to the local path. The \fIversion\fP attribute isn\(cqt required but will be used if present to validate the dependency. .RE .sp \fBgit\fP .RS 4 A Git repository URL (string). .sp The URL may be \c .URL "https://git\-scm.com/docs/git\-clone#_git_urls" "any protocol" supported by Git, which includes SSH, GIT and HTTPS. .sp The Git repository will be cloned, the list of versions (and associated \fIshard.yml\fP) will be extracted from Git tags (e.g., \fIv1.2.3\fP). .sp One of the other attributes (\fIversion\fP, \fItag\fP, \fIbranch\fP or \fIcommit\fP) is required. When missing, Shards will install the HEAD refs. .sp \fBExample:\fP \fIgit: git://git.example.org/crystal\-library.git\fP .RE .sp \fBgithub\fP .RS 4 GitHub repository URL as \fIuser/repo\fP (string) .sp Extends the \fIgit\fP resolver, and acts exactly like it. .sp \fBExample:\fP \fIgithub: ysbaddaden/minitest.cr\fP .RE .sp \fBgitlab\fP .RS 4 GitLab repository URL as \fIuser/repo\fP (string). .sp Extends the \fIgit\fP resolver, and acts exactly like it. .sp Only matches dependencies hosted on \fIgitlab.com\fP. For personal GitLab installations, you must use the generic \fIgit\fP resolver. .sp \fBExample:\fP \fIgitlab: thelonlyghost/minitest.cr\fP .RE .sp \fBbitbucket\fP .RS 4 Bitbucket repository URL as \fIuser/repo\fP (string). .sp Extends the \fIgit\fP resolver, and acts exactly like it. .sp \fBExample:\fP \fIbitbucket: tom/library\fP .RE .sp \fBcodeberg\fP .RS 4 Codeberg repository URL as \fIuser/repo\fP (string). .sp Extends the \fIgit\fP resolver, and acts exactly like it. .sp \fBExample:\fP \fIcodeberg: tom/library\fP .RE .sp \fBhg\fP .RS 4 A Mercurial repository URL (string). .sp The URL may be \c .URL "https://www.mercurial\-scm.org/repo/hg/help/clone" "any protocol" supported by Mercurial, which includes SSH and HTTPS. .sp The Mercurial repository will be cloned, the list of versions (and associated \fIshard.yml\fP) will be extracted from Mercurial tags (e.g., \fIv1.2.3\fP). .sp One of the other attributes (\fIversion\fP, \fItag\fP, \fIbranch\fP, \fIbookmark\fP or \fIcommit\fP) is required. When missing, Shards will install the \fI@\fP bookmark or \fItip\fP. .sp \fBExample:\fP \fIhg: \c .URL "https://hg.example.org/crystal\-library" "" "\fP" .RE .sp \fBfossil\fP .RS 4 A \c .URL "https://www.fossil\-scm.org" "Fossil" "" repository URL (string). .sp The URL may be \c .URL "https://fossil\-scm.org/home/help/clone" "any protocol" supported by Fossil, which includes SSH and HTTPS. .sp The Fossil repository will be cloned, the list of versions (and associated \fIshard.yml\fP) will be extracted from Fossil tags (e.g., \fIv1.2.3\fP). .sp One of the other attributes (\fIversion\fP, \fItag\fP, \fIbranch\fP, or \fIcommit\fP) is required. When missing, Shards will install \fItrunk\fP. .sp \fBExample:\fP \fIfossil: \c .URL "https://fossil.example.org/crystal\-library" "" "\fP" .RE .sp \fBversion\fP .RS 4 A version requirement (string). .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} It may be an explicit version number. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} It may be \fI"*"\fP wildcard if any version will do (this is the default). Shards will then install the latest tagged version (or HEAD if no tagged version available). .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} The version number may be prefixed by an operator: \fI<\fP, \fI<=\fP, \fI>\fP, \fI>=\fP, \fI!=\fP or \fI~>\fP. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} Multiple requirements can be separated by commas. .RE .sp Examples: \fI1.2.3\fP, \fI>= 1.0.0\fP, \fI>= 1.0.0, < 2.0\fP or \fI~> 2.0\fP. .sp Most of the version operators, like \fI>= 1.0.0\fP, are self\-explanatory, but the \fI~>\fP operator has a special meaning. It specifies a minimum version, but allows the last digit specified to go up, excluding the major release number: .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} \fI~> 0.3.5\fP is identical to \fI>= 0.3.5 and < 0.4.0\fP. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} \fI~> 2.0.3\fP is identical to \fI>= 2.0.3 and < 2.1\fP. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} \fI~> 2.1\fP is identical to \fI>= 2.1 and < 3.0\fP. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} \fI~> 0.3\fP is identical to \fI>= 0.3 and < 1.0\fP. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ . sp -1 . IP \(bu 2.3 .\} \fI~> 1\fP is identical to \fI>= 1.0 and < 2.0\fP. .RE .if n .sp .RS 4 .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 .B Note .ps -1 .br .sp Even though \fI2.1.0\-dev\fP is strictly before \fI2.1.0\fP, a version constraint like \fI~> 2.0.3\fP would not install it since only the \fI.3\fP can change but the \fI2.0\fP part is fixed. .sp .5v .RE .sp \fBbranch\fP .RS 4 Install the specified branch of a git dependency, or the named branch of a mercurial or fossil dependency (string). .RE .sp \fBcommit\fP .RS 4 Install the specified commit of a git, mercurial, or fossil dependency (string). .RE .sp \fBtag\fP .RS 4 Install the specified tag of a git, mercurial, or fossil dependency (string). .RE .sp \fBbookmark\fP .RS 4 Install the specified bookmark of a mercurial dependency (string). .RE .SH "EXAMPLE:" .sp Here is an example \fIshard.yml\fP for a library named \fIshards\fP at version \fI1.2.3\fP with some dependencies: .sp .if n .RS 4 .nf .fam C name: shards version: 1.2.3 crystal: \*(Aq>= 0.35.0\*(Aq authors: \- Julien Portalier license: MIT description: | Dependency manager for the Crystal Language dependencies: openssl: github: datanoise/openssl.cr branch: master development_dependencies: minitest: git: https://github.com/ysbaddaden/minitest.cr.git version: "~> 0.1.0" libraries: libgit2: ~> 0.24 scripts: postinstall: make ext targets: shards: main: src/shards.cr .fam .fi .if n .RE .SH "AUTHOR" .sp Written by Julien Portalier and the Crystal project. .SH "SEE ALSO" .sp \fBshards\fP(1)shards-0.19.0/man/shards.1000066400000000000000000000210311473060476400152160ustar00rootroot00000000000000'\" t .\" Title: shards .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.20 .\" Date: 2024-12-18 .\" Manual: Shards Manual .\" Source: shards 0.19.0 .\" Language: English .\" .TH "SHARDS" "1" "2024-12-18" "shards 0.19.0" "Shards Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 .nh .ad l .de URL \fI\\$2\fP <\\$1>\\$3 .. .als MTO URL .if \n[.g] \{\ . mso www.tmac . am URL . ad l . . . am MTO . ad l . . . LINKSTYLE blue R < > .\} .SH "NAME" shards \- dependency manager for the Crystal Language .SH "SYNOPSIS" .sp \fBshards\fP [\fI\fP...] [\fI\fP] [\fI\fP...] .SH "DESCRIPTION" .sp Manages dependencies for Crystal projects and libraries with reproducible installs across computers and systems. .SH "USAGE" .sp \fIshards\fP requires the presence of a \fIshard.yml\fP file in the project folder (working directory). This file describes the project and lists dependencies that are required to build it. See \fBshard.yml\fP(5) for more information on its format. A default file can be created by running \fIshards init\fP. .sp Running \fIshards install\fP resolves and installs the specified dependencies. The installed versions are written into a \fBshard.lock\fP file for using the exact same dependency versions when running \fIshards install\fP again. .sp If your shard builds an application, both \fBshard.yml\fP and \fBshard.lock\fP should be checked into version control to provide reproducible dependency installs. If it is only a library for other shards to depend on, \fBshard.lock\fP should \fInot\fP be checked in, only \fBshard.yml\fP. It’s good advice to add it to \fB.gitignore\fP. .SH "COMMANDS" .sp If no \fIcommand\fP is given, \fBinstall\fP command will be run by default. .sp To see the available options for a particular command, use \fI\-\-help\fP after the command. .sp \fBbuild\fP [\fI\fP] [\fI\fP...] .RS 4 Builds the specified \fI\fP in \fBbin\fP path. If no targets are specified, all are built. This command ensures all dependencies are installed, so it is not necessary to run \fBshards install\fP before. .sp All \fI\fP following the command are delegated to \fBcrystal build\fP. .RE .sp \fBcheck\fP .RS 4 Verifies that all dependencies are installed and requirements are satisfied. .sp Exit status: .sp \fB0\fP .RS 4 Dependencies are satisfied. .RE .sp \fB1\fP .RS 4 Dependencies are not satisfied. .RE .RE .sp \fBinit\fP .RS 4 Initializes a default \fIshard.yml\fP in the current folder. .RE .sp \fBinstall\fP [\-\-frozen] [\-\-without\-development] [\-\-production] [\-\-skip\-postinstall] [\-\-skip\-executables] [\-\-jobs=N] .RS 4 Resolves and installs dependencies into the \fIlib\fP folder. If not already present, generates a \fIshard.lock\fP file from resolved dependencies, locking version numbers or Git commits. .sp Reads and enforces locked versions and commits if a \fIshard.lock\fP file is present. The \fBinstall\fP command may fail if a locked version doesn\(cqt match a requirement, but may succeed if a new dependency was added, as long as it doesn\(cqt generate a conflict, thus generating a new \fIshard.lock\fP file. .sp \-\-frozen .RS 4 Strictly installs locked versions from \fIshard.lock\fP. Fails if \fIshard.lock\fP is missing. .RE .sp \-\-without\-development .RS 4 Does not install development dependencies. .RE .sp \-\-production .RS 4 same as \fI\-\-frozen\fP and \fI\-\-without\-development\fP .RE .sp \-\-skip\-postinstall .RS 4 Does not run postinstall of dependencies. .RE .sp \-\-skip\-executables .RS 4 Does not install executables. .RE .sp \-\-jobs .RS 4 Number of repository downloads to perform in parallel (default: 8). Currently only for git. .RE .RE .sp \fBlist\fP [\-\-tree] .RS 4 Lists the installed dependencies and their versions. .sp Specifying \fI\-\-tree\fP arranges nested dependencies in a tree, instead of a flattened list. .RE .sp \fBlock\fP [\-\-update [...]] .RS 4 Resolves dependencies and creates or updates the \fIshard.lock\fP file as per the \fBinstall\fP command, but never installs the dependencies. .sp Specifying \fI\-\-update\fP follows the same semantics as the \fBupdate\fP command. .RE .sp \fBoutdated\fP [\-\-pre] .RS 4 Lists dependencies that are outdated. .sp When \fI\-\-pre\fP is specified, pre\-release versions are also considered. .RE .sp \fBprune\fP .RS 4 Removes unused dependencies from \fIlib\fP folder. .RE .sp \fBupdate\fP [...] .RS 4 Resolves and updates all dependencies into the \fIlib\fP folder, whatever the locked versions in the \fIshard.lock\fP file. Eventually generates a new \fIshard.lock\fP file. .sp Specifying \fIshards\fP will update these dependencies only, trying to be as conservative as possible with other dependencies, respecting the locked versions in the \fIshard.lock\fP file. .RE .sp \fBversion\fP [\fI\fP] .RS 4 Prints the current version of the shard located at \fIpath\fP (defaults to current directory). .RE .sp To see the available options for a particular command, use \fB\-\-help\fP after a command. .SH "GENERAL OPTIONS" .sp \-\-version .RS 4 Prints the version of \fIshards\fP. .RE .sp \-h, \-\-help .RS 4 Prints usage synopsis. .RE .sp \-\-no\-color .RS 4 Disables colored output. .RE .sp \-\-local .RS 4 Do not update remote repository cache. Instead, Shards will use the local copies already present in the cache (see \fBSHARDS_CACHE_PATH\fP). The command will fail if a dependency is unavailable in the cache. .RE .sp \-q, \-\-quiet .RS 4 Decreases the log verbosity, printing only warnings and errors. .RE .sp \-v, \-\-verbose .RS 4 Increases the log verbosity, printing all debug statements. .RE .SH "INSTALLATION" .sp Shards is usually distributed with Crystal itself. Alternatively, a separate \fIshards\fP package may be available for your system. .sp To install from source, download or clone .URL "https://github.com/crystal\-lang/shards" "the repository" "" and run \fBmake CRFLAGS=\-\-release\fP. The compiled binary is in \fIbin/shards\fP and should be added to \fBPATH\fP. .SH "ENVIRONMENT VARIABLES" .sp SHARDS_OPTS .RS 4 Allows general options to be passed in as environment variable. \fBExample\fP: \fISHARDS_OPTS="\-\-no\-color" shards update\fP .RE .sp SHARDS_CACHE_PATH .RS 4 Defines the cache location. In this folder, shards stores local copies of remote repositories. Defaults to \fI.cache/shards\fP in the home directory (\fI$XDG_CACHE_HOME\fP or \fI$HOME\fP) or the current directory. .RE .sp SHARDS_INSTALL_PATH .RS 4 Defines the location where dependencies are installed. Defaults to \fIlib\fP. .RE .sp SHARDS_BIN_PATH .RS 4 Defines the location where executables are installed. Defaults to \fIbin\fP. .RE .sp CRYSTAL_VERSION .RS 4 Defines the crystal version that dependencies should be resolved against. Defaults to the output of \fIcrystal env CRYSTAL_VERSION\fP. .RE .sp SHARDS_OVERRIDE .RS 4 Defines an alternate location of \fIshard.override.yml\fP. .RE .SH "FILES" .sp shard.yml .RS 4 Describes a shard project including its dependencies. See \fBshard.yml\fP(5) for documentation. .RE .sp shard.override.yml .RS 4 Allows overriding the source and restriction of dependencies. An alternative location can be configured with the env var \fBSHARDS_OVERRIDE\fP. .sp The file contains a YAML document with a single \fBdependencies\fP key. It has the same semantics as in \fBshard.yml\fP. Dependency configuration takes precedence over the configuration in \fBshard.yml\fP or any dependency\(cqs \fBshard.yml\fP. .sp Use cases are local working copies, forcing a specific dependency version despite mismatching constraints, fixing a dependency, checking compatibility with unreleased dependency versions. .RE .sp shard.lock .RS 4 Lockfile that stores information about the installed versions. .sp If your shard builds an application, \fBshard.lock\fP should be checked into version control to provide reproducible dependency installs. .sp If it is only a library for other shards to depend on, \fBshard.lock\fP should \fInot\fP be checked in, only \fBshard.yml\fP. It’s good advice to add it to \fB.gitignore\fP. .RE .SH "REPORTING BUGS" .sp Report shards bugs to \c .URL "https://github.com/crystal\-lang/shards/issues" "" "" .sp Crystal Language home page: \c .URL "https://crystal\-lang.org" "" "" .SH "COPYRIGHT" .sp Copyright © 2024 Julien Portalier. .sp .URL "http://www.apache.org/licenses/LICENSE\-2.0" "License Apache 2.0" "" .sp This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. .SH "AUTHORS" .sp Written by Julien Portalier and the Crystal project. .SH "SEE ALSO" .sp \fBshard.yml\fP(5)shards-0.19.0/renovate.json000066400000000000000000000004671473060476400156250ustar00rootroot00000000000000{ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:base" ], "separateMajorMinor": false, "packageRules": [ { "groupName": "GH Actions", "matchManagers": ["github-actions"], "schedule": ["after 5am and before 8am on Wednesday"] } ] } shards-0.19.0/shard.lock000066400000000000000000000001651473060476400150550ustar00rootroot00000000000000version: 2.0 shards: molinillo: git: https://github.com/crystal-lang/crystal-molinillo.git version: 0.2.0 shards-0.19.0/shard.yml000066400000000000000000000007271473060476400147320ustar00rootroot00000000000000name: shards version: 0.19.0 description: | Resolves, installs and updates project dependencies reproducibly from source repositories. authors: - Julien Portalier documentation: https://crystal-lang.org/reference/man/shards repository: https://github.com/crystal-lang/shards dependencies: molinillo: github: crystal-lang/crystal-molinillo targets: shards: main: src/shards.cr crystal: ">= 0.35.0, < 2.0.0" license: Apache-2.0 shards-0.19.0/spec/000077500000000000000000000000001473060476400140325ustar00rootroot00000000000000shards-0.19.0/spec/integration/000077500000000000000000000000001473060476400163555ustar00rootroot00000000000000shards-0.19.0/spec/integration/build_spec.cr000066400000000000000000000061551473060476400210230ustar00rootroot00000000000000require "./spec_helper" private def bin_path(name) File.join(application_path, "bin", Shards::Helpers.exe(name)) end describe "build" do before_each do Dir.mkdir(File.join(application_path, "src")) File.write(File.join(application_path, "src", "cli.cr"), "puts __FILE__") Dir.mkdir(File.join(application_path, "src", "commands")) File.write(File.join(application_path, "src", "commands", "check.cr"), "puts __LINE__") File.write File.join(application_path, "shard.yml"), <<-YAML name: build version: 0.1.0 targets: app: main: src/cli.cr alt: main: src/cli.cr check: main: src/commands/check.cr YAML end it "builds all targets" do Dir.cd(application_path) do run "shards build --no-color" File.exists?(bin_path("app")).should be_true File.exists?(bin_path("alt")).should be_true File.exists?(bin_path("check")).should be_true `#{Process.quote(bin_path("app"))}`.chomp.should eq(File.join(application_path, "src", "cli.cr")) `#{Process.quote(bin_path("alt"))}`.chomp.should eq(File.join(application_path, "src", "cli.cr")) `#{Process.quote(bin_path("check"))}`.chomp.should eq("1") end end it "builds specified targets" do Dir.cd(application_path) do run "shards build --no-color alt check" File.exists?(bin_path("app")).should be_false File.exists?(bin_path("alt")).should be_true File.exists?(bin_path("check")).should be_true end end it "fails to build unknown target" do Dir.cd(application_path) do ex = expect_raises(FailedCommand) do run "shards build --no-color app unknown check" end ex.stdout.should contain("target unknown was not found") File.exists?(bin_path("app")).should be_true File.exists?(bin_path("check")).should be_false end end it "reports error when target failed to compile" do File.write File.join(application_path, "src", "cli.cr"), "a = ......" Dir.cd(application_path) do ex = expect_raises(FailedCommand) do run "shards build --no-color app" end ex.stdout.should contain("target app failed to compile") ex.stdout.should match(/unexpected token: "?.../) File.exists?(bin_path("app")).should be_false end end {% unless flag?(:win32) %} it "reports warning without failing" do File.write File.join(application_path, "src", "cli.cr"), <<-CODE @[Deprecated] def a end a CODE Dir.cd(application_path) do err = run "shards build --no-color app", clear_env: true err.should match(/eprecated/) File.exists?(bin_path("app")).should be_true end end {% end %} it "errors when no targets defined" do File.write File.join(application_path, "shard.yml"), <<-YAML name: build version: 0.1.0 YAML Dir.cd(application_path) do ex = expect_raises(FailedCommand) do run "shards build --no-color" end ex.stdout.should contain("Targets not defined in shard.yml") File.exists?(bin_path("")).should be_false end end end shards-0.19.0/spec/integration/check_spec.cr000066400000000000000000000056071473060476400210020ustar00rootroot00000000000000require "./spec_helper" describe "check" do it "succeeds when all dependencies are installed" do metadata = { dependencies: {web: "*", orm: "*"}, development_dependencies: {mock: "*"}, } with_shard(metadata) do run "shards install" run "shards check" end end it "succeeds when dependencies match loose requirements" do with_shard({dependencies: {web: "1.2.0"}}) do run "shards install" end with_shard({dependencies: {web: "~> 1.1"}}) do run "shards check" end end it "fails without lockfile" do with_shard({dependencies: {web: "*"}}) do ex = expect_raises(FailedCommand) { run "shards check --no-color" } ex.stdout.should contain("Missing #{Shards::LOCK_FILENAME}") ex.stderr.should be_empty end end it "succeeds without dependencies and lockfile" do with_shard({name: "no_dependencies"}) do run "shards check --no-color" end end it "fails when dependencies are missing" do with_shard({dependencies: {web: "*"}}) do run "shards install" end metadata = { dependencies: {web: "*", orm: "*"}, development_dependencies: {mock: "*"}, } with_shard(metadata) do ex = expect_raises(FailedCommand) { run "shards check --no-color" } ex.stdout.should contain("Dependencies aren't satisfied") ex.stderr.should be_empty end end it "fails when wrong versions are installed" do with_shard({dependencies: {web: "1.0.0"}}) do run "shards install" end with_shard({dependencies: {web: "2.0.0"}}) do ex = expect_raises(FailedCommand) { run "shards check --no-color" } ex.stdout.should contain("Dependencies aren't satisfied") ex.stderr.should be_empty end end it "succeeds when shard.yml version doesn't match git tag" do metadata = { dependencies: { version_mismatch: {git: git_url(:version_mismatch), version: "0.2.0"}, }, } with_shard(metadata) do run "shards install" run "shards check" end end it "fails when another source was installed" do with_shard({dependencies: {awesome: "0.1.0"}}) do run "shards install" end with_shard({dependencies: {awesome: {git: git_url(:forked_awesome)}}}) do ex = expect_raises(FailedCommand) { run "shards check --no-color" } ex.stdout.should contain("Dependencies aren't satisfied") ex.stderr.should be_empty end end it "fails when override changes version to use" do metadata = {dependencies: {awesome: "0.1.0"}} with_shard(metadata) do run "shards install" end override = {dependencies: {awesome: "0.2.0"}} with_shard(metadata, nil, override) do ex = expect_raises(FailedCommand) { run "shards check --no-color" } ex.stdout.should contain("Dependencies aren't satisfied") ex.stderr.should be_empty end end end shards-0.19.0/spec/integration/init_spec.cr000066400000000000000000000013631473060476400206630ustar00rootroot00000000000000require "./spec_helper" private def shard_path File.join(application_path, Shards::SPEC_FILENAME) end describe "init" do it "creates shard.yml" do Dir.cd(application_path) do run "shards init" File.exists?(File.join(application_path, Shards::SPEC_FILENAME)).should be_true spec = Shards::Spec.from_file(shard_path) spec.name.should eq("integration") spec.version.should eq(version "0.1.0") end end it "won't overwrite shard.yml" do Dir.cd(application_path) do File.write(shard_path, "") ex = expect_raises(FailedCommand) { run "shards init --no-color" } ex.stdout.should contain("#{Shards::SPEC_FILENAME} already exists") File.read(shard_path).should be_empty end end end shards-0.19.0/spec/integration/install_spec.cr000066400000000000000000001254061473060476400213730ustar00rootroot00000000000000require "./spec_helper" describe "install" do it "installs dependencies" do metadata = { dependencies: {web: "*", orm: "*", foo: {path: rel_path(:foo)}}, development_dependencies: {mock: "*"}, } with_shard(metadata) do run "shards install" # it installed dependencies (recursively) assert_installed "web", "2.1.0" assert_installed "orm", "0.5.0" assert_installed "pg", "0.2.1" # it installed the path dependency assert_installed "foo", "0.1.0" # it installed development dependencies (recursively, except their # development dependencies) assert_installed "mock" assert_installed "shoulda", "0.1.0" refute_installed "minitest" # it didn't install custom dependencies refute_installed "release" # it locked dependencies assert_locked "web", "2.1.0" assert_locked "orm", "0.5.0" assert_locked "pg", "0.2.1" # it locked development dependencies (not recursively) assert_locked "mock", "0.1.0" refute_locked "minitest" # it didn't lock custom dependencies refute_locked "release" end end it "resolves intersection" do metadata = {dependencies: {web: ">= 1.1.0, < 2.0"}} with_shard(metadata) do run "shards install" assert_installed "web", "1.2.0" end end it "fails when spec is missing" do Dir.cd(application_path) do ex = expect_raises(FailedCommand) { run "shards install --no-color" } ex.stdout.should contain("Missing #{Shards::SPEC_FILENAME}") ex.stdout.should contain("Please run 'shards init'") end end it "reinstall if info file is missing" do metadata = {dependencies: {web: "*"}} with_shard(metadata) do run "shards install" File.delete "#{Shards::INSTALL_DIR}/.shards.info" File.touch "#{Shards::INSTALL_DIR}/web/foo.txt" run "shards install" File.exists?("#{Shards::INSTALL_DIR}/web/foo.txt").should be_false assert_installed "web", "2.1.0" end end it "reinstall if info file is missing (path resolver)" do metadata = {dependencies: {web: {path: rel_path(:web)}}} with_shard(metadata) do run "shards install" File.delete "#{Shards::INSTALL_DIR}/.shards.info" run "shards install" assert_installed "web", "2.1.0" end end it "deletes old .sha1 files" do metadata = {dependencies: {web: "*"}} with_shard(metadata) do Dir.mkdir_p(Shards::INSTALL_DIR) File.touch("#{Shards::INSTALL_DIR}/web.sha1") run "shards install" File.exists?("#{Shards::INSTALL_DIR}/web.sha1").should be_false end end it "won't install prerelease version" do metadata = { dependencies: {unstable: "*"}, } with_shard(metadata) do run "shards install" assert_installed "unstable", "0.2.0" assert_locked "unstable", "0.2.0" end end it "installs specified prerelease version" do metadata = { dependencies: {unstable: "0.3.0.alpha"}, } with_shard(metadata) do run "shards install" assert_installed "unstable", "0.3.0.alpha" assert_locked "unstable", "0.3.0.alpha" end end it "installs prerelease version at refs" do metadata = { dependencies: { unstable: {git: git_url(:unstable), branch: "master"}, }, } with_shard(metadata) do run "shards install" assert_installed "unstable", "0.3.0.beta", git: git_commits(:unstable).first end end it "installs dependencies at locked version" do metadata = { dependencies: {web: "1.0.0"}, development_dependencies: {minitest: "~> 0.1.2"}, } lock = {web: "1.0.0", minitest: "0.1.2"} with_shard(metadata, lock) do run "shards install" assert_installed "web", "1.0.0" assert_locked "web", "1.0.0" assert_installed "minitest", "0.1.2" assert_locked "minitest", "0.1.2" end end it "always installs locked versions" do metadata = {dependencies: {minitest: "0.1.0"}} lock = {minitest: "0.1.0"} with_shard(metadata, lock) do run "shards install" assert_installed "minitest", "0.1.0" assert_locked "minitest", "0.1.0" end metadata = {dependencies: {minitest: "0.1.2"}} lock = {minitest: "0.1.2"} with_shard(metadata, lock) do run "shards install" assert_installed "minitest", "0.1.2" assert_locked "minitest", "0.1.2" end end it "resolves dependency at head when no version tags" do metadata = {dependencies: {"missing": "*"}} with_shard(metadata) { run "shards install" } assert_installed "missing", "0.1.0", git: git_commits(:missing).first end it "install specific commit" do metadata = {dependencies: {"web": {git: git_url(:web), commit: git_commits(:web)[2]}}} with_shard(metadata) { run "shards install" } assert_installed "web", "1.2.0", git: git_commits(:web)[2] end it "install specific abbreviated commit" do metadata = {dependencies: {"web": {git: git_url(:web), commit: git_commits(:web)[2][0...5]}}} with_shard(metadata) { debug "shards install" } assert_installed "web", "1.2.0", git: git_commits(:web)[2] end it "installs dependency at locked commit when refs is a branch" do metadata = { dependencies: { web: {git: git_url(:web), branch: "master"}, }, } lock = {web: "1.2.0+git.commit.#{git_commits(:web)[-5]}"} with_shard(metadata, lock) do run "shards install" assert_installed "web", "1.2.0", git: git_commits(:web)[-5] end end it "installs dependency at locked commit when refs is a version tag" do metadata = { dependencies: {web: {git: git_url(:web), tag: "v1.1.1"}}, } lock = {web: "1.1.1+git.commit.#{git_commits(:web)[-3]}"} with_shard(metadata, lock) do run "shards install" assert_installed "web", "1.1.1", git: git_commits(:web)[-3] end end it "resolves dependency spec at locked commit" do create_git_repository "locked" create_git_release "locked", "0.1.0" create_git_release "locked", "0.2.0", {dependencies: {pg: {git: git_url(:pg)}}} metadata = { dependencies: { "locked": {git: git_url(:locked), branch: "master"}, }, } lock = { "locked": "0.1.0+git.commit.#{git_commits(:locked).last}", } with_shard(metadata, lock) { run "shards install" } assert_installed "locked", "0.1.0", git: git_commits(:locked).last refute_installed "pg" end it "updates locked commit" do metadata = { dependencies: {web: {git: git_url(:web), branch: "master"}}, } with_shard(metadata, {web: "1.2.0+git.commit.#{git_commits(:web)[-5]}"}) do run "shards install" assert_installed "web", "1.2.0", git: git_commits(:web)[-5] end with_shard(metadata, {web: "2.1.0+git.commit.#{git_commits(:web)[0]}"}) do run "shards install" assert_installed "web", "2.1.0", git: git_commits(:web)[0] end end it "updates locked commit when switching from locked version to branch" do metadata = { dependencies: { web: {git: git_url(:web), branch: "master"}, }, } lock = {web: "1.2.0"} expected_commit = git_commits(:web).first with_shard(metadata, lock) do run "shards install" assert_installed "web", "2.1.0", git: expected_commit assert_locked "web", "2.1.0+git.commit.#{expected_commit}" end end pending "updates locked commit when switching between branches (if locked commit is not reachable)" it "updates when dependency requirement changed" do metadata = {dependencies: {web: "2.0.0"}} lock = {web: "1.0.0"} with_shard(metadata, lock) do run "shards install" assert_installed "web", "2.0.0" assert_locked "web", "2.0.0" end end it "keeps installed version if possible when dependency source changed" do metadata = {dependencies: {awesome: {git: git_url(:forked_awesome)}}} lock = {awesome: "0.1.0"} with_shard(metadata, lock) do assert_locked "awesome", "0.1.0", source: {git: git_url(:awesome)} output = run "shards install --no-color" assert_locked "awesome", "0.1.0", source: {git: git_url(:forked_awesome)} assert_installed "awesome", "0.1.0", source: {git: git_url(:forked_awesome)} output.should contain("Ignoring source of \"awesome\" on shard.lock") end end it "keeps nested dependencies locked when main dependency source changed" do metadata = {dependencies: {awesome: {git: git_url(:forked_awesome)}}} lock = {awesome: "0.1.0", d: "0.1.0"} with_shard(metadata, lock) do assert_locked "awesome", "0.1.0", source: {git: git_url(:awesome)} assert_locked "d", "0.1.0", source: {git: git_url(:d)} output = run "shards install --no-color" assert_locked "awesome", "0.1.0", source: {git: git_url(:forked_awesome)} assert_locked "d", "0.1.0", source: {git: git_url(:d)} assert_installed "awesome", "0.1.0", source: {git: git_url(:forked_awesome)} assert_installed "d", "0.1.0", source: {git: git_url(:d)} output.should contain("Ignoring source of \"awesome\" on shard.lock") end end it "reinstall when resolver changes" do metadata = {dependencies: {web: {git: git_url(:web)}}} with_shard(metadata) do run "shards install" assert_locked "web", "2.1.0" end metadata = {dependencies: {web: {path: rel_path(:web)}}} with_shard(metadata) do run "shards install" assert_locked "web", "2.1.0", source: {path: rel_path(:web)} assert_installed "web", "2.1.0", source: {path: rel_path(:web)} end metadata = {dependencies: {web: {git: git_url(:web)}}} with_shard(metadata) do run "shards install" assert_locked "web", "2.1.0", source: {git: git_url(:web)} assert_installed "web", "2.1.0", source: {git: git_url(:web)} end end it "install subdependency of new dependency respecting lock" do metadata = {dependencies: {c: "*", d: "*"}} lock = {d: "0.1.0"} with_shard(metadata, lock) do run "shards install" assert_installed "c", "0.1.0" assert_installed "d", "0.1.0" end end it "installs and updates lockfile for added dependencies" do metadata = { dependencies: { web: "~> 1.0.0", orm: "*", }, } lock = {web: "1.0.0"} with_shard(metadata, lock) do run "shards install" assert_installed "web", "1.0.0" assert_locked "web", "1.0.0" assert_installed "orm", "0.5.0" assert_locked "orm", "0.5.0" end end it "updated lockfile on removed dependencies" do metadata = {dependencies: {web: "~> 1.0.0"}} lock = {web: "1.0.0", orm: "0.5.0"} with_shard(metadata, lock) do run "shards install" assert_installed "web", "1.0.0" assert_locked "web", "1.0.0" refute_installed "orm", "0.5.0" refute_locked "orm", "0.5.0" end end it "locks commit when installing git refs" do metadata = {dependencies: {web: {git: git_url(:web), branch: "master"}}} with_shard(metadata) do run "shards install" assert_locked "web", "2.1.0", git: git_commits(:web).first end end it "upgrade lock file from 1.0" do metadata = {dependencies: {web: "*"}} with_shard(metadata) do File.write "shard.lock", YAML.dump({ version: "1.0", shards: {web: {git: git_url(:web), commit: git_commits(:web).first}}, }) run "shards install" Shards::Lock.from_file("shard.lock").version.should eq(Shards::Lock::CURRENT_VERSION) assert_locked "web", "2.1.0", git: git_commits(:web).first end end ["frozen", "production"].each do |flag| describe "with --#{flag}" do it "fails if shard.lock and shard.yml has different sources" do # The sources will not match, so the .lock is not valid regarding the specs metadata = {dependencies: {awesome: {git: git_url(:forked_awesome)}}} lock = {awesome: "0.1.0", d: "0.1.0"} with_shard(metadata, lock) do assert_locked "awesome", "0.1.0", source: {git: git_url(:awesome)} ex = expect_raises(FailedCommand) { run "shards install --#{flag} --no-color" } ex.stdout.should contain("Outdated shard.lock (awesome source changed)") ex.stderr.should be_empty end end it "fails if shard.lock is missing" do metadata = {dependencies: {web: "*"}} with_shard(metadata) do ex = expect_raises(FailedCommand) { run "shards install --#{flag} --no-color" } ex.stdout.should contain("Missing shard.lock") ex.stderr.should be_empty end end it "fails if locked version is not available in source" do metadata = {dependencies: {awesome: {git: git_url(:awesome)}}} lock = {awesome: "0.1.0.git.commit.1234567890"} with_shard(metadata, lock) do assert_locked "awesome", "0.1.0.git.commit.1234567890", source: {git: git_url(:awesome)} ex = expect_raises(FailedCommand) { run "shards install --#{flag} --no-color" } ex.stdout.should contain("Locked version 0.1.0.git.commit.1234567890 for awesome was not found in git: #{git_url(:awesome)}") ex.stdout.should contain("Please run `shards update`") ex.stderr.should be_empty end end it "fails if shard.lock and shard.yml has different sources with incompatible versions." do # User should use update command in this scenario # forked_awesome does not have a 0.3.0 # awesome has 0.3.0 metadata = {dependencies: {awesome: {git: git_url(:forked_awesome)}}} lock = {awesome: "0.3.0"} with_shard(metadata, lock) do assert_locked "awesome", "0.3.0", source: {git: git_url(:awesome)} ex = expect_raises(FailedCommand) { run "shards install --#{flag} --no-color" } ex.stdout.should contain("Locked version 0.3.0 for awesome was not found in git: #{git_url(:forked_awesome)} (locked source is git: #{git_url(:awesome)})") ex.stdout.should contain("Please run `shards update`") ex.stderr.should be_empty end end it "fails to install when dependency requirement changed" do metadata = {dependencies: {web: "2.0.0"}} lock = {web: "1.0.0"} with_shard(metadata, lock) do ex = expect_raises(FailedCommand) { run "shards install --no-color --#{flag}" } ex.stdout.should contain("Outdated shard.lock") ex.stderr.should be_empty refute_installed "web" end end it "fails to install when dependency requirement (commit) changed" do metadata = {dependencies: {inprogress: {git: git_url(:inprogress), commit: git_commits(:inprogress)[1]}}} lock = {inprogress: "0.1.0+git.commit.#{git_commits(:inprogress).first}"} with_shard(metadata, lock) do ex = expect_raises(FailedCommand) { run "shards install --no-color --#{flag}" } ex.stdout.should contain("Outdated shard.lock") refute_installed "inprogress" end end it "doesn't install new dependencies" do metadata = { dependencies: { web: "~> 1.0.0", orm: "*", }, } lock = {web: "1.0.0"} with_shard(metadata, lock) do ex = expect_raises(FailedCommand) { run "shards install --#{flag} --no-color" } ex.stdout.should contain("Outdated shard.lock") ex.stderr.should be_empty end end it "install" do metadata = {dependencies: {web: "*"}} lock = {web: "1.0.0"} with_shard(metadata, lock) do run "shards install --#{flag}" assert_installed "web", "1.0.0" end end it "install with locked commit" do metadata = {dependencies: {web: "*"}} web_version = "2.1.0+git.commit.#{git_commits(:web).first}" lock = {web: web_version} with_shard(metadata, lock) do run "shards install --#{flag}" assert_installed "web", "2.1.0", git: git_commits(:web).first end end it "install with locked commit by a previous shards version" do metadata = {dependencies: {web: "*"}} with_shard(metadata) do File.write "shard.lock", {version: "1.0", shards: {web: {git: git_url(:web), commit: git_commits(:web).first}}} run "shards install --#{flag}" assert_installed "web", "2.1.0", git: git_commits(:web).first end end it "fails if lock is not up to date with override in main dependency" do metadata = {dependencies: { awesome: "*", }} lock = {awesome: "0.1.0", d: "0.1.0"} override = {dependencies: { awesome: {git: git_url(:forked_awesome), branch: "feature/super"}, }} expected_commit = git_commits(:forked_awesome).first with_shard(metadata, lock, override) do ex = expect_raises(FailedCommand) { run "shards install --no-color --#{flag}" } ex.stdout.should contain("Outdated shard.lock") ex.stderr.should be_empty refute_installed "awesome" end end it "fails if lock is not up to date with override in nested dependency" do metadata = {dependencies: { intermediate: "*", }} lock = {intermediate: "0.1.0", awesome: "0.1.0", d: "0.1.0"} override = {dependencies: { awesome: {git: git_url(:forked_awesome), branch: "feature/super"}, }} expected_commit = git_commits(:forked_awesome).first with_shard(metadata, lock, override) do ex = expect_raises(FailedCommand) { run "shards install --no-color --#{flag}" } ex.stdout.should contain("Outdated shard.lock") ex.stderr.should be_empty refute_installed "awesome" end end end end describe "with --without-development" do it "doesn't install development dependencies" do metadata = { dependencies: {web: "*", orm: "*"}, development_dependencies: {mock: "*"}, } with_shard(metadata) do File.exists?(File.join(application_path, "shard.lock")).should be_false run "shards install --without-development" # it installed dependencies (recursively) assert_installed "web" assert_installed "orm" # it didn't install development dependencies refute_installed "mock" refute_installed "minitest" File.exists?(File.join(application_path, "shard.lock")).should be_true end end end describe "with --production" do it "doesn't install development dependencies" do metadata = { dependencies: {web: "*", orm: "*"}, development_dependencies: {mock: "*"}, } # --production requires a lock file because it implies --frozen lock = {web: "1.0.0", orm: "0.3.0"} with_shard(metadata, lock) do run "shards install --production" # it installed dependencies (recursively) assert_installed "web" assert_installed "orm" # it didn't install development dependencies refute_installed "mock" refute_installed "minitest" end end end it "generates lockfile when project has no dependencies" do with_shard({name: "test"}) do run "shards install" lockfile = File.join(application_path, "shard.lock") File.exists?(lockfile).should be_true File.read(lockfile).should eq <<-YAML version: 2.0 shards: {} YAML end end it "touches lockfile if no new dependencies are installed" do metadata = {dependencies: {d: "*", c: "*"}} with_shard(metadata) do run "shards install" File.touch "shard.lock", Time.utc(1901, 12, 14) mtime = File.info("shard.lock").modification_time run "shards install" File.info("shard.lock").modification_time.should be >= mtime end end it "updates lockfile on completely removed dependencies" do metadata = NamedTuple.new lock = {web: "1.0.0"} with_shard(metadata, lock) do run "shards install" refute_installed "web" refute_locked "web" end end it "updates lockfile when there are no dependencies" do with_shard({name: "empty"}) do run "shards install" mtime = File.info("shard.lock").modification_time run "shards install" File.info("shard.lock").modification_time.should be >= mtime Shards::Lock.from_file("shard.lock").version.should eq(Shards::Lock::CURRENT_VERSION) end end it "creates ./lib/ when there are no dependencies" do with_shard({name: "empty"}) do File.exists?("./lib/").should be_false run "shards install" File.directory?("./lib/").should be_true end end it "runs postinstall script" do with_shard({dependencies: {post: "*"}}) do output = run "shards install --no-color" File.exists?(install_path("post", "made.txt")).should be_true output.should contain("Postinstall of post: make\n") end end it "can skip postinstall script" do with_shard({dependencies: {post: "*"}}) do output = run "shards install --no-color --skip-postinstall" File.exists?(install_path("post", "made.txt")).should be_false output.should contain("Postinstall of post: make (skipped)") end end {% if flag?(:win32) %} # Crystal bug in handling a failing subprocess pending "prints details and removes dependency when postinstall script fails" {% else %} it "prints details and removes dependency when postinstall script fails" do with_shard({dependencies: {fails: "*"}}) do ex = expect_raises(FailedCommand) { run "shards install --no-color" } ex.stdout.should contain("E: Failed postinstall of fails on make:\n") ex.stdout.should contain("test -n ''\n") Dir.exists?(install_path("fails")).should be_false end end {% end %} it "runs postinstall with transitive dependencies" do with_shard({dependencies: {transitive: "*"}}) do run "shards install" binary = install_path("transitive", Shards::Helpers.exe("version")) File.exists?(binary).should be_true `#{Process.quote(binary)}`.chomp.should eq("version @ 0.1.0") end end it "runs install and postinstall in reverse topological order" do with_shard({dependencies: {transitive_2: "*"}}) do output = run "shards install --no-color" install_lines = output.lines.select /^\w: (Installing|Postinstall)/ install_lines[0].should match(/Installing version /) install_lines[1].should match(/Installing transitive /) install_lines[2].should match(/Postinstall of transitive:/) install_lines[3].should match(/Installing transitive_2 /) install_lines[4].should match(/Postinstall of transitive_2:/) end end it "fails with circular dependencies" do create_git_repository "a" create_git_release "a", "0.1.0", {dependencies: {b: {git: git_path("b")}}} create_git_repository "b" create_git_release "b", "0.1.0", {dependencies: {a: {git: git_path("a")}}} with_shard({dependencies: {a: "*"}}) do ex = expect_raises(FailedCommand) { run "shards install --no-color" } ex.stdout.should contain("There is a circular dependency between a and b") end end it "fails when shard name doesn't match" do metadata = { dependencies: { typo: {git: git_url(:mock), version: "*"}, }, } with_shard(metadata) do ex = expect_raises(FailedCommand) { run "shards install --no-color" } ex.stdout.should contain("Error shard name (mock) doesn't match dependency name (typo)") end end it "warns when shard.yml version doesn't match git tag" do metadata = { dependencies: { version_mismatch: {git: git_url(:version_mismatch), version: "0.2.0"}, }, } with_shard(metadata) do stdout = run "shards install --no-color" stdout.should contain("W: Shard \"version_mismatch\" version (0.1.0) doesn't match tag version (0.2.0)") assert_installed "version_mismatch" end end it "doesn't warn when version mismatch is fixed" do metadata = { dependencies: { version_mismatch: {git: git_url(:version_mismatch), version: "0.2.1"}, }, } with_shard(metadata) do stdout = run "shards install --no-color" stdout.should_not contain("doesn't match tag version") assert_installed "version_mismatch", "0.2.1" end end it "test install old with version when shard was renamed" do metadata = { dependencies: { old_name: {git: git_url(:renamed), version: "0.1.0"}, }, } with_shard(metadata) do run "shards install" assert_installed "old_name", "0.1.0" end end it "test install new when shard was renamed" do metadata = { dependencies: { new_name: {git: git_url(:renamed)}, }, } with_shard(metadata) do run "shards install" assert_installed "new_name", "0.2.0" end end it "fail install old version when shard was renamed" do metadata = { dependencies: { new_name: {git: git_url(:renamed), version: "0.1.0"}, }, } with_shard(metadata) do ex = expect_raises(FailedCommand) { run "shards install --no-color" } ex.stdout.should contain("Error shard name (old_name) doesn't match dependency name (new_name)") end end it "fail install new version when shard was renamed" do metadata = { dependencies: { old_name: {git: git_url(:renamed), version: "0.2.0"}, }, } with_shard(metadata) do ex = expect_raises(FailedCommand) { run "shards install --no-color" } ex.stdout.should contain("Error shard name (new_name) doesn't match dependency name (old_name)") end end it "install untagged version when shard was renamed" do metadata = { dependencies: { another_name: {git: git_url(:renamed), branch: "master"}, }, } with_shard(metadata) do run "shards install" assert_installed "another_name", "0.3.0", git: git_commits(:renamed).first end end it "installs executables at version" do metadata = { dependencies: {binary: "0.1.0"}, } with_shard(metadata) { run("shards install --no-color") } foobar = File.join(application_path, "bin", Shards::Helpers.exe("foobar")) baz = File.join(application_path, "bin", Shards::Helpers.exe("baz")) foo = File.join(application_path, "bin", Shards::Helpers.exe("foo")) crystal = File.join(application_path, "bin", "crystal.cr") File.exists?(foobar).should be_true # "Expected to have installed bin/foobar executable" File.exists?(baz).should be_true # "Expected to have installed bin/baz executable" File.exists?(foo).should be_false # "Expected not to have installed bin/foo executable" File.exists?(crystal).should be_true `#{Process.quote(foobar)}`.should eq("OK") `#{Process.quote(baz)}`.should eq("KO") File.read(crystal).should eq %(puts "crystal") end it "errors on missing executable" do metadata = { dependencies: {"executable_missing": "*"}, } with_shard(metadata) do ex = expect_raises(FailedCommand) { run "shards install --no-color" } ex.stdout.should contain <<-ERROR E: Could not find executable "nonexistent" ERROR end end it "skips installing executables" do metadata = { dependencies: {binary: "0.1.0"}, } with_shard(metadata) { run("shards install --no-color --skip-executables") } foobar = File.join(application_path, "bin", Shards::Helpers.exe("foobar")) baz = File.join(application_path, "bin", Shards::Helpers.exe("baz")) foo = File.join(application_path, "bin", Shards::Helpers.exe("foo")) File.exists?(foobar).should be_false File.exists?(baz).should be_false File.exists?(foo).should be_false end it "installs executables at refs" do metadata = { dependencies: { binary: {git: git_url(:binary), commit: git_commits(:binary)[-1]}, }, } with_shard(metadata) { run("shards install --no-color") } foobar = File.join(application_path, "bin", Shards::Helpers.exe("foobar")) baz = File.join(application_path, "bin", Shards::Helpers.exe("baz")) foo = File.join(application_path, "bin", Shards::Helpers.exe("foo")) File.exists?(foobar).should be_true # "Expected to have installed bin/foobar executable" File.exists?(baz).should be_true # "Expected to have installed bin/baz executable" File.exists?(foo).should be_false # "Expected not to have installed bin/foo executable" end it "shows conflict message" do metadata = { dependencies: { c: "~> 0.1.0", d: ">= 0.2.0", }, } with_shard(metadata) do ex = expect_raises(FailedCommand) { run "shards install --no-color" } ex.stdout.should contain <<-ERROR E: Unable to satisfy the following requirements: - `d (>= 0.2.0)` required by `shard.yml` - `d (0.1.0)` required by `c 0.1.0` ERROR end end it "installs dependency with shard.yml created in latest version" do metadata = {dependencies: {noshardyml: "*"}} with_shard(metadata) do run "shards install" assert_installed "noshardyml", "0.2.0" end end describe "shows missing shard.yml in debug info" do it "git" do metadata = {dependencies: {noshardyml: "*"}} with_shard(metadata) do stdout = run "shards install --no-color -v" assert_installed "noshardyml", "0.2.0" stdout.should contain(%(D: Missing "shard.yml" for "noshardyml" at tag v0.1.0)) end end it "path" do metadata = {dependencies: {reallynoshardyml: {path: rel_path("reallynoshardyml")}}} with_shard(metadata) do ex = expect_raises(FailedCommand) { run "shards install --no-color -v" } ex.stdout.should contain(%(E: Missing "shard.yml" for "reallynoshardyml" at #{File.expand_path(rel_path("reallynoshardyml")).inspect})) end end end it "expands path and shows in debug info if missing" do metadata = {dependencies: {nonexistent: {path: "~/nonexistent-path"}}} with_shard(metadata) do ex = expect_raises(FailedCommand) { run "shards install --no-color -v" } ex.stdout.should contain(%(E: Failed no such path: #{Path.home.join("nonexistent-path")})) end end it "install dependency with no shard.yml and show warning" do metadata = {dependencies: {noshardyml: "0.1.0"}} with_shard(metadata) do stdout = run "shards install --no-color", env: {"CRYSTAL_VERSION" => "0.34.0"} assert_installed "noshardyml", "0.1.0" stdout.should contain(%(W: Shard "noshardyml" version (0.1.0) doesn't have a shard.yml file)) end end it "shows error when branch does not exist" do metadata = {dependencies: {web: {git: git_url(:web), branch: "foo"}}} with_shard(metadata) do ex = expect_raises(FailedCommand) { run "shards install --no-color" } ex.stdout.should contain(%(E: Could not find branch foo for shard "web" in the repository #{git_url(:web)})) end end it "shows error when tag does not exist" do metadata = {dependencies: {web: {git: git_url(:web), tag: "foo"}}} with_shard(metadata) do ex = expect_raises(FailedCommand) { run "shards install --no-color" } ex.stdout.should contain(%(E: Could not find tag foo for shard "web" in the repository #{git_url(:web)})) end end it "shows error when commit does not exist" do metadata = {dependencies: {web: {git: git_url(:web), commit: "f8f67cc67d6bd3479811825a49a16260a8c767a3"}}} with_shard(metadata) do ex = expect_raises(FailedCommand) { run "shards install --no-color" } ex.stdout.should contain(%(E: Could not find commit f8f67cc67d6bd3479811825a49a16260a8c767a3 for shard "web" in the repository #{git_url(:web)})) end end it "shows error when installing by ref and shard.yml doesn't exist" do metadata = {dependencies: {noshardyml: {git: git_url(:noshardyml), tag: "v0.1.0"}}} with_shard(metadata) do ex = expect_raises(FailedCommand) { run "shards install --no-color" } ex.stdout.should contain(%(E: No shard.yml was found for shard "noshardyml" at commit #{git_commits(:noshardyml)[1]})) end end it "shows error when installing by ref and spec is invalid" do metadata = {dependencies: {invalidspec: {git: git_url(:invalidspec), tag: "v0.1.0"}}} with_shard(metadata) do ex = expect_raises(FailedCommand) { run "shards install --no-color" } ex.stdout.should contain(%(E: Invalid shard.yml for shard "invalidspec" at commit #{git_commits(:invalidspec)[0]}: Expected SCALAR but was SEQUENCE_START at line 5, column 1)) end end it "install latest version despite current crystal being older version, but warn" do metadata = {dependencies: {incompatible: "*"}} with_shard(metadata) do stdout = run "shards install --no-color", env: {"CRYSTAL_VERSION" => "0.3.0"} assert_installed "incompatible", "1.0.0" stdout.should contain(%(W: Shard "incompatible" may be incompatible with Crystal 0.3.0)) end end it "install latest version despite current crystal being newer version, but warn" do metadata = {dependencies: {incompatible: "*"}} with_shard(metadata) do stdout = run "shards install --no-color", env: {"CRYSTAL_VERSION" => "2.0.0"} assert_installed "incompatible", "1.0.0" stdout.should contain(%(W: Shard "incompatible" may be incompatible with Crystal 2.0.0)) end end it "does match crystal prerelease" do metadata = {dependencies: {incompatible: "*"}} with_shard(metadata) do run "shards install", env: {"CRYSTAL_VERSION" => "1.0.0-pre1"} assert_installed "incompatible", "1.0.0" end end it "fails on conflicting sources" do metadata = {dependencies: { intermediate: "*", awesome: {git: git_url(:forked_awesome)}, }} with_shard(metadata) do ex = expect_raises(FailedCommand) { run "shards install --no-color" } ex.stdout.should contain("Error shard name (awesome) has ambiguous sources") end end it "can override to use local path" do metadata = {dependencies: { intermediate: "*", }} override = {dependencies: { awesome: {path: git_path(:forked_awesome)}, }} with_shard(metadata, nil, override) do run "shards install" assert_installed "awesome", "0.2.0", source: {path: git_path(:forked_awesome)} assert_locked "awesome", "0.2.0", source: {path: git_path(:forked_awesome)} end end it "can override to use forked git repository branch" do metadata = {dependencies: { intermediate: "*", }} override = {dependencies: { awesome: {git: git_url(:forked_awesome), branch: "feature/super"}, }} expected_commit = git_commits(:forked_awesome).first with_shard(metadata, nil, override) do run "shards install" assert_installed "awesome", "0.2.0+git.commit.#{expected_commit}", source: {git: git_url(:forked_awesome)} assert_locked "awesome", "0.2.0+git.commit.#{expected_commit}", source: {git: git_url(:forked_awesome)} end end it "updates to override with branch if lock is not up to date in main dependency" do metadata = {dependencies: { awesome: "*", }} lock = {awesome: "0.1.0", d: "0.1.0"} override = {dependencies: { awesome: {git: git_url(:forked_awesome), branch: "feature/super"}, }} expected_commit = git_commits(:forked_awesome).first with_shard(metadata, lock, override) do run "shards install" assert_installed "awesome", "0.2.0+git.commit.#{expected_commit}", source: {git: git_url(:forked_awesome)} assert_locked "awesome", "0.2.0+git.commit.#{expected_commit}", source: {git: git_url(:forked_awesome)} # nested dependencies are unlocked version assert_installed "d", "0.2.0" assert_locked "d", "0.2.0" end end it "updates to override with branch if lock is not up to date in nested dependency" do metadata = {dependencies: { intermediate: "*", }} lock = {intermediate: "0.1.0", awesome: "0.1.0", d: "0.1.0"} override = {dependencies: { awesome: {git: git_url(:forked_awesome), branch: "feature/super"}, }} expected_commit = git_commits(:forked_awesome).first with_shard(metadata, lock, override) do run "shards install" assert_installed "awesome", "0.2.0+git.commit.#{expected_commit}", source: {git: git_url(:forked_awesome)} assert_locked "awesome", "0.2.0+git.commit.#{expected_commit}", source: {git: git_url(:forked_awesome)} # nested dependencies are unlocked version assert_installed "d", "0.2.0" assert_locked "d", "0.2.0" end end it "updates to override with version if lock is not up to date in main dependency" do metadata = {dependencies: { awesome: "*", }} lock = {awesome: "0.1.0", d: "0.1.0"} override = {dependencies: { awesome: {git: git_url(:forked_awesome), version: "0.2.0"}, }} with_shard(metadata, lock, override) do run "shards install" assert_installed "awesome", "0.2.0", source: {git: git_url(:forked_awesome)} assert_locked "awesome", "0.2.0", source: {git: git_url(:forked_awesome)} end end it "updates to override with version if lock is not up to date in nested dependency" do metadata = {dependencies: { intermediate: "*", }} lock = {intermediate: "0.1.0", awesome: "0.1.0", d: "0.1.0"} override = {dependencies: { awesome: {git: git_url(:forked_awesome), version: "0.2.0"}, }} with_shard(metadata, lock, override) do run "shards install" assert_installed "awesome", "0.2.0", source: {git: git_url(:forked_awesome)} assert_locked "awesome", "0.2.0", source: {git: git_url(:forked_awesome)} end end it "keeps nested dependency lock if it's also a main dependency" do metadata = {dependencies: { awesome: "*", d: "*", }} lock = {awesome: "0.1.0", d: "0.1.0"} override = {dependencies: { awesome: {git: git_url(:forked_awesome), branch: "feature/super"}, }} expected_commit = git_commits(:forked_awesome).first with_shard(metadata, lock, override) do run "shards install" assert_installed "awesome", "0.2.0+git.commit.#{expected_commit}", source: {git: git_url(:forked_awesome)} assert_locked "awesome", "0.2.0+git.commit.#{expected_commit}", source: {git: git_url(:forked_awesome)} # keep nested dependencies locked version assert_installed "d", "0.1.0" assert_locked "d", "0.1.0" end end it "keeps override with branch in locked commit in main dependency" do # There is one commit more in this forked_awesome feature/super branch locked_commit = git_commits(:forked_awesome)[1] metadata = {dependencies: { awesome: "*", }} lock = { awesome: {version: "0.2.0+git.commit.#{locked_commit}", git: git_url(:forked_awesome)}, d: "0.1.0", } override = {dependencies: { awesome: {git: git_url(:forked_awesome), branch: "feature/super"}, }} with_shard(metadata, lock, override) do run "shards install" assert_installed "awesome", "0.2.0+git.commit.#{locked_commit}", source: {git: git_url(:forked_awesome)} assert_locked "awesome", "0.2.0+git.commit.#{locked_commit}", source: {git: git_url(:forked_awesome)} end end it "keeps override with branch in locked commit in nested dependency" do # There is one commit more in this forked_awesome feature/super branch locked_commit = git_commits(:forked_awesome)[1] metadata = {dependencies: { intermediate: "*", }} lock = { intermediate: "0.1.0", awesome: {version: "0.2.0+git.commit.#{locked_commit}", git: git_url(:forked_awesome)}, d: "0.1.0", } override = {dependencies: { awesome: {git: git_url(:forked_awesome), branch: "feature/super"}, }} with_shard(metadata, lock, override) do run "shards install" assert_installed "awesome", "0.2.0+git.commit.#{locked_commit}", source: {git: git_url(:forked_awesome)} assert_locked "awesome", "0.2.0+git.commit.#{locked_commit}", source: {git: git_url(:forked_awesome)} end end it "uses override relative file specified in SHARDS_OVERRIDE env var" do metadata = {dependencies: { intermediate: "*", }} ignored_override = {dependencies: { awesome: {path: git_path(:forked_awesome)}, }} ci_override = {dependencies: { awesome: {git: git_url(:forked_awesome)}, }} with_shard(metadata, nil, ignored_override) do File.write "shard.ci.yml", to_override_yaml(ci_override) run "shards install", env: {"SHARDS_OVERRIDE" => "shard.ci.yml"} assert_installed "awesome", "0.2.0", source: {git: git_url(:forked_awesome)} assert_locked "awesome", "0.2.0", source: {git: git_url(:forked_awesome)} end end it "allows empty shard.override.yml" do with_shard({dependencies: nil}) do File.write "shard.override.yml", "" run "shards install" end end it "fails if file specified in SHARDS_OVERRIDE env var does not exist" do metadata = {dependencies: { intermediate: "*", }} ignored_override = {dependencies: { awesome: {path: git_path(:forked_awesome)}, }} with_shard(metadata, nil, ignored_override) do ex = expect_raises(FailedCommand) do run "shards install --no-color", env: {"SHARDS_OVERRIDE" => "shard.missing.yml"} end ex.stdout.should contain("Missing shard.missing.yml") end end describe "mtime" do it "mtime lib > shard.lock > shard.yml" do metadata = {dependencies: { web: "*", }} with_shard(metadata) do run "shards install" File.info("shard.lock").modification_time.should be <= File.info("lib").modification_time File.info("shard.yml").modification_time.should be <= File.info("shard.lock").modification_time run "shards install" File.info("shard.lock").modification_time.should be <= File.info("lib").modification_time File.info("shard.yml").modification_time.should be <= File.info("shard.lock").modification_time end end it "mtime shard.lock > shard.yml even when unmodified" do metadata = {dependencies: { web: "*", }} with_shard(metadata) do run "shards install" File.touch("shard.yml") run "shards install" File.info("shard.lock").modification_time.should be <= File.info("lib").modification_time File.info("shard.yml").modification_time.should be <= File.info("shard.lock").modification_time end end end it "fails when git is missing" do metadata = {dependencies: {web: "*"}} with_shard(metadata) do ex = expect_raises(FailedCommand) { run "shards install --no-color", env: {"PATH" => File.expand_path("../../bin", __DIR__), "SHARDS_CACHE_PATH" => ""} } ex.stdout.should contain "Error missing git command line tool. Please install Git first!" end end end shards-0.19.0/spec/integration/list_spec.cr000066400000000000000000000065321473060476400206760ustar00rootroot00000000000000require "./spec_helper" describe "list" do it "lists all dependencies" do metadata = { dependencies: {web: "*", orm: "*"}, development_dependencies: {mock: "*"}, } with_shard(metadata) do run "shards install" stdout = run "shards list" stdout.should contain("web (2.1.0)") stdout.should contain("orm (0.5.0)") stdout.should contain("pg (0.2.1)") stdout.should contain("mock (0.1.0)") stdout.should contain("shoulda (0.1.0)") end end it "--without-development doesn't list development dependencies" do metadata = { dependencies: {web: "*", orm: "*"}, development_dependencies: {mock: "*"}, } with_shard(metadata) do run "shards install --without-development" stdout = run "shards list --without-development" stdout.should contain("web (2.1.0)") stdout.should contain("orm (0.5.0)") stdout.should contain("pg (0.2.1)") stdout.should_not contain("mock") stdout.should_not contain("shoulda") end end it "--production doesn't list development dependencies" do metadata = { dependencies: {web: "*", orm: "*"}, development_dependencies: {mock: "*"}, } # --production requires a lock file because it implies --frozen lock = {web: "1.0.0", orm: "0.3.0"} with_shard(metadata, lock) do run "shards install --production" stdout = run "shards list --production" stdout.should contain("web (1.0.0)") stdout.should contain("orm (0.3.0)") stdout.should_not contain("mock") stdout.should_not contain("shoulda") end end it "lists tree all dependencies" do metadata = { dependencies: {web: "*", orm: "*"}, development_dependencies: {mock: "*"}, } with_shard(metadata) do run "shards install" stdout = run "shards list --tree" stdout.should contain(" * web (2.1.0)") stdout.should contain(" * orm (0.5.0)") stdout.should contain(" * pg (0.2.1)") stdout.should contain(" * mock (0.1.0)") stdout.should contain(" * shoulda (0.1.0)") end end it "show error when dependencies are not installed" do metadata = { dependencies: {web: "*", orm: "*"}, development_dependencies: {mock: "*"}, } with_shard(metadata) do ex = expect_raises(FailedCommand) { run "shards list --no-color" } ex.stdout.should contain("Dependencies aren't satisfied. Install them with 'shards install'") end end it "show previous installed dependency when source has changed" do with_shard({dependencies: {awesome: "0.1.0"}}) do run "shards install" end with_shard({dependencies: {awesome: {version: "0.2.0", git: git_url(:forked_awesome)}}}) do stdout = run "shards list --tree" stdout.should contain(" * awesome (0.1.0)") stdout.should contain(" * d (0.2.0)") end end it "show previous installed dependency when override is added" do metadata = {dependencies: {awesome: "0.1.0"}} with_shard(metadata) do run "shards install" end override = {dependencies: {awesome: "0.2.0"}} with_shard(metadata, nil, override) do stdout = run "shards list --tree" stdout.should contain(" * awesome (0.1.0)") stdout.should contain(" * d (0.2.0)") end end end shards-0.19.0/spec/integration/lock_spec.cr000066400000000000000000000050711473060476400206500ustar00rootroot00000000000000require "./spec_helper" describe "lock" do it "fails when spec is missing" do Dir.cd(application_path) do ex = expect_raises(FailedCommand) { run "shards lock --no-color" } ex.stdout.should contain("Missing #{Shards::SPEC_FILENAME}") ex.stdout.should contain("Please run 'shards init'") end end it "doesn't generate lockfile when project has no dependencies" do with_shard({name: "test"}) do run "shards lock" File.exists?(File.join(application_path, "shard.lock")).should be_false end end it "creates lockfile" do metadata = { dependencies: {web: "*", orm: "*", foo: {path: rel_path(:foo)}}, development_dependencies: {mock: "*"}, } with_shard(metadata) do run "shards lock" # it locked dependencies (recursively): assert_locked "web", "2.1.0" assert_locked "orm", "0.5.0" assert_locked "pg", "0.2.1" # it locked development dependencies (not recursively) assert_locked "mock", "0.1.0" refute_locked "minitest" # it didn't install anything: refute_installed "web" refute_installed "orm" refute_installed "pg" refute_installed "foo" refute_installed "mock" refute_installed "shoulda" end end it "locks is consistent with lockfile" do metadata = { dependencies: {web: "*"}, development_dependencies: {minitest: "~> 0.1"}, } lock = {web: "1.0.0", minitest: "0.1.2"} with_shard(metadata, lock) do run "shards lock" assert_locked "web", "1.0.0" assert_locked "minitest", "0.1.2" end end it "locks new dependencies" do metadata = {dependencies: {web: "~> 1.0.0", orm: "*"}} lock = {web: "1.0.0"} with_shard(metadata, lock) do run "shards lock" assert_locked "web", "1.0.0" assert_locked "orm", "0.5.0" assert_locked "pg", "0.2.1" end end it "removes dependencies" do metadata = {dependencies: {web: "~> 1.0.0"}} lock = {web: "1.0.0", orm: "0.5.0", pg: "0.2.1"} with_shard(metadata, lock) do run "shards lock" assert_locked "web", "1.0.0" refute_locked "orm", "0.5.0" refute_locked "pg", "0.2.1" end end it "updates lockfile" do metadata = { dependencies: {web: "~> 1.0"}, development_dependencies: {minitest: "~> 0.1"}, } lock = {web: "1.0.0", minitest: "0.1.2"} with_shard(metadata, lock) do run "shards lock --update" assert_locked "web", "1.2.0" assert_locked "minitest", "0.1.3" end end end shards-0.19.0/spec/integration/outdated_spec.cr000066400000000000000000000247521473060476400215400ustar00rootroot00000000000000require "./spec_helper" describe "outdated" do it "up to date" do with_shard({dependencies: {web: "*"}}) do run "shards install" stdout = run "shards outdated --no-color" stdout.should contain("I: Dependencies are up to date!") end end it "not latest version" do with_shard({dependencies: {orm: "*"}}, {orm: "0.3.1"}) do run "shards install" stdout = run "shards outdated --no-color" stdout.should contain("W: Outdated dependencies:") stdout.should contain(" * orm (installed: 0.3.1, available: 0.5.0)") end end it "no releases" do commit = git_commits("missing").first with_shard({dependencies: {missing: "*"}}, {missing: "0.1.0+git.commit.#{commit}"}) do run "shards install" stdout = run "shards outdated --no-color" stdout.should contain("I: Dependencies are up to date!") end end it "available version matching pessimistic operator" do with_shard({dependencies: {orm: "~> 0.3.0"}}, {orm: "0.3.1"}) do run "shards install" stdout = run "shards outdated --no-color" stdout.should contain("W: Outdated dependencies:") stdout.should contain(" * orm (installed: 0.3.1, available: 0.3.2, latest: 0.5.0)") end end it "reports new prerelease" do with_shard({dependencies: {unstable: "0.3.0.alpha"}}) do run "shards install" end with_shard({dependencies: {unstable: "~> 0.3.0.alpha"}}) do stdout = run "shards outdated --no-color" stdout.should contain("W: Outdated dependencies:") stdout.should contain(" * unstable (installed: 0.3.0.alpha, available: 0.3.0.beta)") end end it "won't report prereleases by default" do with_shard({dependencies: {preview: "*"}}, {preview: "0.2.0"}) do run "shards install" stdout = run "shards outdated --no-color" stdout.should contain("W: Outdated dependencies:") stdout.should contain(" * preview (installed: 0.2.0, available: 0.3.0)") end end it "reports prereleases when asked" do with_shard({dependencies: {preview: "*"}}, {preview: "0.2.0"}) do run "shards install" stdout = run "shards outdated --pre --no-color" stdout.should contain("W: Outdated dependencies:") stdout.should contain(" * preview (installed: 0.2.0, available: 0.4.0.a)") end end it "fails when source has changed" do with_shard({dependencies: {awesome: "0.1.0"}}) do run "shards install" end with_shard({dependencies: {awesome: {git: git_url(:forked_awesome)}}}) do ex = expect_raises(FailedCommand) { run "shards outdated --no-color" } ex.stdout.should contain("Outdated shard.lock (awesome source changed)") end end it "fails when requirements would require an update" do with_shard({dependencies: {awesome: "0.1.0"}}) do run "shards install" end with_shard({dependencies: {awesome: "0.2.0"}}) do ex = expect_raises(FailedCommand) { run "shards outdated --no-color" } ex.stdout.should contain("Outdated shard.lock (awesome requirements changed)") end end it "fails when requirements would require an update due to override" do metadata = {dependencies: {awesome: "0.1.0"}} with_shard(metadata) do run "shards install" end override = {dependencies: {awesome: "0.2.0"}} with_shard(metadata, nil, override) do ex = expect_raises(FailedCommand) { run "shards outdated --no-color" } ex.stdout.should contain("Outdated shard.lock (awesome requirements changed)") end end it "not latest version in override (same source)" do metadata = {dependencies: {awesome: "0.1.0"}} lock = {awesome: "0.1.0"} override = {dependencies: {awesome: "*"}} with_shard(metadata, lock, override) do run "shards install" stdout = run "shards outdated --no-color" stdout.should contain("W: Outdated dependencies:") stdout.should contain(" * awesome (installed: 0.1.0, available: 0.3.0)") end end it "not latest version in override (different source)" do metadata = {dependencies: {awesome: "0.1.0"}} lock = {awesome: {version: "0.1.0", git: git_url(:forked_awesome)}} override = {dependencies: {awesome: {git: git_url(:forked_awesome)}}} with_shard(metadata, lock, override) do run "shards install" stdout = run "shards outdated --no-color" stdout.should contain("W: Outdated dependencies:") stdout.should contain(" * awesome (installed: 0.1.0, available: 0.2.0)") end end it "up to date in override" do metadata = {dependencies: {awesome: "0.1.0"}} lock = {awesome: {version: "0.2.0", git: git_url(:forked_awesome)}} override = {dependencies: {awesome: {git: git_url(:forked_awesome)}}} with_shard(metadata, lock, override) do run "shards install" stdout = run "shards outdated --no-color" stdout.should contain("I: Dependencies are up to date!") end end describe "non-release" do describe "without releases" do it "latest any" do with_shard({dependencies: {missing: "*"}}, {missing: "0.1.0+git.commit.#{git_commits("missing").first}"}) do run "shards install" stdout = run "shards outdated --no-color" stdout.should contain("I: Dependencies are up to date!") end end it "latest branch" do with_shard({dependencies: {missing: {git: git_url("missing"), branch: "master"}}}, {missing: "0.1.0+git.commit.#{git_commits("missing").first}"}) do run "shards install" stdout = run "shards outdated --no-color" stdout.should contain("I: Dependencies are up to date!") end end it "latest commit" do with_shard({dependencies: {missing: {git: git_url("missing"), commit: git_commits("missing").first}}}, {missing: "0.1.0+git.commit.#{git_commits("missing").first}"}) do run "shards install" stdout = run "shards outdated --no-color" stdout.should contain("I: Dependencies are up to date!") end end it "outdated any" do commits = git_commits("inprogress") with_shard({dependencies: {inprogress: "*"}}, {inprogress: "0.1.0+git.commit.#{commits[1]}"}) do run "shards install" stdout = run "shards outdated --no-color" stdout.should contain("W: Outdated dependencies:") stdout.should contain(" * inprogress (installed: 0.1.0 at #{commits[1][0..6]}, available: 0.1.0 at #{commits.first[0..6]})") end end it "outdated branch" do commits = git_commits("inprogress") with_shard({dependencies: {inprogress: {git: git_url("inprogress"), branch: "master"}}}, {inprogress: "0.1.0+git.commit.#{commits[1]}"}) do run "shards install" stdout = run "shards outdated --no-color" stdout.should contain("W: Outdated dependencies:") stdout.should contain(" * inprogress (installed: 0.1.0 at #{commits[1][0..6]}, available: 0.1.0 at #{commits.first[0..6]})") end end it "outdated commit" do commits = git_commits("inprogress") with_shard({dependencies: {inprogress: {git: git_url("inprogress"), commit: commits[1]}}}, {inprogress: "0.1.0+git.commit.#{commits[1]}"}) do run "shards install" stdout = run "shards outdated --no-color" stdout.should contain("W: Outdated dependencies:") stdout.should contain(" * inprogress (installed: 0.1.0 at #{commits[1][0..6]}") # TODO: commit # stdout.should contain(" * inprogress (installed: 0.1.0 at #{commits[1][0..6]}, available: 0.1.0 at #{commits.first[0..6]})") end end end describe "with previous releases" do it "outdated any" do commits = git_commits("heading") with_shard({dependencies: {heading: "*"}}, {heading: "0.1.0+git.commit.#{commits[1]}"}) do run "shards install" stdout = run "shards outdated --no-color" stdout.should contain("W: Outdated dependencies:") stdout.should contain(" * heading (installed: 0.1.0 at #{commits[1][0..6]}, available: 0.1.0)") # TODO: stdout.should contain(" * heading (installed: 0.1.0 at #{commits[1][0..6]}, available: 0.1.0 at #{commits.first[0..6]})") end end it "latest any" do commits = git_commits("heading") with_shard({dependencies: {heading: "*"}}, {heading: "0.1.0+git.commit.#{commits.first}"}) do run "shards install" stdout = run "shards outdated --no-color" stdout.should contain("I: Dependencies are up to date!") end end it "latest branch" do commits = git_commits("heading") with_shard({dependencies: {heading: {git: git_url("heading"), branch: "master"}}}, {heading: "0.1.0+git.commit.#{commits.first}"}) do run "shards install" stdout = run "shards outdated --no-color" stdout.should contain("I: Dependencies are up to date!") end end end it "outdated any with new release" do commits = git_commits("release_hist") with_shard({dependencies: {release_hist: "*"}}, {release_hist: "0.1.0+git.commit.#{commits[1]}"}) do run "shards install" stdout = run "shards outdated --no-color" stdout.should contain("W: Outdated dependencies:") stdout.should contain(" * release_hist (installed: 0.1.0 at #{commits[1][0..6]}, available: 0.2.0)") end end it "outdated branch without new release" do installed_commit = git_commits("branched", "feature")[1] branch_head = git_commits("branched", "feature").first with_shard({dependencies: {branched: {git: git_url("branched"), branch: "feature"}}}, {branched: "0.1.0+git.commit.#{installed_commit}"}) do run "shards install" stdout = run "shards outdated --no-color" stdout.should contain("W: Outdated dependencies:") stdout.should contain(" * branched (installed: 0.1.0 at #{installed_commit[0..6]}, available: 0.1.0 at #{branch_head[0..6]}, latest: 0.2.0)") end end it "latest branch with release on HEAD" do branch_head = git_commits("branched", "feature").first with_shard({dependencies: {branched: {git: git_url("branched"), branch: "feature"}}}, {branched: "0.1.0+git.commit.#{branch_head}"}) do run "shards install" stdout = run "shards outdated --no-color" stdout.should contain("W: Outdated dependencies:") stdout.should contain(" * branched (installed: 0.1.0 at #{branch_head[0..6]}, latest: 0.2.0)") end end end end shards-0.19.0/spec/integration/prune_spec.cr000066400000000000000000000024161473060476400210510ustar00rootroot00000000000000require "./spec_helper" private def installed_dependencies Dir.children(install_path).reject!(".shards.info") end describe "prune" do before_each do metadata = { dependencies: {web: "*", orm: {git: git_url(:orm), branch: "master"}}, development_dependencies: {mock: "*"}, } with_shard(metadata) { run "shards install" } metadata = { dependencies: {web: "*"}, } with_shard(metadata) { run "shards update" } end it "removes unused dependencies" do Dir.cd(application_path) { run "shards prune" } installed_dependencies.should eq(["web"]) Shards::Info.new(install_path).installed.keys.should eq(["web"]) end it "removes directories" do Dir.mkdir(install_path("test")) Dir.cd(application_path) { run "shards prune" } installed_dependencies.should eq(["web"]) end it "won't remove files" do File.write(install_path(".keep_hidden"), "") File.write(install_path("keep_not_hidden"), "") Dir.cd(application_path) { run "shards prune" } installed_dependencies.sort.should eq([".keep_hidden", "keep_not_hidden", "web"]) end it "should not fail if the install directory does not exist" do FileUtils.rm_rf(install_path) Dir.cd(application_path) { run "shards prune" } end end shards-0.19.0/spec/integration/run_spec.cr000066400000000000000000000106071473060476400205250ustar00rootroot00000000000000require "./spec_helper" private def bin_path(name) File.join(application_path, "bin", Shards::Helpers.exe(name)) end describe "run" do before_each do Dir.mkdir(File.join(application_path, "src")) File.write(File.join(application_path, "src", "cli.cr"), "puts __FILE__") end after_each do File.delete File.join(application_path, "shard.yml") end it "fails when no targets defined" do File.write File.join(application_path, "shard.yml"), <<-YAML name: build version: 0.1.0 YAML Dir.cd(application_path) do ex = expect_raises(FailedCommand) do run "shards run --no-color" end ex.stdout.should contain("Targets not defined in shard.yml") end end it "fails when passing multiple targets" do File.write File.join(application_path, "shard.yml"), <<-YAML name: build version: 0.1.0 targets: app: main: src/cli.cr alt: main: src/cli.cr YAML Dir.cd(application_path) do ex = expect_raises(FailedCommand) do run "shards run --no-color app alt" end ex.stdout.should contain("Error please specify only one target. If you meant to pass arguments you may use 'shards run target -- args'") end end it "fails when multiple targets, no arg" do File.write File.join(application_path, "shard.yml"), <<-YAML name: build version: 0.1.0 targets: app: main: src/cli.cr alt: main: src/cli.cr YAML Dir.cd(application_path) do ex = expect_raises(FailedCommand) do run "shards run --no-color" end ex.stdout.should contain("Error please specify the target with 'shards run target'") end end it "runs when only one target" do File.write File.join(application_path, "shard.yml"), <<-YAML name: build version: 0.1.0 targets: app: main: src/cli.cr YAML Dir.cd(application_path) do output = run("shards run --no-color") File.exists?(bin_path("app")).should be_true output.should contain("Executing: app") output.chomp.should contain(File.join(application_path, "src", "cli.cr")) end end it "runs specified target" do File.write File.join(application_path, "shard.yml"), <<-YAML name: build version: 0.1.0 targets: app: main: src/cli.cr alt: main: src/cli.cr YAML Dir.cd(application_path) do output = run("shards run --no-color app") File.exists?(bin_path("app")).should be_true File.exists?(bin_path("alt")).should be_false output.should contain("Executing: app") output.chomp.should contain(File.join(application_path, "src", "cli.cr")) end end it "passes back execution failure from child process" do File.write File.join(application_path, "src", "fail.cr"), <<-CR puts "This command fails" exit 5 CR File.write File.join(application_path, "shard.yml"), <<-YAML name: build version: 0.1.0 targets: fail: main: src/fail.cr YAML Dir.cd(application_path) do ex = expect_raises(FailedCommand) do run "shards run --no-color" end ex.stdout.should contain("This command fails") end end it "forwards additional ARGV to child process" do File.write File.join(application_path, "src", "args.cr"), <<-CR print "args: ", ARGV.join(',') CR File.write File.join(application_path, "shard.yml"), <<-YAML name: build version: 0.1.0 targets: app: main: src/args.cr YAML Dir.cd(application_path) do output = run("shards run --no-color -- foo bar baz") output.should contain("Executing: app foo bar baz") output.should contain("args: foo,bar,baz") end end it "works well with stdin" do File.write File.join(application_path, "src", "stdin.cr"), <<-CR print "input: ", STDIN.gets.inspect CR File.write File.join(application_path, "shard.yml"), <<-YAML name: build version: 0.1.0 targets: app: main: src/stdin.cr YAML Dir.cd(application_path) do input = IO::Memory.new("hello from stdin") output = run("shards run --no-color", input: input) output.should contain("Executing: app") output.should contain(%(input: "hello from stdin")) end end end shards-0.19.0/spec/integration/spec_helper.cr000066400000000000000000000256151473060476400212050ustar00rootroot00000000000000ENV["PATH"] = "#{File.expand_path("../../bin", __DIR__)}#{Process::PATH_DELIMITER}#{ENV["PATH"]}" ENV["SHARDS_CACHE_PATH"] = ".shards" require "spec" require "../../src/config" require "../../src/helpers" require "../../src/lock" require "../../src/spec" require "../support/factories" require "../support/cli" require "../support/requirement" Spec.before_suite do Shards::Helpers.rm_rf_children(tmp_path) setup_repositories end Spec.before_each do Shards::Resolver.clear_resolver_cache end private def setup_repositories # git dependencies for testing version resolution: create_git_repository "web", "1.0.0", "1.1.0", "1.1.1", "1.1.2", "1.2.0", "2.0.0", "2.1.0" create_git_repository "pg", "0.1.0", "0.2.0", "0.2.1", "0.3.0" create_git_repository "optional", "0.2.0", "0.2.1", "0.2.2" create_git_repository "shoulda", "0.1.0" create_git_repository "minitest", "0.1.0", "0.1.1", "0.1.2", "0.1.3" create_git_repository "mock" create_git_release "mock", "0.1.0", { dependencies: {shoulda: {git: git_path("shoulda"), version: "< 0.3.0"}}, development_dependencies: {minitest: {git: git_path("minitest")}}, } create_git_repository "orm", "0.1.0", "0.2.0", "0.3.0", "0.3.1", "0.3.2", "0.4.0" create_git_release "orm", "0.5.0", { dependencies: {pg: {git: git_path("pg"), version: "< 0.3.0"}}, } create_git_repository "release", "0.2.0", "0.2.1", "0.2.2" create_git_release "release", "0.3.0", { ncustom_dependencies: {pg: {git: git_path("optional")}}, } # git dependencies with prereleases: create_git_repository "unstable", "0.1.0", "0.2.0", "0.3.0.alpha", "0.3.0.beta" create_git_repository "preview", "0.1.0", "0.2.0", "0.3.0.a", "0.3.0.b", "0.3.0", "0.4.0.a" # path dependency: create_path_repository "foo", "0.1.0" # dependency with neither a shard.yml and/or version tags: # create_git_repository "empty" # create_git_commit "empty", "initial release" create_git_repository "missing" create_shard "missing", "0.1.0" create_git_commit "missing", "initial release" create_git_repository "noshardyml" create_git_release "noshardyml", "0.1.0", false create_git_release "noshardyml", "0.2.0" create_git_repository "reallynoshardyml" create_git_release "reallynoshardyml", "0.1.0", false create_git_repository "invalidspec" create_git_release "invalidspec", "0.1.0", {crystal: [""]} # dependencies with postinstall scripts: create_git_repository "post" {% if flag?(:win32) %} create_executable "post", "make", %(File.touch("made.txt")) {% else %} create_file "post", "Makefile", "all:\n\ttouch made.txt\n" {% end %} create_git_release "post", "0.1.0", {scripts: {postinstall: "make"}} create_git_repository "fails" {% if flag?(:win32) %} create_executable "fails", "make", %(exit 1) {% else %} create_file "fails", "Makefile", "all:\n\ttest -n ''\n" {% end %} create_git_release "fails", "0.1.0", {scripts: {postinstall: "make"}} # transitive dependencies in postinstall scripts: create_git_repository "version" create_file "version", "src/version.cr", %(module Version; STRING = "version @ 0.1.0"; end) create_git_release "version", "0.1.0" create_git_repository "renamed" create_git_release "renamed", "0.1.0", {name: "old_name"} create_git_release "renamed", "0.2.0", {name: "new_name"} create_git_version_commit "renamed", "0.3.0", {name: "another_name"} create_git_repository "version_mismatch" create_git_release "version_mismatch", "0.1.0" create_git_release "version_mismatch", "0.2.0", {version: "0.1.0"} create_git_release "version_mismatch", "0.2.1" create_git_repository "inprogress" create_git_version_commit "inprogress", "0.1.0" create_git_version_commit "inprogress", "0.1.0" create_git_repository "transitive" create_file "transitive", "src/version.cr", %(require "version"; puts Version::STRING) create_git_release "transitive", "0.2.0", { dependencies: {version: {git: git_url(:version)}}, scripts: { postinstall: %(#{{{ flag?(:win32) ? "crystal" : "${CRYSTAL:-crystal}" }}} build src/version.cr), }, } create_git_repository "transitive_2" create_git_release "transitive_2", "0.1.0", { dependencies: { transitive: {git: git_url(:transitive)}, }, scripts: { postinstall: "../transitive/version", }, } # dependencies with executables: create_git_repository "binary" create_executable "binary", "bin/foobar", %(print "OK") create_executable "binary", "bin/baz", %(print "KO") create_file "binary", "bin/crystal.cr", %(puts "crystal") create_git_release "binary", "0.1.0", {executables: ["foobar", "baz", "crystal.cr"]} create_executable "binary", "bin/foo", %(print "FOO") create_git_release "binary", "0.2.0", {executables: ["foobar", "baz", "foo"]} create_git_repository "executable_missing" create_git_release "executable_missing", "0.1.0", {executables: ["nonexistent"]} create_git_repository "c" create_git_release "c", "0.1.0", {dependencies: {d: {git: git_url(:d), version: "0.1.0"}}} create_git_release "c", "0.2.0", {dependencies: {d: {git: git_url(:d), version: "0.2.0"}}} create_git_repository "d" create_git_release "d", "0.1.0" create_git_release "d", "0.2.0" create_git_repository "incompatible" create_git_release "incompatible", "0.1.0", {crystal: "~>0.1, >=0.1.0"} create_git_release "incompatible", "0.2.0", {crystal: "~>0.2, >=0.2.0"} create_git_release "incompatible", "0.3.0", {crystal: "~>0.4, >=0.4"} create_git_release "incompatible", "1.0.0", {crystal: "~>1.0, >=1.0.0"} create_git_repository "awesome" create_git_release "awesome", "0.1.0", { dependencies: {d: {git: git_url(:d)}}, } # Release v0.1.0 is the same in awesome and forked_awesome create_fork_git_repository "forked_awesome", "awesome" # But v0.2.0 is not, they might be different create_git_release "forked_awesome", "0.2.0", { name: "awesome", dependencies: {d: {git: git_url(:d)}}, } create_git_release "awesome", "0.2.0", { dependencies: {d: {git: git_url(:d)}}, } # Release v0.3.0 is only available in the original create_git_release "awesome", "0.3.0", { dependencies: {d: {git: git_url(:d)}}, } checkout_new_git_branch "forked_awesome", "feature/super" create_file "forked_awesome", File.join("src", "super_feature.cr"), "" create_git_commit "forked_awesome", "Starting super feature" create_git_commit "forked_awesome", "More on super feature" create_git_repository "intermediate" create_git_release "intermediate", "0.1.0", { dependencies: {awesome: {git: git_url(:awesome)}}, } # repo with release and unreleased commits create_git_repository "heading" create_git_release "heading", "0.1.0" create_git_version_commit "heading", "0.1.0" create_git_version_commit "heading", "0.1.0" # repo with release and preceding commit create_git_repository "release_hist" create_git_release "release_hist", "0.1.0" create_git_version_commit "release_hist", "0.1.0" create_git_release "release_hist", "0.2.0" # repo with branch and new release outside branch create_git_repository "branched" create_git_release "branched", "0.1.0" checkout_new_git_branch "branched", "feature" create_git_version_commit "branched", "0.1.0" checkout_git_branch "branched", "master" create_git_release "branched", "0.2.0" end private def assert(value, message, file, line) fail(message, file, line) unless value end private def refute(value, message, file, line) fail(message, file, line) if value end def assert_installed(name, version = nil, file = __FILE__, line = __LINE__, *, git = nil, source = nil) assert File.exists?(install_path(name)), "expected #{name} dependency to have been installed", file, line Shards::Resolver.clear_resolver_cache # Parsing Shards::Info might use cache of resolvers. Avoid it info = Shards::Info.new(install_path) dependency = info.installed[name]? assert dependency, "expected #{name} to be present in the shards.info file", file, line if dependency && version expected_version = git ? "#{version}+git.commit.#{git}" : version dependency.version.should eq(version expected_version), file: file, line: line end if dependency && source expected_source = source_from_named_tuple(source) actual_source = source_from_resolver(dependency.resolver) assert expected_source == actual_source, "expected #{name} dependency to have been installed using #{expected_source}", file, line end end def refute_installed(name, version = nil, file = __FILE__, line = __LINE__) if version if Dir.exists?(install_path(name)) assert File.exists?(install_path(name, "shard.yml")), "expected shard.yml for installed #{name} dependency was not found", file, line spec = Shards::Spec.from_file(install_path(name, "shard.yml")) spec.version.should_not eq(version), file: file, line: line end else refute Dir.exists?(install_path(name)), "expected #{name} dependency to not have been installed", file, line end end def assert_installed_file(path, file = __FILE__, line = __LINE__) assert File.exists?(File.join(install_path(name), path)), "Expected #{path} to have been installed", file, line end def assert_locked(name, version = nil, file = __FILE__, line = __LINE__, *, git = nil, source = nil) path = File.join(application_path, "shard.lock") assert File.exists?(path), "expected shard.lock to have been generated", file, line Shards::Resolver.clear_resolver_cache # Parsing Shards::Lock might use cache of resolvers. Avoid it locks = Shards::Lock.from_file(path) assert lock = locks.shards.find { |d| d.name == name }, "expected #{name} dependency to have been locked", file, line if lock && version expected_version = git ? "#{version}+git.commit.#{git}" : version actual_value = lock.version.value assert expected_version == actual_value, "expected #{name} dependency to have been locked at version #{version} instead of #{actual_value}", file, line end if lock && source expected_source = source_from_named_tuple(source) actual_source = source_from_resolver(lock.resolver) assert expected_source == actual_source, "expected #{name} dependency to have been locked using #{expected_source} instead of #{actual_source}", file, line end end private def source_from_named_tuple(source : NamedTuple) source.to_yaml.lines.last end private def source_from_resolver(resolver : Shards::Resolver) resolver.yaml_source_entry end def refute_locked(name, version = nil, file = __FILE__, line = __LINE__) path = File.join(application_path, "shard.lock") assert File.exists?(path), "expected shard.lock to have been generated", file, line locks = Shards::Lock.from_file(path) refute locks.shards.find { |d| d.name == name }, "expected #{name} dependency to not have been locked", file, line end def install_path(*path_names) File.join(application_path, Shards::INSTALL_DIR, *path_names) end def debug(command) run "#{command} --verbose" rescue ex : FailedCommand puts puts ex.stdout puts ex.stderr end shards-0.19.0/spec/integration/subcommand_spec.cr000066400000000000000000000023401473060476400220440ustar00rootroot00000000000000require "./spec_helper" describe "subcommand" do it "forwards all arguments to subcommand" do create_shard("dummy", "0.1.0") {% if flag?(:win32) %} create_executable "dummy", "bin/shards-dummy", %(print ARGV.join(" ")) {% else %} path = create_file("dummy", "bin/shards-dummy", "#!/bin/sh\necho $@\n") File.chmod(path, 0o755) {% end %} with_path(git_path("dummy/bin")) do output = run("shards dummy --no-color --verbose --unknown other argument") output.should contain(%(--no-color --verbose --unknown other argument)) end end it "correctly forwards '--help' option to subcommand" do create_shard("dummy", "0.1.0") {% if flag?(:win32) %} create_executable "dummy", "bin/shards-dummy", %(print ARGV.join(" ")) {% else %} path = create_file("dummy", "bin/shards-dummy", "#!/bin/sh\necho $@\n") File.chmod(path, 0o755) {% end %} with_path(git_path("dummy/bin")) do output = run("shards dummy --help") output.should contain(%(--help)) end end end private def with_path(path, &) old_path = ENV["PATH"] ENV["PATH"] = "#{File.expand_path(path)}#{Process::PATH_DELIMITER}#{ENV["PATH"]}" yield ensure ENV["PATH"] = old_path end shards-0.19.0/spec/integration/update_spec.cr000066400000000000000000000377111473060476400212100ustar00rootroot00000000000000require "./spec_helper" describe "update" do it "installs dependencies" do metadata = { dependencies: {web: "*", orm: "*"}, development_dependencies: {mock: "*"}, } with_shard(metadata) do run "shards update" # it installed dependencies (recursively) assert_installed "web", "2.1.0" assert_installed "orm", "0.5.0" assert_installed "pg", "0.2.1" # it installed development dependencies (not recursively) assert_installed "mock" refute_installed "minitest", "0.1.3" # it didn't install custom dependencies refute_installed "release" # it locked dependencies assert_locked "web", "2.1.0" assert_locked "orm", "0.5.0" assert_locked "pg", "0.2.1" # it locked development dependencies (not recursively) assert_locked "mock", "0.1.0" refute_locked "minitest" # it didn't lock custom dependencies refute_locked "release" end end it "updates locked dependencies" do metadata = { dependencies: {web: "2.0.0"}, development_dependencies: {minitest: "~> 0.1.2"}, } lock = {web: "1.0.0", minitest: "0.1.2"} with_shard(metadata, lock) do run "shards update" assert_installed "web", "2.0.0" assert_locked "web", "2.0.0" assert_installed "minitest", "0.1.3" assert_locked "minitest", "0.1.3" end end it "unlocks subdependency" do metadata = {dependencies: {c: "*"}} lock = {c: "0.1.0", d: "0.1.0"} with_shard(metadata, lock) do run "shards update c" assert_installed "c", "0.2.0" assert_installed "d", "0.2.0" end end it "updates specified dependencies" do metadata = {dependencies: {web: "*", orm: "*", optional: "*"}} lock = {web: "1.0.0", orm: "0.4.0", optional: "0.2.0"} with_shard(metadata, lock) do run "shards update orm optional" # keeps unspecified dependencies: assert_installed "web", "1.0.0" assert_locked "web", "1.0.0" # updates specified dependencies: assert_installed "orm", "0.5.0" assert_locked "orm", "0.5.0" assert_installed "optional", "0.2.2" assert_locked "optional", "0.2.2" # installs additional dependencies: assert_installed "pg", "0.2.1" assert_locked "pg", "0.2.1" end end it "won't install prerelease version" do metadata = {dependencies: {unstable: "*"}} lock = {unstable: "0.1.0"} with_shard(metadata, lock) do run "shards update" assert_installed "unstable", "0.2.0" assert_locked "unstable", "0.2.0" end end it "installs specified prerelease version" do metadata = {dependencies: {unstable: "~> 0.3.0.alpha"}} lock = {unstable: "0.3.0.alpha"} with_shard(metadata, lock) do run "shards update" assert_installed "unstable", "0.3.0.beta" assert_locked "unstable", "0.3.0.beta" end end it "updates locked specified prerelease" do metadata = {dependencies: {unstable: "~> 0.3.0.alpha"}} lock = {unstable: "0.3.0.alpha"} with_shard(metadata, lock) do run "shards update" assert_installed "unstable", "0.3.0.beta" assert_locked "unstable", "0.3.0.beta" end end it "updates from prerelease to release with approximate operator" do metadata = {dependencies: {preview: "~> 0.3.0.a"}} lock = {preview: "0.3.0.alpha"} with_shard(metadata, lock) do run "shards update" assert_installed "preview", "0.3.0" assert_locked "preview", "0.3.0" end end # TODO: detect version, and prefer release (0.3.0) over further prereleases (?) it "updates to latest prerelease with >= operator" do metadata = {dependencies: {preview: ">= 0.3.0.a"}} lock = {preview: "0.3.0.a"} with_shard(metadata, lock) do run "shards update" assert_installed "preview", "0.4.0.a" assert_locked "preview", "0.4.0.a" end end it "updates locked commit" do metadata = { dependencies: {web: {git: git_url(:web), branch: "master"}}, } lock = {web: git_commits(:web)[-5]} with_shard(metadata, lock) do run "shards update" assert_installed "web", "2.1.0", git: git_commits(:web).first assert_locked "web", "2.1.0", git: git_commits(:web).first end end it "installs new dependencies" do metadata = { dependencies: { web: "~> 1.1.0", orm: "*", }, } lock = {web: "1.1.2"} with_shard(metadata, lock) do run "shards update" assert_installed "web", "1.1.2" assert_locked "web", "1.1.2" assert_installed "orm", "0.5.0" assert_locked "orm", "0.5.0" end end it "removes dependencies" do metadata = {dependencies: {web: "~> 1.1.0"}} lock = {web: "1.0.0", orm: "0.5.0"} with_shard(metadata, lock) do run "shards update" assert_installed "web", "1.1.2" assert_locked "web", "1.1.2" refute_installed "orm" refute_locked "orm" end end it "finds then updates new compatible version" do create_git_repository "oopsie", "1.1.0", "1.2.0" metadata = {dependencies: {oopsie: "~> 1.1.0"}} lock = {oopsie: "1.1.0"} with_shard(metadata, lock) do run "shards install" assert_installed "oopsie", "1.1.0" end create_git_release "oopsie", "1.1.1" with_shard(metadata, lock) do run "shards update" assert_installed "oopsie", "1.1.1" end end it "generates lockfile for empty dependencies" do metadata = {dependencies: {} of Symbol => String} with_shard(metadata) do run "shards update" path = File.join(application_path, "shard.lock") File.exists?(path).should be_true File.read(path).should eq <<-YAML version: 2.0 shards: {} YAML end end it "runs postinstall with transitive dependencies" do with_shard({dependencies: {transitive: "*"}}, {transitive: "0.1.0"}) do run "shards update" binary = install_path("transitive", Shards::Helpers.exe("version")) File.exists?(binary).should be_true `#{Process.quote(binary)}`.chomp.should eq("version @ 0.1.0") end end it "skips postinstall with transitive dependencies" do with_shard({dependencies: {transitive: "*"}}, {transitive: "0.1.0"}) do output = run "shards update --no-color --skip-postinstall" binary = install_path("transitive", Shards::Helpers.exe("version")) File.exists?(binary).should be_false output.should contain("Postinstall of transitive: #{{{ flag?(:win32) ? "crystal" : "${CRYSTAL:-crystal}" }}} build src/version.cr (skipped)") end end it "installs new executables" do metadata = {dependencies: {binary: "0.2.0"}} lock = {binary: "0.1.0"} with_shard(metadata, lock) { run("shards update --no-color") } foobar = File.join(application_path, "bin", Shards::Helpers.exe("foobar")) baz = File.join(application_path, "bin", Shards::Helpers.exe("baz")) foo = File.join(application_path, "bin", Shards::Helpers.exe("foo")) File.exists?(foobar).should be_true # "Expected to have installed bin/foobar executable" File.exists?(baz).should be_true # "Expected to have installed bin/baz executable" File.exists?(foo).should be_true # "Expected to have installed bin/foo executable" `#{Process.quote(foobar)}`.should eq("OK") `#{Process.quote(baz)}`.should eq("KO") `#{Process.quote(foo)}`.should eq("FOO") end it "skips installing new executables" do metadata = {dependencies: {binary: "0.2.0"}} lock = {binary: "0.1.0"} with_shard(metadata, lock) { run("shards update --no-color --skip-executables") } foobar = File.join(application_path, "bin", Shards::Helpers.exe("foobar")) baz = File.join(application_path, "bin", Shards::Helpers.exe("baz")) foo = File.join(application_path, "bin", Shards::Helpers.exe("foo")) File.exists?(foobar).should be_false File.exists?(baz).should be_false File.exists?(foo).should be_false end it "doesn't update local cache" do metadata = { dependencies: {local_cache: "*"}, } with_shard(metadata) do # error: dependency isn't in local cache ex = expect_raises(FailedCommand) { run("shards install --local --no-color") } ex.stdout.should contain(%(E: Missing repository cache for "local_cache".)) end # re-run without --local to install the dependency: create_git_repository "local_cache", "1.0", "2.0" with_shard(metadata) { run("shards install") } assert_locked "local_cache", "2.0" # create a new release: create_git_release "local_cache", "3.0" # re-run with --local, which won't find the new release: with_shard(metadata) { run("shards update --local") } assert_locked "local_cache", "2.0" # run again without --local, which will find & install the new release: with_shard(metadata) { run("shards update") } assert_locked "local_cache", "3.0" end it "updates when dependency source changed" do metadata = {dependencies: {web: {path: git_path(:web)}}} lock = {web: "2.1.0"} with_shard(metadata, lock) do assert_locked "web", "2.1.0", source: {git: git_url(:web)} run "shards update" assert_locked "web", "2.1.0", source: {path: git_path(:web)} assert_installed "web", "2.1.0", source: {path: git_path(:web)} end end it "keeping installed version requires constraint in shard.yml" do # forked_awesome has 0.2.0 metadata = {dependencies: {awesome: {git: git_url(:forked_awesome), version: "~> 0.1.0"}}} lock = {awesome: "0.1.0"} with_shard(metadata, lock) do assert_locked "awesome", "0.1.0", source: {git: git_url(:awesome)} run "shards update" assert_locked "awesome", "0.1.0", source: {git: git_url(:forked_awesome)} assert_installed "awesome", "0.1.0", source: {git: git_url(:forked_awesome)} end end it "bumps nested dependencies locked when main dependency source changed" do metadata = {dependencies: {awesome: {git: git_url(:forked_awesome)}}} lock = {awesome: "0.1.0", d: "0.1.0"} with_shard(metadata, lock) do assert_locked "awesome", "0.1.0", source: {git: git_url(:awesome)} assert_locked "d", "0.1.0", source: {git: git_url(:d)} # d is not a top dependency, so it is bumped since it's required only by awesome run "shards update awesome" assert_locked "awesome", "0.2.0", source: {git: git_url(:forked_awesome)} assert_locked "d", "0.2.0", source: {git: git_url(:d)} assert_installed "awesome", "0.2.0", source: {git: git_url(:forked_awesome)} assert_installed "d", "0.2.0", source: {git: git_url(:d)} end end it "can update to forked branch after lock" do metadata = {dependencies: {awesome: {git: git_url(:forked_awesome), branch: "feature/super"}}} lock = {awesome: "0.1.0", d: "0.1.0"} with_shard(metadata, lock) do assert_locked "awesome", "0.1.0", source: {git: git_url(:awesome)} run "shards update" assert_locked "awesome", "0.2.0", git: git_commits(:forked_awesome).first assert_installed "awesome", "0.2.0", git: git_commits(:forked_awesome).first end end it "can update top dependency with override branch" do metadata = {dependencies: { awesome: "*", }} lock = {awesome: "0.1.0"} override = {dependencies: { awesome: {git: git_url(:forked_awesome), branch: "feature/super"}, }} expected_commit = git_commits(:forked_awesome).first with_shard(metadata, lock, override) do run "shards update" assert_installed "awesome", "0.2.0+git.commit.#{expected_commit}", source: {git: git_url(:forked_awesome)} assert_locked "awesome", "0.2.0+git.commit.#{expected_commit}", source: {git: git_url(:forked_awesome)} end end it "can update top dependency override version" do metadata = {dependencies: { awesome: "*", }} lock = {awesome: "0.1.0"} override = {dependencies: { awesome: {git: git_url(:forked_awesome), version: "0.1.0"}, }} with_shard(metadata, lock, override) do run "shards update" assert_installed "awesome", "0.1.0", source: {git: git_url(:forked_awesome)} assert_locked "awesome", "0.1.0", source: {git: git_url(:forked_awesome)} end end it "can update to nested override branch" do metadata = {dependencies: { intermediate: "*", }} lock = {intermediate: "0.1.0", awesome: "0.1.0"} override = {dependencies: { awesome: {git: git_url(:forked_awesome), branch: "feature/super"}, }} expected_commit = git_commits(:forked_awesome).first with_shard(metadata, lock, override) do run "shards update" assert_installed "awesome", "0.2.0+git.commit.#{expected_commit}", source: {git: git_url(:forked_awesome)} assert_locked "awesome", "0.2.0+git.commit.#{expected_commit}", source: {git: git_url(:forked_awesome)} end end it "can update to nested override version" do metadata = {dependencies: { intermediate: "*", }} lock = {intermediate: "0.1.0", awesome: "0.1.0"} override = {dependencies: { awesome: {git: git_url(:forked_awesome), version: "0.1.0"}, }} with_shard(metadata, lock, override) do run "shards update" assert_installed "awesome", "0.1.0", source: {git: git_url(:forked_awesome)} assert_locked "awesome", "0.1.0", source: {git: git_url(:forked_awesome)} end end it "update to nested latest override if no version" do metadata = {dependencies: { intermediate: "*", }} lock = {intermediate: "0.1.0", awesome: "0.1.0"} override = {dependencies: { awesome: {git: git_url(:forked_awesome)}, # latest version of forked_awesome is 0.2.0 }} with_shard(metadata, lock, override) do run "shards update" assert_installed "awesome", "0.2.0", source: {git: git_url(:forked_awesome)} assert_locked "awesome", "0.2.0", source: {git: git_url(:forked_awesome)} end end it "updating all with override does unlock nested" do metadata = {dependencies: { intermediate: "*", }} lock = {intermediate: "0.1.0", awesome: "0.1.0", d: "0.1.0"} override = {dependencies: { awesome: {git: git_url(:forked_awesome)}, # latest version of forked_awesome is 0.2.0 }} with_shard(metadata, lock, override) do run "shards update" assert_installed "d", "0.2.0" assert_locked "d", "0.2.0" end end describe "mtime" do it "mtime lib > shard.lock > shard.yml" do metadata = {dependencies: { web: "*", }} with_shard(metadata) do run "shards update" File.info("shard.lock").modification_time.should be <= File.info("lib").modification_time File.info("shard.yml").modification_time.should be <= File.info("shard.lock").modification_time run "shards update" File.info("shard.lock").modification_time.should be <= File.info("lib").modification_time File.info("shard.yml").modification_time.should be <= File.info("shard.lock").modification_time end end it "mtime shard.lock > shard.yml even when unmodified" do metadata = {dependencies: { web: "*", }} with_shard(metadata) do run "shards update" File.touch("shard.yml") run "shards update" File.info("shard.lock").modification_time.should be <= File.info("lib").modification_time File.info("shard.yml").modification_time.should be <= File.info("shard.lock").modification_time end end end it "updates lockfile when there are no dependencies" do with_shard({name: "empty"}) do run "shards update" mtime = File.info("shard.lock").modification_time run "shards update" File.info("shard.lock").modification_time.should be >= mtime Shards::Lock.from_file("shard.lock").version.should eq(Shards::Lock::CURRENT_VERSION) end end it "creates ./lib/ when there are no dependencies" do with_shard({name: "empty"}) do File.exists?("./lib/").should be_false run "shards update" File.directory?("./lib/").should be_true end end end shards-0.19.0/spec/integration/version_spec.cr000066400000000000000000000015141473060476400214030ustar00rootroot00000000000000require "./spec_helper" describe "version" do it "version default directory" do metadata = { version: "1.33.7", } with_shard(metadata) do stdout = run "shards version" stdout.should contain("1.33.7") end end it "version within directory" do metadata = { version: "0.0.42", } with_shard(metadata) do inner_path = File.join(application_path, "lib/test") Dir.mkdir_p inner_path outer_path = File.expand_path("..", application_path) Dir.cd(outer_path) do stdout = run "shards version #{Process.quote(inner_path)}" stdout.should contain("0.0.42") end end end it "fails version" do expect_raises(FailedCommand) do root = File.expand_path("/", Dir.current) run "shards version #{Process.quote(root)}" end end end shards-0.19.0/spec/support/000077500000000000000000000000001473060476400155465ustar00rootroot00000000000000shards-0.19.0/spec/support/cli.cr000066400000000000000000000055501473060476400166500ustar00rootroot00000000000000Spec.before_each do path = application_path if File.exists?(path) Shards::Helpers.rm_rf_children(path) else Dir.mkdir_p(path) end end def with_shard(metadata, lock = nil, override = nil, &) Dir.cd(application_path) do File.write "shard.yml", to_shard_yaml(metadata) File.write "shard.lock", to_lock_yaml(lock) if lock File.write "shard.override.yml", to_override_yaml(override) if override yield end end def to_shard_yaml(metadata) String.build do |yml| yml << "name: " << (metadata[:name]? || "test").inspect << '\n' yml << "version: " << (metadata[:version]? || "0.0.0").inspect << '\n' metadata.each do |key, value| if key.to_s.ends_with?("dependencies") write_dependencies(yml, key, value) elsif key.to_s == "targets" yml << "targets:\n" if value.responds_to?(:each) value.each do |target, info| yml << " " << target.to_s << ":\n" if info.responds_to?(:each) info.each do |main, src| yml << " main: " << src.inspect << '\n' end end end end end end end end def to_override_yaml(metadata) String.build do |yml| metadata.each do |key, value| if key.to_s == "dependencies" write_dependencies(yml, key, value) end end end end # This is used for dependencies and development_dependencies private def write_dependencies(yml, key, value) yml << key << ':' if value.responds_to?(:each) yml << '\n' value.each do |name, version| yml << " " << name << ":\n" case version when String yml << " git: " << git_url(name).inspect << '\n' yml << " version: " << version.inspect << '\n' unless version == "*" # when Hash # version.each do |k, v| # yml << " " << k << ": " << v.inspect << '\n' # end when NamedTuple version.each do |k, v| yml << " " << k.to_s << ": " << v.inspect << '\n' end else yml << " git: " << git_url(name).inspect << '\n' end end else yml << value end end def to_lock_yaml(lock) return unless lock YAML.dump({ version: Shards::Lock::CURRENT_VERSION, shards: lock.to_a.to_h do |name, data| if data.is_a?(NamedTuple) git = data[:git] version = data[:version] else git = git_url(name) version = data end {name, {git: git, version: version}} end, }) end module Shards::Specs @@application_path : String? def self.application_path @@application_path ||= File.expand_path("../../tmp/integration", __DIR__).tap do |path| if File.exists?(path) Shards::Helpers.rm_rf_children(path) else Dir.mkdir_p(path) end end end end def application_path Shards::Specs.application_path end shards-0.19.0/spec/support/factories.cr000066400000000000000000000255141473060476400200620ustar00rootroot00000000000000class FailedCommand < Exception getter stdout : String getter stderr : String def initialize(message, @stdout, @stderr) super "#{message}: #{stdout} -- #{stderr}" end end def create_path_repository(project, version = nil) Dir.mkdir_p(File.join(git_path(project), "src")) File.write(File.join(git_path(project), "src", "#{project}.cr"), "module #{project.capitalize}\nend") create_shard project, version if version end def create_git_repository(project, *versions) Dir.cd(tmp_path) do run "git init #{Process.quote(project)}" Dir.cd(git_path(project)) do run "git checkout --orphan master" run "git config user.email author@example.com" run "git config user.name Author" end end Dir.mkdir(File.join(git_path(project), "src")) File.write(File.join(git_path(project), "src", "#{project}.cr"), "module #{project.capitalize}\nend") Dir.cd(git_path(project)) do run "git add #{Process.quote("src/#{project}.cr")}" end versions.each { |version| create_git_release project, version } end def create_fork_git_repository(project, upstream) Dir.cd(tmp_path) do run "git clone #{Process.quote(git_url(upstream))} #{Process.quote(project)}" Dir.cd(git_path(project)) do run "git config user.email fork@example.com" run "git config user.name Fork" end end end def create_git_version_commit(project, version, shard : Bool | NamedTuple = true) Dir.cd(git_path(project)) do if shard contents = shard.is_a?(NamedTuple) ? shard : nil create_shard project, version, contents end Dir.cd(git_path(project)) do name = shard[:name]? if shard.is_a?(NamedTuple) name ||= project File.touch "src/#{name}.cr" run "git add #{Process.quote("src/#{name}.cr")}" end create_git_commit project, "release: v#{version}" end end def create_git_release(project, version, shard : Bool | NamedTuple = true) create_git_version_commit(project, version, shard) create_git_tag(project, "v#{version}") end def create_git_tag(project, version) Dir.cd(git_path(project)) do run "git tag --no-sign #{Process.quote(version)}" end end def create_git_commit(project, message = "new commit") Dir.cd(git_path(project)) do run "git add ." run "git commit --allow-empty --no-gpg-sign -m #{Process.quote(message)}" end end def checkout_new_git_branch(project, branch) Dir.cd(git_path(project)) do run "git checkout -b #{Process.quote(branch)}" end end def checkout_git_branch(project, branch) Dir.cd(git_path(project)) do run "git checkout #{Process.quote(branch)}" end end def create_hg_repository(project, *versions) Dir.cd(tmp_path) do run "hg init #{Process.quote(project)}" end Dir.mkdir(File.join(hg_path(project), "src")) File.write(File.join(hg_path(project), "src", "#{project}.cr"), "module #{project.capitalize}\nend") Dir.cd(hg_path(project)) do run "hg add #{Process.quote("src/#{project}.cr")}" end versions.each { |version| create_hg_release project, version } end def create_fork_hg_repository(project, upstream) Dir.cd(tmp_path) do run "hg clone #{Process.quote(hg_url(upstream))} #{Process.quote(project)}" end end def create_hg_version_commit(project, version, shard : Bool | NamedTuple = true) Dir.cd(hg_path(project)) do if shard contents = shard.is_a?(NamedTuple) ? shard : nil create_shard project, version, contents end Dir.cd(hg_path(project)) do name = shard[:name]? if shard.is_a?(NamedTuple) name ||= project File.touch "src/#{name}.cr" run "hg add #{Process.quote("src/#{name}.cr")}" end create_hg_commit project, "release: v#{version}" end end def create_hg_release(project, version, shard : Bool | NamedTuple = true) create_hg_version_commit(project, version, shard) create_hg_tag(project, "v#{version}") end def create_hg_tag(project, version) Dir.cd(hg_path(project)) do run "hg tag -u #{Process.quote("Your Name ")} #{Process.quote(version)}" end end def create_hg_commit(project, message = "new commit") Dir.cd(hg_path(project)) do File.write("src/#{project}.cr", "# #{message}", mode: "a") run "hg commit -u #{Process.quote("Your Name ")} -A -m #{Process.quote(message)}" end end def checkout_new_hg_bookmark(project, branch) Dir.cd(hg_path(project)) do run "hg bookmark #{Process.quote(branch)}" end end def checkout_new_hg_branch(project, branch) Dir.cd(hg_path(project)) do run "hg branch #{Process.quote(branch)}" end end def checkout_hg_rev(project, rev) Dir.cd(hg_path(project)) do run "hg update -C #{Process.quote(rev)}" end end def create_fossil_repository(project, *versions) Dir.cd(tmp_path) do run "fossil init #{Process.quote(project)}.fossil" # Use a workaround so we don't use --workdir in case the specs are run on a # machine with an old Fossil version. See the #install_sources method in # src/resolvers/fossil.cr Dir.mkdir(fossil_path(project)) unless Dir.exists?(fossil_path(project)) Dir.cd(fossil_path(project)) do run "fossil open #{Process.quote(File.join(tmp_path, project))}.fossil" end end Dir.mkdir(File.join(fossil_path(project), "src")) File.write(File.join(fossil_path(project), "src", "#{project}.cr"), "module #{project.capitalize}\nend") Dir.cd(fossil_path(project)) do run %|fossil add #{Process.quote("src/#{project}.cr")}| end versions.each { |version| create_fossil_release project, version, tag: "v#{version}" } end def create_fossil_release(project, version, shard : Bool | NamedTuple = true, tag : String? = nil) create_fossil_version_commit(project, version, shard, tag) end def create_fossil_version_commit(project, version, shard : Bool | NamedTuple = true, tag : String? = nil) Dir.cd(fossil_path(project)) do if shard contents = shard.is_a?(NamedTuple) ? shard : nil create_shard project, version, contents end name = shard[:name]? if shard.is_a?(NamedTuple) name ||= project File.touch "src/#{name}.cr" run "fossil addremove" create_fossil_commit project, "release: v#{version}", tag end end def create_fossil_commit(project, message = "new commit", tag : String? = nil) Dir.cd(fossil_path(project)) do File.write("src/#{project}.cr", "# #{message}", mode: "a") run "fossil addremove" # Use --hash here to work around a file that's changed, but the size and # mtime are the same. Depending on the resolution of mtime on the # underlying filesystem, shard.yml may fall into this edge case during # testing. # # https://fossil-users.fossil-scm.narkive.com/9ybRAo1U/error-file-is-different-on-disk-compared-to-the-repository-during-commti if tag run "fossil commit --hash --tag #{Process.quote(tag)} -m #{Process.quote(message)}" else run "fossil commit --hash -m #{Process.quote(message)}" end end end def create_fork_fossil_repository(project, upstream) Dir.cd(tmp_path) do run "fossil clone #{Process.quote(fossil_url(upstream))} #{Process.quote(project)}" end end def create_fossil_tag(project, version) Dir.cd(fossil_path(project)) do run "fossil tag add #{Process.quote(version)} current" end end def checkout_new_fossil_branch(project, branch) Dir.cd(fossil_path(project)) do run "fossil branch new #{Process.quote(branch)} current" run "fossil checkout branch" end end def checkout_fossil_rev(project, rev) Dir.cd(fossil_path(project)) do run "fossil checkout #{Process.quote(rev)}" end end def create_shard(project, version, contents : NamedTuple? = nil) spec = {name: project, version: version, crystal: Shards.crystal_version} spec = spec.merge(contents) if contents create_file project, "shard.yml", spec.to_yaml end def create_file(project, filename, contents) path = File.join(git_path(project), filename) parent = File.dirname(path) Dir.mkdir_p(parent) unless Dir.exists?(parent) File.write(path, contents) path end def create_executable(project, filename, source) path = create_file(project, filename + ".cr", source) Dir.cd(File.dirname(path)) do run "crystal build #{Process.quote(File.basename(path))}" end File.delete(path) end def git_commits(project, rev = "HEAD") Dir.cd(git_path(project)) do run("git log --format=%H #{Process.quote(rev)}").strip.split('\n') end end def git_url(project) "file://#{Path[git_path(project)].to_posix}" end def git_path(project) File.join(tmp_path, project.to_s) end def hg_commits(project, rev = ".") Dir.cd(hg_path(project)) do run("hg log --template=#{Process.quote("{node}\n")} -r #{Process.quote(rev)}").strip.split('\n') end end def hg_url(project) "file://#{Path[hg_path(project)].to_posix}" end def hg_path(project) File.join(tmp_path, project.to_s) end def fossil_commits(project, rev = "trunk") # This is using the workaround code in case the machine running the specs is # using an old Fossil version. See the #commit_sha1_at method in # src/resolvers/fossil.cr for info. Dir.cd(fossil_path(project)) do retStr = run("fossil timeline #{Process.quote(rev)} -t ci -W 0").strip.split('\n') retLines = retStr.flat_map do |line| /^.+ \[(.+)\].*/.match(line).try &.[1] end retLines.reject! &.nil? [/artifact:\s+(.+)/.match(run("fossil whatis #{retLines[0]}")).not_nil!.[1]] end end def fossil_url(project) "file://#{Path[fossil_path(project)].to_posix}" end def fossil_path(project) File.join(tmp_path, "#{project.to_s}") end def rel_path(project) "../../spec/.repositories/#{project}" end module Shards::Specs @@tmp_path : String? def self.tmp_path @@tmp_path ||= begin path = File.expand_path("../../.repositories", __FILE__) Dir.mkdir(path) unless Dir.exists?(path) path end end @@crystal_path : String? def self.crystal_path # Memoize so each integration spec do not need to create this process. # If crystal is bin/crystal this also reduce the noise of Using compiled compiler at ... @@crystal_path ||= "#{Shards::INSTALL_DIR}#{Process::PATH_DELIMITER}#{`#{Shards.crystal_bin} env CRYSTAL_PATH`.chomp}" end end def tmp_path Shards::Specs.tmp_path end def run(command, *, env = nil, clear_env = false, input = Process::Redirect::Close) cmd_env = { "CRYSTAL_PATH" => Shards::Specs.crystal_path, } if clear_env cmd_env["CRYSTAL_OPTS"] = "" end cmd_env.merge!(env) if env output, error = IO::Memory.new, IO::Memory.new {% if flag?(:win32) %} # FIXME: Concurrent streams are currently broken on Windows. Need to drop one for now. error = nil {% end %} status = Process.run(command, shell: true, env: cmd_env, input: input, output: output, error: error || Process::Redirect::Close) output = output.to_s.gsub("\r\n", "\n") error = error.to_s.gsub("\r\n", "\n") if status.success? output + error else raise FailedCommand.new("command failed: #{command}", output, error) end end shards-0.19.0/spec/support/requirement.cr000066400000000000000000000007331473060476400204370ustar00rootroot00000000000000def branch(name) Shards::GitBranchRef.new(name) end def commit(sha1) Shards::GitCommitRef.new(sha1) end def hg_bookmark(name) Shards::HgBookmarkRef.new(name) end def hg_branch(name) Shards::HgBranchRef.new(name) end def fossil_branch(name) Shards::FossilBranchRef.new(name) end def version(version) Shards::Version.new(version) end def versions(versions) versions.map { |v| version(v) } end def version_req(pattern) Shards::VersionReq.new(pattern) end shards-0.19.0/spec/unit/000077500000000000000000000000001473060476400150115ustar00rootroot00000000000000shards-0.19.0/spec/unit/dependency_spec.cr000066400000000000000000000057651473060476400205040ustar00rootroot00000000000000require "./spec_helper" module Shards describe Dependency do it "parse for path" do dep = parse_dependency({foo: {path: "/foo"}}) dep.name.should eq("foo") dep.resolver.is_a?(PathResolver).should be_true dep.resolver.source.should eq("/foo") dep.requirement.should eq(Any) end it "parse for git" do dep = parse_dependency({foo: {git: "/foo"}}) dep.name.should eq("foo") dep.resolver.is_a?(GitResolver).should be_true dep.resolver.source.should eq("/foo") dep.requirement.should eq(Any) end it "parse for git with version requirement" do dep = parse_dependency({foo: {git: "/foo", version: "~> 1.2"}}) dep.name.should eq("foo") dep.resolver.is_a?(GitResolver).should be_true dep.resolver.source.should eq("/foo") dep.requirement.should eq(VersionReq.new("~> 1.2")) end it "parse for git with branch requirement" do dep = parse_dependency({foo: {git: "/foo", branch: "test"}}) dep.name.should eq("foo") dep.resolver.is_a?(GitResolver).should be_true dep.resolver.source.should eq("/foo") dep.requirement.should eq(GitBranchRef.new("test")) end it "parse for git with tag requirement" do dep = parse_dependency({foo: {git: "/foo", tag: "test"}}) dep.name.should eq("foo") dep.resolver.is_a?(GitResolver).should be_true dep.resolver.source.should eq("/foo") dep.requirement.should eq(GitTagRef.new("test")) end it "parse for git with commit requirement" do dep = parse_dependency({foo: {git: "/foo", commit: "7e2e840"}}) dep.name.should eq("foo") dep.resolver.is_a?(GitResolver).should be_true dep.resolver.source.should eq("/foo") dep.requirement.should eq(GitCommitRef.new("7e2e840")) end it "parse for github" do dep = parse_dependency({foo: {github: "foo/bar"}}) dep.name.should eq("foo") dep.resolver.is_a?(GitResolver).should be_true dep.resolver.source.should eq("https://github.com/foo/bar.git") end it "allow extra arguments" do dep = parse_dependency({foo: {path: "/foo", branch: "master"}}) dep.name.should eq("foo") dep.resolver.is_a?(PathResolver).should be_true dep.requirement.should eq(Any) end it "format with to_s" do parse_dependency({foo: {git: ""}}).to_s.should eq("foo (*)") parse_dependency({foo: {git: "", version: "~> 1.0"}}).to_s.should eq("foo (~> 1.0)") parse_dependency({foo: {git: "", branch: "feature"}}).to_s.should eq("foo (branch feature)") parse_dependency({foo: {git: "", tag: "rc-1.0"}}).to_s.should eq("foo (tag rc-1.0)") parse_dependency({foo: {git: "", commit: "4478d8afe8c728f44b47d3582a270423cd7fc07d"}}).to_s.should eq("foo (commit 4478d8a)") end end end private def parse_dependency(dep) pull = YAML::PullParser.new(dep.to_yaml) pull.read_stream do pull.read_document do pull.read_mapping do Shards::Dependency.from_yaml(pull) end end end end shards-0.19.0/spec/unit/fossil_resolver_spec.cr000066400000000000000000000202461473060476400215750ustar00rootroot00000000000000require "./spec_helper" private def resolver(name) Shards::FossilResolver.new(name, fossil_url(name)) end module Shards # Allow overriding `source` for the specs class FossilResolver def source=(@source) @origin_url = nil # This needs to be cleared so that #origin_url re-runs `fossil remote-url` end end describe FossilResolver, tags: %w[fossil] do before_each do create_fossil_repository "empty" create_fossil_commit "empty", "initial release" create_fossil_repository "unreleased" create_fossil_version_commit "unreleased", "0.1.0" checkout_new_fossil_branch "unreleased", "branch" create_fossil_commit "unreleased", "testing" checkout_fossil_rev "unreleased", "trunk" create_fossil_repository "unreleased-bm" create_fossil_version_commit "unreleased-bm", "0.1.0" create_fossil_commit "unreleased-bm", "testing" checkout_fossil_rev "unreleased-bm", "trunk" create_fossil_repository "library", "0.0.1", "0.1.0", "0.1.1", "0.1.2", "0.2.0" # Create a version tag not prefixed by 'v' which should be ignored create_fossil_tag "library", "99.9.9" end it "normalizes sources" do # don't normalise other domains FossilResolver.normalize_key_source("fossil", "HTTPs://myfossilserver.com/Repo").should eq({"fossil", "HTTPs://myfossilserver.com/Repo"}) # don't change protocol from ssh FossilResolver.normalize_key_source("fossil", "ssh://fossil@myfossilserver.com/Repo").should eq({"fossil", "ssh://fossil@myfossilserver.com/Repo"}) end it "available releases" do # Since we're working with the local filesystem, we need to use the .fossil files resolver("empty.fossil").available_releases.should be_empty resolver("library.fossil").available_releases.should eq(versions ["0.0.1", "0.1.0", "0.1.1", "0.1.2", "0.2.0"]) end it "latest version for ref" do expect_raises(Shards::Error, "No shard.yml was found for shard \"empty.fossil\" at commit #{fossil_commits(:empty)[0]}") do resolver("empty.fossil").latest_version_for_ref(fossil_branch "tip") end expect_raises(Shards::Error, "No shard.yml was found for shard \"empty.fossil\" at commit #{fossil_commits(:empty)[0]}") do resolver("empty.fossil").latest_version_for_ref(nil) end resolver("unreleased.fossil").latest_version_for_ref(fossil_branch "trunk").should eq(version "0.1.0+fossil.commit.#{fossil_commits(:unreleased)[0]}") resolver("unreleased.fossil").latest_version_for_ref(fossil_branch "branch").should eq(version "0.1.0+fossil.commit.#{fossil_commits(:unreleased, "branch")[0]}") resolver("unreleased.fossil").latest_version_for_ref(nil).should eq(version "0.1.0+fossil.commit.#{fossil_commits(:unreleased)[0]}") resolver("unreleased-bm.fossil").latest_version_for_ref(fossil_branch "trunk").should eq(version "0.1.0+fossil.commit.#{fossil_commits("unreleased-bm")[0]}") resolver("unreleased-bm.fossil").latest_version_for_ref(nil).should eq(version "0.1.0+fossil.commit.#{fossil_commits("unreleased-bm")[0]}") resolver("library.fossil").latest_version_for_ref(fossil_branch "trunk").should eq(version "0.2.0+fossil.commit.#{fossil_commits(:library)[0]}") resolver("library.fossil").latest_version_for_ref(nil).should eq(version "0.2.0+fossil.commit.#{fossil_commits(:library)[0]}") expect_raises(Shards::Error, "Could not find branch foo for shard \"library.fossil\" in the repository #{fossil_url(:library)}") do resolver("library.fossil").latest_version_for_ref(fossil_branch "foo") end end it "versions for" do expect_raises(Shards::Error, "No shard.yml was found for shard \"empty.fossil\" at commit #{fossil_commits(:empty)[0]}") do resolver("empty.fossil").versions_for(Any) end resolver("library.fossil").versions_for(Any).should eq(versions ["0.0.1", "0.1.0", "0.1.1", "0.1.2", "0.2.0"]) resolver("library.fossil").versions_for(VersionReq.new "~> 0.1.0").should eq(versions ["0.1.0", "0.1.1", "0.1.2"]) resolver("library.fossil").versions_for(fossil_branch "trunk").should eq(versions ["0.2.0+fossil.commit.#{fossil_commits(:library)[0]}"]) resolver("unreleased.fossil").versions_for(fossil_branch "trunk").should eq(versions ["0.1.0+fossil.commit.#{fossil_commits(:unreleased)[0]}"]) resolver("unreleased.fossil").versions_for(Any).should eq(versions ["0.1.0+fossil.commit.#{fossil_commits(:unreleased)[0]}"]) resolver("unreleased-bm.fossil").versions_for(fossil_branch "trunk").should eq(versions ["0.1.0+fossil.commit.#{fossil_commits("unreleased-bm")[0]}"]) resolver("unreleased-bm.fossil").versions_for(Any).should eq(versions ["0.1.0+fossil.commit.#{fossil_commits("unreleased-bm")[0]}"]) end it "read spec for release" do spec = resolver("library.fossil").spec(version "0.1.1") spec.original_version.should eq(version "0.1.1") spec.version.should eq(version "0.1.1") end it "read spec for commit" do version = version("0.2.0+fossil.commit.#{fossil_commits(:library)[0]}") spec = resolver("library.fossil").spec(version) spec.original_version.should eq(version "0.2.0") spec.version.should eq(version) end it "install" do library = resolver("library.fossil") library.install_sources(version("0.1.2"), install_path("library")) File.exists?(install_path("library", "src/library.cr")).should be_true File.exists?(install_path("library", "shard.yml")).should be_true Spec.from_file(install_path("library", "shard.yml")).version.should eq(version "0.1.2") library.install_sources(version("0.2.0"), install_path("library")) Spec.from_file(install_path("library", "shard.yml")).version.should eq(version "0.2.0") end it "install commit" do library = resolver("library.fossil") version = version "0.2.0+fossil.commit.#{fossil_commits(:library)[0]}" library.install_sources(version, install_path("library")) Spec.from_file(install_path("library", "shard.yml")).version.should eq(version "0.2.0") end it "origin changed" do library = FossilResolver.new("library", fossil_url("library.fossil")) library.install_sources(version("0.1.2"), install_path("library")) # Change the origin in the cache repo to https://foss.heptapod.net/foo/bar Dir.cd(library.local_path) do run "fossil remote-url -R #{library.name}.fossil https://foss.heptapod.net/foo/bar" end # All of these alternatives should not trigger origin as changed same_origins = [ "https://foss.heptapod.net/foo/bar", "https://foss.heptapod.net:1234/foo/bar", "http://foss.heptapod.net/foo/bar", "ssh://foss.heptapod.net/foo/bar", "bob@foss.heptapod.net:foo/bar", "foss.heptapod.net:foo/bar", ] same_origins.each do |origin| library.source = origin library.origin_changed?.should be_false end # These alternatives should all trigger origin as changed changed_origins = [ "https://foss.heptapod.net/foo/bar2", "https://foss.heptapod.net/foos/bar", "https://hghubz.com/foo/bar", "file:///foss.heptapod.net/foo/bar", "hg@foss.heptapod.net:foo/bar2", "hg@foss.heptapod2.net.com:foo/bar", "", ] changed_origins.each do |origin| library.source = origin library.origin_changed?.should be_true end end it "renders report version" do resolver("library.fossil").report_version(version "1.2.3").should eq("1.2.3") resolver("library.fossil").report_version(version "1.2.3+fossil.commit.654875c9dbfa8d72fba70d65fd548d51ffb85aff").should eq("1.2.3 at 654875c") end it "#matches_ref" do resolver = FossilResolver.new("", "") resolver.matches_ref?(FossilCommitRef.new("1234567890abcdef"), Shards::Version.new("0.1.0.+fossil.commit.1234567")).should be_true resolver.matches_ref?(FossilCommitRef.new("1234567890abcdef"), Shards::Version.new("0.1.0.+fossil.commit.1234567890abcdef")).should be_true resolver.matches_ref?(FossilCommitRef.new("1234567"), Shards::Version.new("0.1.0.+fossil.commit.1234567890abcdef")).should be_true end end end shards-0.19.0/spec/unit/git_resolver_spec.cr000066400000000000000000000217121473060476400210600ustar00rootroot00000000000000require "./spec_helper" private def resolver(name) Shards::GitResolver.new(name, git_url(name)) end module Shards # Allow overriding `source` for the specs class GitResolver def source=(@source) end end describe GitResolver, tags: %w[git] do before_each do create_git_repository "empty" create_git_commit "empty", "initial release" create_git_repository "unreleased" create_git_version_commit "unreleased", "0.1.0" checkout_new_git_branch "unreleased", "branch" create_git_commit "unreleased", "testing" checkout_git_branch "unreleased", "master" create_git_repository "library", "0.0.1", "0.1.0", "0.1.1", "0.1.2", "0.2.0" # Create a version tag not prefixed by 'v' which should be ignored create_git_tag "library", "99.9.9" end it "normalizes github bitbucket gitlab sources" do # deal with case insensitive paths GitResolver.normalize_key_source("github", "repo/path").should eq({"git", "https://github.com/repo/path.git"}) GitResolver.normalize_key_source("github", "rEpo/pAth").should eq({"git", "https://github.com/repo/path.git"}) GitResolver.normalize_key_source("github", "REPO/PATH").should eq({"git", "https://github.com/repo/path.git"}) GitResolver.normalize_key_source("bitbucket", "repo/path").should eq({"git", "https://bitbucket.com/repo/path.git"}) GitResolver.normalize_key_source("bitbucket", "rEpo/pAth").should eq({"git", "https://bitbucket.com/repo/path.git"}) GitResolver.normalize_key_source("bitbucket", "REPO/PATH").should eq({"git", "https://bitbucket.com/repo/path.git"}) GitResolver.normalize_key_source("gitlab", "repo/path").should eq({"git", "https://gitlab.com/repo/path.git"}) GitResolver.normalize_key_source("gitlab", "rEpo/pAth").should eq({"git", "https://gitlab.com/repo/path.git"}) GitResolver.normalize_key_source("gitlab", "REPO/PATH").should eq({"git", "https://gitlab.com/repo/path.git"}) GitResolver.normalize_key_source("codeberg", "REPO/PATH").should eq({"git", "https://codeberg.org/repo/path.git"}) # normalise full git paths GitResolver.normalize_key_source("git", "HTTPS://User:Pass@Github.com/Repo/Path.git?Shallow=true")[1].should eq "https://User:Pass@github.com/repo/path.git?Shallow=true" GitResolver.normalize_key_source("git", "HTTPS://User:Pass@Bitbucket.com/Repo/Path.Git?Shallow=true")[1].should eq "https://User:Pass@bitbucket.com/repo/path.git?Shallow=true" GitResolver.normalize_key_source("git", "HTTPS://User:Pass@Gitlab.com/Repo/Path?Shallow=true")[1].should eq "https://User:Pass@gitlab.com/repo/path.git?Shallow=true" GitResolver.normalize_key_source("git", "HTTPS://User:Pass@www.Github.com/Repo/Path?Shallow=true")[1].should eq "https://User:Pass@github.com/repo/path.git?Shallow=true" GitResolver.normalize_key_source("git", "HTTPS://User:Pass@codeBerg.org/Repo/Path.Git?Shallow=true")[1].should eq "https://User:Pass@codeberg.org/repo/path.git?Shallow=true" # don't normalise other domains GitResolver.normalize_key_source("git", "HTTPs://mygitserver.com/Repo.git").should eq({"git", "HTTPs://mygitserver.com/Repo.git"}) # don't change protocol from ssh GitResolver.normalize_key_source("git", "ssh://git@github.com/Repo/Path?Shallow=true").should eq({"git", "ssh://git@github.com/Repo/Path?Shallow=true"}) end it "available releases" do resolver("empty").available_releases.should be_empty resolver("library").available_releases.should eq(versions ["0.0.1", "0.1.0", "0.1.1", "0.1.2", "0.2.0"]) end it "latest version for ref" do expect_raises(Shards::Error, "No shard.yml was found for shard \"empty\" at commit #{git_commits(:empty)[0]}") do resolver("empty").latest_version_for_ref(branch "master") end expect_raises(Shards::Error, "No shard.yml was found for shard \"empty\" at commit #{git_commits(:empty)[0]}") do resolver("empty").latest_version_for_ref(nil) end resolver("unreleased").latest_version_for_ref(branch "master").should eq(version "0.1.0+git.commit.#{git_commits(:unreleased)[0]}") resolver("unreleased").latest_version_for_ref(branch "branch").should eq(version "0.1.0+git.commit.#{git_commits(:unreleased, "branch")[0]}") resolver("unreleased").latest_version_for_ref(nil).should eq(version "0.1.0+git.commit.#{git_commits(:unreleased)[0]}") resolver("library").latest_version_for_ref(branch "master").should eq(version "0.2.0+git.commit.#{git_commits(:library)[0]}") resolver("library").latest_version_for_ref(nil).should eq(version "0.2.0+git.commit.#{git_commits(:library)[0]}") expect_raises(Shards::Error, "Could not find branch foo for shard \"library\" in the repository #{git_url(:library)}") do resolver("library").latest_version_for_ref(branch "foo") end end it "versions for" do expect_raises(Shards::Error, "No shard.yml was found for shard \"empty\" at commit #{git_commits(:empty)[0]}") do resolver("empty").versions_for(Any) end resolver("library").versions_for(Any).should eq(versions ["0.0.1", "0.1.0", "0.1.1", "0.1.2", "0.2.0"]) resolver("library").versions_for(VersionReq.new "~> 0.1.0").should eq(versions ["0.1.0", "0.1.1", "0.1.2"]) resolver("library").versions_for(branch "master").should eq(versions ["0.2.0+git.commit.#{git_commits(:library)[0]}"]) resolver("unreleased").versions_for(branch "master").should eq(versions ["0.1.0+git.commit.#{git_commits(:unreleased)[0]}"]) resolver("unreleased").versions_for(Any).should eq(versions ["0.1.0+git.commit.#{git_commits(:unreleased)[0]}"]) end it "read spec for release" do spec = resolver("library").spec(version "0.1.1") spec.original_version.should eq(version "0.1.1") spec.version.should eq(version "0.1.1") end it "read spec for commit" do version = version("0.2.0+git.commit.#{git_commits(:library)[0]}") spec = resolver("library").spec(version) spec.original_version.should eq(version "0.2.0") spec.version.should eq(version) end it "install" do library = resolver("library") library.install_sources(version("0.1.2"), install_path("library")) File.exists?(install_path("library", "src/library.cr")).should be_true File.exists?(install_path("library", "shard.yml")).should be_true Spec.from_file(install_path("library", "shard.yml")).version.should eq(version "0.1.2") library.install_sources(version("0.2.0"), install_path("library")) Spec.from_file(install_path("library", "shard.yml")).version.should eq(version "0.2.0") end it "install commit" do library = resolver("library") version = version "0.2.0+git.commit.#{git_commits(:library)[0]}" library.install_sources(version, install_path("library")) Spec.from_file(install_path("library", "shard.yml")).version.should eq(version "0.2.0") end it "origin changed" do library = GitResolver.new("library", git_url("library")) library.install_sources(version("0.1.2"), install_path("library")) # Change the origin in the cache repo to https://github.com/foo/bar Dir.cd(library.local_path) do run "git remote set-url origin https://github.com/foo/bar" end # # All of these alternatives should not trigger origin as changed same_origins = [ "https://github.com/foo/bar", "https://github.com:1234/foo/bar", "http://github.com/foo/bar", "ssh://github.com/foo/bar", "git://github.com/foo/bar", "rsync://github.com/foo/bar", "git@github.com:foo/bar", "bob@github.com:foo/bar", "github.com:foo/bar", ] same_origins.each do |origin| library.source = origin library.origin_changed?.should be_false end # These alternatives should all trigger origin as changed changed_origins = [ "https://github.com/foo/bar2", "https://github.com/foos/bar", "https://githubz.com/foo/bar", "file:///github.com/foo/bar", "git@github.com:foo/bar2", "git@github2.com:foo/bar", "", ] changed_origins.each do |origin| library.source = origin library.origin_changed?.should be_true end end it "renders report version" do resolver("library").report_version(version "1.2.3").should eq("1.2.3") resolver("library").report_version(version "1.2.3+git.commit.654875c9dbfa8d72fba70d65fd548d51ffb85aff").should eq("1.2.3 at 654875c") end it "#matches_ref" do resolver = GitResolver.new("", "") resolver.matches_ref?(GitCommitRef.new("1234567890abcdef"), Shards::Version.new("0.1.0.+git.commit.1234567")).should be_true resolver.matches_ref?(GitCommitRef.new("1234567890abcdef"), Shards::Version.new("0.1.0.+git.commit.1234567890abcdef")).should be_true resolver.matches_ref?(GitCommitRef.new("1234567"), Shards::Version.new("0.1.0.+git.commit.1234567890abcdef")).should be_true end end end shards-0.19.0/spec/unit/hg_resolver_spec.cr000066400000000000000000000176551473060476400207060ustar00rootroot00000000000000require "./spec_helper" private def resolver(name) Shards::HgResolver.new(name, hg_url(name)) end module Shards # Allow overriding `source` for the specs class HgResolver def source=(@source) end end describe HgResolver, tags: %w[hg] do before_each do create_hg_repository "empty" create_hg_commit "empty", "initial release" create_hg_repository "unreleased" create_hg_version_commit "unreleased", "0.1.0" checkout_new_hg_branch "unreleased", "branch" create_hg_commit "unreleased", "testing" checkout_hg_rev "unreleased", "default" create_hg_repository "unreleased-bm" create_hg_version_commit "unreleased-bm", "0.1.0" checkout_new_hg_bookmark "unreleased-bm", "branch" create_hg_commit "unreleased-bm", "testing" checkout_hg_rev "unreleased-bm", "default" create_hg_repository "library", "0.0.1", "0.1.0", "0.1.1", "0.1.2", "0.2.0" # Create a version tag not prefixed by 'v' which should be ignored create_hg_tag "library", "99.9.9" end it "normalizes github bitbucket gitlab sources" do # don't normalise other domains HgResolver.normalize_key_source("hg", "HTTPs://myhgserver.com/Repo").should eq({"hg", "HTTPs://myhgserver.com/Repo"}) # don't change protocol from ssh HgResolver.normalize_key_source("hg", "ssh://hg@myhgserver.com/Repo").should eq({"hg", "ssh://hg@myhgserver.com/Repo"}) end it "available releases" do resolver("empty").available_releases.should be_empty resolver("library").available_releases.should eq(versions ["0.0.1", "0.1.0", "0.1.1", "0.1.2", "0.2.0"]) end it "latest version for ref" do expect_raises(Shards::Error, "No shard.yml was found for shard \"empty\" at commit #{hg_commits(:empty)[0]}") do resolver("empty").latest_version_for_ref(hg_branch "default") end expect_raises(Shards::Error, "No shard.yml was found for shard \"empty\" at commit #{hg_commits(:empty)[0]}") do resolver("empty").latest_version_for_ref(nil) end resolver("unreleased").latest_version_for_ref(hg_branch "default").should eq(version "0.1.0+hg.commit.#{hg_commits(:unreleased)[0]}") resolver("unreleased").latest_version_for_ref(hg_branch "branch").should eq(version "0.1.0+hg.commit.#{hg_commits(:unreleased, "branch")[0]}") resolver("unreleased").latest_version_for_ref(nil).should eq(version "0.1.0+hg.commit.#{hg_commits(:unreleased)[0]}") resolver("unreleased-bm").latest_version_for_ref(hg_branch "default").should eq(version "0.1.0+hg.commit.#{hg_commits("unreleased-bm")[0]}") resolver("unreleased-bm").latest_version_for_ref(hg_bookmark "branch").should eq(version "0.1.0+hg.commit.#{hg_commits("unreleased-bm", "branch")[0]}") resolver("unreleased-bm").latest_version_for_ref(nil).should eq(version "0.1.0+hg.commit.#{hg_commits("unreleased-bm")[0]}") resolver("library").latest_version_for_ref(hg_branch "default").should eq(version "0.2.0+hg.commit.#{hg_commits(:library)[0]}") resolver("library").latest_version_for_ref(nil).should eq(version "0.2.0+hg.commit.#{hg_commits(:library)[0]}") expect_raises(Shards::Error, "Could not find branch foo for shard \"library\" in the repository #{hg_url(:library)}") do resolver("library").latest_version_for_ref(hg_branch "foo") end end it "versions for" do expect_raises(Shards::Error, "No shard.yml was found for shard \"empty\" at commit #{hg_commits(:empty)[0]}") do resolver("empty").versions_for(Any) end resolver("library").versions_for(Any).should eq(versions ["0.0.1", "0.1.0", "0.1.1", "0.1.2", "0.2.0"]) resolver("library").versions_for(VersionReq.new "~> 0.1.0").should eq(versions ["0.1.0", "0.1.1", "0.1.2"]) resolver("library").versions_for(hg_branch "default").should eq(versions ["0.2.0+hg.commit.#{hg_commits(:library)[0]}"]) resolver("unreleased").versions_for(hg_branch "default").should eq(versions ["0.1.0+hg.commit.#{hg_commits(:unreleased)[0]}"]) resolver("unreleased").versions_for(Any).should eq(versions ["0.1.0+hg.commit.#{hg_commits(:unreleased)[0]}"]) resolver("unreleased-bm").versions_for(hg_branch "default").should eq(versions ["0.1.0+hg.commit.#{hg_commits("unreleased-bm")[0]}"]) resolver("unreleased-bm").versions_for(Any).should eq(versions ["0.1.0+hg.commit.#{hg_commits("unreleased-bm")[0]}"]) end it "read spec for release" do spec = resolver("library").spec(version "0.1.1") spec.original_version.should eq(version "0.1.1") spec.version.should eq(version "0.1.1") end it "read spec for commit" do version = version("0.2.0+hg.commit.#{hg_commits(:library)[0]}") spec = resolver("library").spec(version) spec.original_version.should eq(version "0.2.0") spec.version.should eq(version) end it "install" do library = resolver("library") library.install_sources(version("0.1.2"), install_path("library")) File.exists?(install_path("library", "src/library.cr")).should be_true File.exists?(install_path("library", "shard.yml")).should be_true Spec.from_file(install_path("library", "shard.yml")).version.should eq(version "0.1.2") library.install_sources(version("0.2.0"), install_path("library")) Spec.from_file(install_path("library", "shard.yml")).version.should eq(version "0.2.0") end it "install commit" do library = resolver("library") version = version "0.2.0+hg.commit.#{hg_commits(:library)[0]}" library.install_sources(version, install_path("library")) Spec.from_file(install_path("library", "shard.yml")).version.should eq(version "0.2.0") end it "origin changed" do library = HgResolver.new("library", hg_url("library")) library.install_sources(version("0.1.2"), install_path("library")) # Change the origin in the cache repo to https://foss.heptapod.net/foo/bar hgrc_path = File.join(library.local_path, ".hg", "hgrc") hgrc = File.read(hgrc_path) hgrc = hgrc.gsub(/(default\s*=\s*)([^\r\n]*)/, "\\1https://foss.heptapod.net/foo/bar") File.write(hgrc_path, hgrc) # # All of these alternatives should not trigger origin as changed same_origins = [ "https://foss.heptapod.net/foo/bar", "https://foss.heptapod.net:1234/foo/bar", "http://foss.heptapod.net/foo/bar", "ssh://foss.heptapod.net/foo/bar", "hg://foss.heptapod.net/foo/bar", "rsync://foss.heptapod.net/foo/bar", "hg@foss.heptapod.net:foo/bar", "bob@foss.heptapod.net:foo/bar", "foss.heptapod.net:foo/bar", ] same_origins.each do |origin| library.source = origin library.origin_changed?.should be_false end # These alternatives should all trigger origin as changed changed_origins = [ "https://foss.heptapod.net/foo/bar2", "https://foss.heptapod.net/foos/bar", "https://hghubz.com/foo/bar", "file:///foss.heptapod.net/foo/bar", "hg@foss.heptapod.net:foo/bar2", "hg@foss.heptapod2.net.com:foo/bar", "", ] changed_origins.each do |origin| library.source = origin library.origin_changed?.should be_true end end it "renders report version" do resolver("library").report_version(version "1.2.3").should eq("1.2.3") resolver("library").report_version(version "1.2.3+hg.commit.654875c9dbfa8d72fba70d65fd548d51ffb85aff").should eq("1.2.3 at 654875c") end it "#matches_ref" do resolver = HgResolver.new("", "") resolver.matches_ref?(HgCommitRef.new("1234567890abcdef"), Shards::Version.new("0.1.0.+hg.commit.1234567")).should be_true resolver.matches_ref?(HgCommitRef.new("1234567890abcdef"), Shards::Version.new("0.1.0.+hg.commit.1234567890abcdef")).should be_true resolver.matches_ref?(HgCommitRef.new("1234567"), Shards::Version.new("0.1.0.+hg.commit.1234567890abcdef")).should be_true end end end shards-0.19.0/spec/unit/info_spec.cr000066400000000000000000000020541473060476400173050ustar00rootroot00000000000000require "./spec_helper" module Shards describe Info do before_each do Helpers.rm_rf(Shards.install_path) end it "create with default install directory" do info = Info.new info.install_path.should eq(install_path) info.installed.should be_empty end it "reads existing file" do Dir.mkdir_p(install_path) File.write File.join(install_path, ".shards.info"), SAMPLE_INFO info = Info.new info.installed.should eq({ "foo" => Package.new("foo", GitResolver.new("foo", "https://example.com/foo.git"), version "1.2.3"), }) end it "save changes" do info = Info.new dep = Package.new("foo", GitResolver.new("foo", "https://example.com/foo.git"), version "1.2.3") info.installed["foo"] = dep info.save info_file = File.read File.join(install_path, ".shards.info") info_file.should eq(SAMPLE_INFO) end end SAMPLE_INFO = <<-YAML --- version: 1.0 shards: foo: git: https://example.com/foo.git version: 1.2.3 YAML end shards-0.19.0/spec/unit/lock_spec.cr000066400000000000000000000045721473060476400173110ustar00rootroot00000000000000require "./spec_helper" require "../../src/lock" module Shards describe Lock do it "parses" do create_git_repository "library", "0.1.0" lock = Lock.from_yaml <<-YAML version: 1.0 shards: repo: github: user/repo version: 1.2.3 example: git: #{git_url(:library)} commit: #{git_commits(:library)[0]} new_git: git: https://example.com/new.git version: 1.2.3+git.commit.0d246ee6c52d4e758651b8669a303f04be9a2a96 new_path: path: ../path version: 0.1.2 YAML lock.version.should eq("1.0") shards = lock.shards shards.size.should eq(4) shards[0].name.should eq("repo") shards[0].resolver.should eq(GitResolver.new("repo", "https://github.com/user/repo.git")) shards[0].version.should eq(version "1.2.3") shards[0].to_s.should eq("repo (1.2.3)") shards[1].name.should eq("example") shards[1].resolver.should eq(GitResolver.new("example", git_url(:library))) shards[1].version.should eq(version "0.1.0+git.commit.#{git_commits(:library)[0]}") shards[1].to_s.should eq("example (0.1.0 at #{git_commits(:library)[0][0...7]})") shards[2].name.should eq("new_git") shards[2].resolver.should eq(GitResolver.new("new_git", "https://example.com/new.git")) shards[2].version.should eq(version "1.2.3+git.commit.0d246ee6c52d4e758651b8669a303f04be9a2a96") shards[2].to_s.should eq("new_git (1.2.3 at 0d246ee)") shards[3].name.should eq("new_path") shards[3].resolver.should eq(PathResolver.new("new_path", "../path")) shards[3].version.should eq(version "0.1.2") shards[3].to_s.should eq("new_path (0.1.2 at ../path)") end it "raises on unknown version" do expect_raises(InvalidLock, "Unsupported #{LOCK_FILENAME}.") { Lock.from_yaml("version: 99\n") } end it "raises on invalid format" do expect_raises(Error, "Invalid #{LOCK_FILENAME}.") { Lock.from_yaml("") } expect_raises(Error, "Invalid #{LOCK_FILENAME}.") { Lock.from_yaml("version: 1.0\n") } expect_raises(Error, "Invalid #{LOCK_FILENAME}.") { Lock.from_yaml("version: 1.0\nshards:\n") } end it "parses empty shards" do lock = Lock.from_yaml <<-YAML version: 2.0 shards: {} YAML lock.shards.empty?.should be_true end end end shards-0.19.0/spec/unit/override_spec.cr000066400000000000000000000062141473060476400201730ustar00rootroot00000000000000require "./spec_helper" require "../../src/override" module Shards describe Override do it "parses" do override = Override.from_yaml <<-YAML dependencies: repo: github: user/repo version: 1.2.3 example: git: https://example.com/example-crystal.git branch: master local: path: /var/clones/local YAML override.dependencies.size.should eq(3) override.dependencies[0].name.should eq("repo") override.dependencies[0].resolver.should eq(GitResolver.new("repo", "https://github.com/user/repo.git")) override.dependencies[0].requirement.should eq(version_req "1.2.3") override.dependencies[1].name.should eq("example") override.dependencies[1].resolver.should eq(GitResolver.new("example", "https://example.com/example-crystal.git")) override.dependencies[1].requirement.should eq(branch "master") override.dependencies[2].name.should eq("local") override.dependencies[2].resolver.should eq(PathResolver.new("local", "/var/clones/local")) override.dependencies[2].requirement.should eq(Any) end it "fails dependency with duplicate resolver" do expect_raises Shards::ParseError, %(Duplicate resolver mapping for dependency "foo" at line 4, column 5) do Override.from_yaml <<-YAML dependencies: foo: github: user/repo gitlab: user/repo YAML end end it "fails dependency with missing resolver" do expect_raises Shards::ParseError, %(Missing resolver for dependency "foo" at line 2, column 3) do Override.from_yaml <<-YAML dependencies: foo: branch: master YAML end end it "accepts dependency with extra attributes" do override = Override.from_yaml <<-YAML dependencies: foo: github: user/repo extra: foobar YAML dep = Dependency.new("foo", GitResolver.new("foo", "https://github.com/user/repo.git"), Any) override.dependencies[0].should eq dep end it "skips unknown attributes" do override = Override.from_yaml("\nanme: test\ncustom:\n test: more\nname: test\nversion: 1\n") override.dependencies.should be_empty end it "raises on unknown attributes if validating" do expect_raises(ParseError, "unknown attribute: deps") { Override.from_yaml("deps:", validate: true) } end it "fails to parse dependencies" do str = <<-YAML dependencies: github: spalger/crystal-mime branch: master YAML expect_raises(ParseError) { Override.from_yaml(str) } end it "errors on duplicate attributes" do expect_raises(ParseError, %(duplicate attribute "dependencies")) do Override.from_yaml <<-YAML dependencies: bar: github: foo/bar dependencies: baz: github: foo/baz YAML end end it "parses empty dependencies" do override = Override.from_yaml("dependencies: {}\n") override.dependencies.should be_empty end end end shards-0.19.0/spec/unit/package_spec.cr000066400000000000000000000047551473060476400177570ustar00rootroot00000000000000require "./spec_helper" require "../../src/package" private def resolver(name) Shards::PathResolver.new(name, git_path(name)) end private def git_resolver(name) Shards::GitResolver.new(name, git_path(name)) end module Shards describe Package do before_each do create_path_repository "library", "1.2.3" create_git_repository "repo", "0.1.2", "0.1.3" end it "installs" do package = Package.new("library", resolver("library"), version "1.2.3") package.installed?.should be_false package.install package.installed?.should be_true end it "reads spec from installed dir" do package = Package.new("repo", git_resolver("repo"), version "0.1.2") package.install File.open(install_path("repo", "shard.yml"), "a") do |f| f.puts "license: FOO" end package.spec.license.should eq("FOO") end it "fallback to resolver to read spec" do package = Package.new("repo", git_resolver("repo"), version "0.1.2") package.install File.delete install_path("repo", "shard.yml") package.spec.version.should eq(version "0.1.2") end it "reads spec from resolver if not installed" do package = Package.new("repo", git_resolver("repo"), version "0.1.3") package.install package = Package.new("repo", git_resolver("repo"), version "0.1.2") package.spec.original_version.should eq(version "0.1.2") end it "different version is not installed" do package = Package.new("library", resolver("library"), version "1.2.3") package.install package2 = Package.new("library", resolver("library"), version "2.0.0") package2.installed?.should be_false end it "different resolver is not installed" do package = Package.new("library", resolver("library"), version "1.2.3") package.install package2 = Package.new("library", resolver("foo"), version "1.2.3") package2.installed?.should be_false end it "not installed if missing target" do package = Package.new("library", resolver("library"), version "1.2.3") package.install Shards::Helpers.rm_rf(install_path("library")) package.installed?.should be_false end it "cleanups target before installing" do Dir.mkdir_p(install_path) File.touch(install_path("library")) package = Package.new("library", resolver("library"), version "1.2.3") package.install File.symlink?(install_path("library")).should be_true end end end shards-0.19.0/spec/unit/path_resolver_spec.cr000066400000000000000000000022461473060476400212320ustar00rootroot00000000000000require "./spec_helper" private def resolver(name) Shards::PathResolver.new(name, git_path(name)) end module Shards describe PathResolver do before_each do create_path_repository "library", "1.2.3" end it "available versions" do resolver("library").available_releases.should eq([version "1.2.3"]) end it "read spec" do resolver("library").spec("1.2.3").version.should eq(version "1.2.3") end it "install" do resolver("library").tap do |library| library.install_sources(version("1.2.3"), install_path("library")) File.exists?(install_path("library", "src/library.cr")).should be_true File.exists?(install_path("library", "shard.yml")).should be_true Spec.from_file(install_path("library", "shard.yml")).version.should eq(version "1.2.3") end end it "install fails when path doesnt exist" do expect_raises(Error) do resolver("unknown").install_sources(version("1.0.0"), install_path("unknown")) end end it "renders report version" do resolver("library").report_version(version "1.2.3").should eq("1.2.3 at #{git_path("library")}") end end end shards-0.19.0/spec/unit/resolver_spec.cr000066400000000000000000000021121473060476400202060ustar00rootroot00000000000000require "./spec_helper" module Shards describe Resolver do it "find resolver" do Resolver.find_resolver("git", "test", "file:///tmp/test") .should eq(GitResolver.new("test", "file:///tmp/test")) end it "compares" do resolver = PathResolver.new("name", "/path") resolver.should eq(resolver) resolver.should eq(PathResolver.new("name", "/path")) resolver.should_not eq(PathResolver.new("name2", "/path")) resolver.should_not eq(PathResolver.new("name", "/path2")) resolver.should_not eq(GitResolver.new("name", "/path")) end describe "#spec" do it "reports parse error location" do create_path_repository "foo", "1.2.3" create_file "foo", "shard.yml", "name: foo\nname: foo\n" resolver = Shards::PathResolver.new("foo", git_path("foo")) error = expect_raises(ParseError, %(Error in foo:shard.yml: duplicate attribute "name" at line 2, column 1)) do resolver.spec Shards::Version.new("1.2.3") end error.resolver.should eq resolver end end end end shards-0.19.0/spec/unit/spec_helper.cr000066400000000000000000000013071473060476400176310ustar00rootroot00000000000000ENV["SHARDS_CACHE_PATH"] = ".shards" ENV["SHARDS_INSTALL_PATH"] = File.expand_path(".lib", __DIR__) require "spec" require "../../src/config" require "../../src/helpers" require "../../src/logger" require "../../src/resolvers/*" require "../support/factories" require "../support/requirement" module Shards set_warning_log_level end Spec.before_each do clear_repositories Shards::Resolver.clear_resolver_cache Shards.info.reload end private def clear_repositories Shards::Helpers.rm_rf_children(tmp_path) Shards::Helpers.rm_rf(Shards.cache_path) Shards::Helpers.rm_rf(Shards.install_path) end def install_path(project, *path_names) File.join(Shards.install_path, project, *path_names) end shards-0.19.0/spec/unit/spec_spec.cr000066400000000000000000000226131473060476400173070ustar00rootroot00000000000000require "./spec_helper" module Shards describe Spec do it "parse minimal shard" do spec = Spec.from_yaml("name: shards\nversion: 0.1.0\n") spec.name.should eq("shards") spec.version.should eq(version "0.1.0") spec.description.should be_nil spec.license.should be_nil spec.authors.should be_empty spec.read_from_yaml?.should be_true end it "create without yaml" do spec = Spec.new("name", version "0.1.0") spec.version.should eq(version "0.1.0") spec.read_from_yaml?.should be_false end it "parse description" do spec = Spec.from_yaml("name: shards\nversion: 0.1.0\ndescription: short description") spec.description.should eq("short description") spec = Spec.from_yaml("name: shards\nversion: 0.1.0\ndescription: |\n slightly longer description") spec.description.should eq("slightly longer description") end it "parse license" do spec = Spec.from_yaml("name: shards\nversion: 0.1.0\nlicense: BSD-2-Clause") spec.license.should eq("BSD-2-Clause") spec.license_url.should eq("https://spdx.org/licenses/BSD-2-Clause") spec = Spec.from_yaml("name: shards\nversion: 0.1.0\nlicense: http://example.com/LICENSE") spec.license.should eq("http://example.com/LICENSE") spec.license_url.should eq("http://example.com/LICENSE") end it "parse crystal" do spec = Spec.from_yaml("name: shards\nversion: 0.7.0\ncrystal: 0.20.0") spec.crystal.should eq("0.20.0") end it "parse authors" do spec = Spec.from_yaml("name: shards\nversion: 0.1.0\nauthors:\n - Julien Portalier \n - Ary") spec.authors.size.should eq(2) spec.authors[0].name.should eq("Julien Portalier") spec.authors[0].email.should eq("julien@portalier.com") spec.authors[1].name.should eq("Ary") spec.authors[1].email.should be_nil end it "parse dependencies" do spec = Spec.from_yaml <<-YAML name: orm version: 1.0.0 dependencies: repo: github: user/repo version: 1.2.3 example: git: https://example.com/example-crystal.git branch: master local: path: /var/clones/local YAML spec.dependencies.size.should eq(3) spec.dependencies[0].name.should eq("repo") spec.dependencies[0].resolver.should eq(GitResolver.new("repo", "https://github.com/user/repo.git")) spec.dependencies[0].requirement.should eq(version_req "1.2.3") spec.dependencies[1].name.should eq("example") spec.dependencies[1].resolver.should eq(GitResolver.new("example", "https://example.com/example-crystal.git")) spec.dependencies[1].requirement.should eq(branch "master") spec.dependencies[2].name.should eq("local") spec.dependencies[2].resolver.should eq(PathResolver.new("local", "/var/clones/local")) spec.dependencies[2].requirement.should eq(Any) end it "parses empty mappings/sequences" do spec = Spec.from_yaml <<-YAML name: orm version: 1.0.0 authors: dependencies: development_dependencies: targets: executables: libraries: scripts: YAML spec.dependencies.empty?.should be_true end it "fails dependency with duplicate resolver" do expect_raises Shards::ParseError, %(Duplicate resolver mapping for dependency "foo" at line 6, column 5) do Spec.from_yaml <<-YAML name: orm version: 1.0.0 dependencies: foo: github: user/repo gitlab: user/repo YAML end end it "fails dependency with missing resolver" do expect_raises Shards::ParseError, %(Missing resolver for dependency "foo" at line 4, column 3) do Spec.from_yaml <<-YAML name: orm version: 1.0.0 dependencies: foo: branch: master YAML end end it "accepts dependency with extra attributes" do spec = Spec.from_yaml <<-YAML name: orm version: 1.0.0 dependencies: foo: github: user/repo extra: foobar YAML dep = Dependency.new("foo", GitResolver.new("foo", "https://github.com/user/repo.git"), Any) spec.dependencies[0].should eq dep end it "parse development dependencies" do spec = Spec.from_yaml <<-YAML name: orm version: 1.0.0 development_dependencies: minitest: github: ysbaddaden/minitest.cr version: 0.1.4 webmock: git: https://github.com/manastech/webcmok-crystal.git branch: master YAML spec.development_dependencies.size.should eq(2) spec.development_dependencies[0].name.should eq("minitest") spec.development_dependencies[0].resolver.should eq(GitResolver.new("minitest", "https://github.com/ysbaddaden/minitest.cr.git")) spec.development_dependencies[0].requirement.should eq(version_req "0.1.4") spec.development_dependencies[1].name.should eq("webmock") spec.development_dependencies[1].resolver.should eq(GitResolver.new("webmock", "https://github.com/manastech/webcmok-crystal.git")) spec.development_dependencies[1].requirement.should eq(branch "master") end it "parse targets" do spec = Spec.from_yaml <<-YAML name: shards version: 0.7.0 targets: shards: main: src/shards.cr cli: main: src/command/cli.cr YAML spec.targets.size.should eq(2) spec.targets[0].name.should eq("shards") spec.targets[0].main.should eq("src/shards.cr") spec.targets[1].name.should eq("cli") spec.targets[1].main.should eq("src/command/cli.cr") end it "fails target missing main" do expect_raises Shards::ParseError, %(Missing property "main" for target "foo" at line 4, column 3) do Spec.from_yaml <<-YAML name: orm version: 1.0.0 targets: foo: foo: bar YAML end end it "parse executables" do spec = Spec.from_yaml <<-YAML name: test version: 1.0.0 executables: - micrate - icr YAML spec.executables.should eq(%w(micrate icr)) expect_raises(Error) do spec = Spec.from_yaml <<-YAML name: test version: 1.0.0 executables: micrate: src/micrate.cr YAML end end it "parse libraries" do spec = Spec.from_yaml <<-YAML name: sqlite3 version: 1.0.0 libraries: libsqlite3: 3.8.0 libfoo: "*" YAML spec.libraries.size.should eq(2) spec.libraries[0].soname.should eq("libsqlite3") spec.libraries[0].version.should eq("3.8.0") spec.libraries[1].soname.should eq("libfoo") spec.libraries[1].version.should eq("*") end it "fails to parse invalid library" do empty_version = <<-YAML name: sqlite3 version: 1.0.0 libraries: libsqlite3: YAML expect_raises(Error) { Spec.from_yaml(empty_version) } list = <<-YAML name: sqlite3 version: 1.0.0 libraries: - libsqlite3 - libfoo YAML expect_raises(ParseError) { Spec.from_yaml(list) } end it "skips unknown attributes" do spec = Spec.from_yaml("\nanme: test\ncustom:\n test: more\nname: test\nversion: 1\n") spec.name.should eq("test") spec.version.should eq(version "1") spec = Spec.from_yaml("\nanme:\nname: test\nversion: 1\n") spec.name.should eq("test") spec.version.should eq(version "1") end it "raises on unknown attributes if validating" do expect_raises(ParseError, "unknown attribute: anme") { Spec.from_yaml("anme:", validate: true) } end it "raises when required attributes are missing" do expect_raises(ParseError, "missing required attribute: name") { Spec.from_yaml("license: MIT") } expect_raises(ParseError, "missing required attribute: version") { Spec.from_yaml("name: test") } end it "fails to parse dependencies" do str = <<-YAML name: amethyst version: 0.1.7 dependencies: github: spalger/crystal-mime branch: master YAML expect_raises(ParseError) { Spec.from_yaml(str) } end it "errors on duplicate attributes" do expect_raises(ParseError, %(duplicate attribute "name")) do Spec.from_yaml <<-YAML name: foo name: foo YAML end expect_raises(ParseError, %(duplicate attribute "version")) do Spec.from_yaml <<-YAML name: foo version: 1.0.0 version: 1.0.0 YAML end expect_raises(ParseError, %(duplicate attribute "dependencies")) do Spec.from_yaml <<-YAML name: foo version: 1.0.0 dependencies: bar: github: foo/bar dependencies: baz: github: foo/baz YAML end expect_raises(ParseError, %(duplicate attribute "development_dependencies")) do Spec.from_yaml <<-YAML name: foo version: 1.0.0 development_dependencies: bar: github: foo/bar development_dependencies: baz: github: foo/baz YAML end end end end shards-0.19.0/spec/unit/version_req_spec.cr000066400000000000000000000013641473060476400207110ustar00rootroot00000000000000require "./spec_helper" module Shards describe VersionReq do it "parses" do VersionReq.new("~> 1.0").patterns.should eq(["~> 1.0"]) VersionReq.new("~> 1.0, < 1.8").patterns.should eq(["~> 1.0", "< 1.8"]) VersionReq.new("~> 1.0,, < 1.8").patterns.should eq(["~> 1.0", "< 1.8"]) end it "to_s" do VersionReq.new("~> 1.0").to_s.should eq("~> 1.0") VersionReq.new("~> 1.0,< 1.8").to_s.should eq("~> 1.0, < 1.8") end it "prerelease?" do VersionReq.new("~> 1.0").prerelease?.should be_false VersionReq.new("~> 1.0-a").prerelease?.should be_true VersionReq.new("~> 1.0, < 1.8").prerelease?.should be_false VersionReq.new("~> 1.0, < 1.8-a").prerelease?.should be_true end end end shards-0.19.0/spec/unit/versions_spec.cr000066400000000000000000000201551473060476400202240ustar00rootroot00000000000000require "./spec_helper" private def resolve(versions : Array(String), req : String) : Array(String) Shards::Versions.resolve(versions(versions), version_req req).map &.value end private def matches?(version : String, req : String) : Bool Shards::Versions.matches?(version(version), version_req(req)) end module Shards describe Versions do # class VersionsTest < Minitest::Test it "prerelease?" do Versions.prerelease?("1.0").should be_false Versions.prerelease?("1.0.0.1").should be_false Versions.prerelease?("1.0a").should be_true Versions.prerelease?("1.0.alpha").should be_true Versions.prerelease?("1.0.0-rc1").should be_true Versions.prerelease?("1.0.0-pre.1.2.x.y").should be_true Versions.prerelease?("1.0.0-pre+20190129").should be_true Versions.prerelease?("1.0+20190129").should be_false Versions.prerelease?("1.0+build1").should be_false end it "compare" do # a is older than b: Versions.compare("1.0.0", "1.0.1").should eq(1) Versions.compare("1.0.0", "2.0.0").should eq(1) Versions.compare("1.0", "1.0.0.1").should eq(1) Versions.compare("1.0.0", "1.0.0.1").should eq(1) # a == b Versions.compare("0.1", "0.1").should eq(0) Versions.compare("0.1", "0.1.0.0").should eq(0) Versions.compare("0.1.0", "0.1").should eq(0) Versions.compare("2.0.0", "2.0.0").should eq(0) # a is newer than b: Versions.compare("1.0.1", "1.0.0").should eq(-1) Versions.compare("2.0.0", "1.0.0").should eq(-1) Versions.compare("1.0.0.1", "1.0").should eq(-1) Versions.compare("1.0.0.1", "1.0.0").should eq(-1) end it "compare preversions" do # a is older than b: Versions.compare("1.0.0-beta", "1.0.0").should eq(1) Versions.compare("1.0.0.alpha", "1.0.0").should eq(1) Versions.compare("1.0.0.alpha", "1.0.0.beta").should eq(1) Versions.compare("1.0.beta", "1.0.0").should eq(1) Versions.compare("1.0.alpha", "1.0.0-beta").should eq(1) Versions.compare("1.0-pre1", "1.0-pre2").should eq(1) Versions.compare("1.0-pre2", "1.0-pre10").should eq(1) # a == b Versions.compare("1.0.0-beta", "1.0.0-beta").should eq(0) Versions.compare("1.0.0-alpha", "1.0.0.alpha").should eq(0) Versions.compare("1.0.beta", "1.0.0.beta").should eq(0) Versions.compare("1.0.beta", "1.0.0.0.0.0.beta").should eq(0) # a is newer than b: Versions.compare("1.0.0", "1.0.0-beta").should eq(-1) Versions.compare("1.0.0", "1.0.0.alpha").should eq(-1) Versions.compare("1.0.0.beta", "1.0.0.alpha").should eq(-1) Versions.compare("1.0.0", "1.0.beta").should eq(-1) Versions.compare("1.0.0-beta", "1.0.alpha").should eq(-1) Versions.compare("1.0-pre2", "1.0-pre1").should eq(-1) Versions.compare("1.0-pre10", "1.0-pre2").should eq(-1) end it "compare ignores semver metadata" do Versions.compare("1.1+20180110", "1.0+20180110").should eq(-1) Versions.compare("1.0+build1", "1.0+build2").should eq(0) Versions.compare("1.0+20180110", "1.1+20180110").should eq(1) end it "sort" do 100.times do versions = %w( 0.0.1 0.1.0 0.1.1 0.1.2 0.2.0 0.2.1 0.2.10 0.2.10.1 0.2.11 0.10.0 0.11.0 0.20.0 0.20.1 1.0.0-alpha 1.0.0.beta 1.0.0-pre1 1.0.0-pre2 1.0.0-rc1 1.0.0-rc2 1.0.0-rc10 1.0.0 ).shuffle Versions.sort(versions).should eq(%w( 1.0.0 1.0.0-rc10 1.0.0-rc2 1.0.0-rc1 1.0.0-pre2 1.0.0-pre1 1.0.0.beta 1.0.0-alpha 0.20.1 0.20.0 0.11.0 0.10.0 0.2.11 0.2.10.1 0.2.10 0.2.1 0.2.0 0.1.2 0.1.1 0.1.0 0.0.1 )) end end it "resolve any" do versions = %w(0.0.1 0.1.0 0.1.1 0.1.2 0.2.0 0.10.0) resolve(versions, "*").should eq(versions) end it "resolve eq" do versions = %w(0.0.1 0.1.0 0.1.1 0.1.2 0.2.0 0.10.0) resolve(versions, "0.2.0").should eq(["0.2.0"]) resolve(versions, "0.1.1").should eq(["0.1.1"]) resolve(versions, "0.10.0").should eq(["0.10.0"]) resolve(versions, "1.0.0").should be_empty resolve(versions, "0.0.1.alpha").should be_empty end it "resolves neq" do versions = %w(0.0.1 0.1.0 0.1.1 0.1.2 0.2.0 0.10.0) resolve(versions, "!= 0.1.0").should eq(["0.0.1", "0.1.1", "0.1.2", "0.2.0", "0.10.0"]) resolve(versions, "!= 0.1.0, != 0.2.0, != 0.10.0").should eq(["0.0.1", "0.1.1", "0.1.2"]) end it "resolve gt" do versions = %w(0.0.1 0.1.0 0.1.1 0.1.2 0.2.0 0.10.0) resolve(versions, "> 0.1.2").should eq(["0.2.0", "0.10.0"]) resolve(versions, "> 0.1.1").should eq(["0.1.2", "0.2.0", "0.10.0"]) end it "resolve gte" do versions = %w(0.0.1 0.1.0 0.1.1 0.1.2 0.2.0 0.10.0) resolve(versions, ">= 0.2.0").should eq(["0.2.0", "0.10.0"]) resolve(versions, ">= 0.1.2").should eq(["0.1.2", "0.2.0", "0.10.0"]) end it "resolve lt" do versions = %w(0.0.1 0.1.0 0.1.1 0.1.2 0.2.0 0.10.0) resolve(versions, "< 0.1.0").should eq(["0.0.1"]) resolve(versions, "< 0.2.0").should eq(["0.0.1", "0.1.0", "0.1.1", "0.1.2"]) end it "resolve lte" do versions = %w(0.0.1 0.1.0 0.1.1 0.1.2 0.2.0 0.10.0) resolve(versions, "<= 0.1.0").should eq(["0.0.1", "0.1.0"]) resolve(versions, "<= 0.2.0").should eq(["0.0.1", "0.1.0", "0.1.1", "0.1.2", "0.2.0"]) end it "resolve approximate" do versions = %w(0.0.1 0.1.0 0.1.1 0.1.2 0.2.0 0.10.0 1.0.0 1.5.0 2.0.0) resolve(versions, "~> 0.1.0").should eq(["0.1.0", "0.1.1", "0.1.2"]) resolve(versions, "~> 0.1").should eq(["0.1.0", "0.1.1", "0.1.2", "0.2.0", "0.10.0"]) resolve(versions, "~> 1").should eq(["1.0.0", "1.5.0"]) resolve(["0.1"], "~> 0.1").should eq(["0.1"]) resolve(["0.1"], "~> 0.1.0").should eq(["0.1"]) end it "resolve intersection" do versions = %w(0.0.1 0.1.0 0.1.1 0.1.2 0.2.0 0.10.0) resolve(versions, ">= 0.1.0, < 0.2.0").should eq(["0.1.0", "0.1.1", "0.1.2"]) end it "matches?" do matches?("0.1.0", "*").should be_true matches?("1.0.0", "*").should be_true matches?("1.0.0", "1.0.0").should be_true matches?("1.0.0", "1.0").should be_true matches?("1.0.0", "1.0.1").should be_false matches?("1.0.0", "> 0.1.0, < 1.0.1, != 1.0.0").should be_false matches?("0.5.0", "> 0.1, < 1.0, != 0.5").should be_false matches?("1.0.1", ">= 1.0.0, != 2.0").should be_true matches?("1.8", ">= 1.0.1, != 2.0").should be_true matches?("1.0.0", ">= 1.0.0").should be_true matches?("1.0.0", ">= 1.0").should be_true matches?("1.0.1", ">= 1.0.0").should be_true matches?("1.0.0", ">= 1.0.1").should be_false matches?("1.0.0", "> 1.0.0").should be_false matches?("1.0.0", "> 1.0").should be_false matches?("1.0.1", "> 1.0.0").should be_true matches?("1.0.0", "> 1.0.1").should be_false matches?("1.0.0", "<= 1.0.0").should be_true matches?("1.0.0", "<= 1.0").should be_true matches?("1.0.1", "<= 1.0.0").should be_false matches?("1.0.0", "<= 1.0.1").should be_true matches?("1.0.0", "< 1.0.0").should be_false matches?("1.0.0", "< 1.0").should be_false matches?("1.0.1", "< 1.0.0").should be_false matches?("1.0.0", "< 1.0.1").should be_true matches?("1.0.0", "~> 1.0.0").should be_true matches?("1.0.0", "~> 1.0").should be_true matches?("1.0.0", "~> 1").should be_true matches?("1.1.0", "~> 1").should be_true matches?("1.0.0", "~> 1.1").should be_false matches?("2.0.0", "~> 1").should be_false matches?("1.0.1", "~> 1.0.0").should be_true matches?("1.0.0", "~> 1.0.1").should be_false matches?("1.0.0", "> 0.1.0, < 1.0.1").should be_true matches?("1.0.1", "> 0.1.0, < 1.0.1").should be_false end end end shards-0.19.0/src/000077500000000000000000000000001473060476400136675ustar00rootroot00000000000000shards-0.19.0/src/cli.cr000066400000000000000000000132351473060476400147700ustar00rootroot00000000000000require "option_parser" require "./commands/*" module Shards def self.display_help_and_exit(opts) puts <<-HELP shards [...] [] Commands: build [] [] - Build the specified in `bin` path, all build_options are delegated to `crystal build`. check - Verify all dependencies are installed. init - Initialize a `shard.yml` file. install - Install dependencies, creating or using the `shard.lock` file. list [--tree] - List installed dependencies. lock [--update] [...] - Lock dependencies in `shard.lock` but doesn't install them. outdated [--pre] - List dependencies that are outdated. prune - Remove unused dependencies from `lib` folder. run [] [] - Build and run specified target update [...] - Update dependencies and `shard.lock`. version [] - Print the current version of the shard. General options: HELP puts opts exit end def self.run display_help = false OptionParser.parse(cli_options) do |opts| path = Dir.current opts.on("--no-color", "Disable colored output.") { self.colors = false } opts.on("--version", "Print the `shards` version.") { puts self.version_string; exit } opts.on("--frozen", "Strictly installs locked versions from shard.lock.") do self.frozen = true end opts.on("--without-development", "Does not install development dependencies.") do self.with_development = false end opts.on("--production", "same as `--frozen --without-development`") do self.frozen = true self.with_development = false end opts.on("--skip-postinstall", "Does not run postinstall of dependencies") do self.skip_postinstall = true end opts.on("--skip-executables", "Does not install executables") do self.skip_executables = true end opts.on("--local", "Don't update remote repositories, use the local cache only.") { self.local = true } opts.on("--jobs=N", "Number of repository downloads to perform in parallel (default: 8). Currently only for git.") { |n| self.jobs = n.to_i } # TODO: remove in the future opts.on("--ignore-crystal-version", "Has no effect. Kept for compatibility, to be removed in the future.") { } opts.on("-v", "--verbose", "Increase the log verbosity, printing all debug statements.") { self.set_debug_log_level } opts.on("-q", "--quiet", "Decrease the log verbosity, printing only warnings and errors.") { self.set_warning_log_level } opts.on("-h", "--help", "Print usage synopsis.") { display_help = true } opts.unknown_args do |args, options| case args[0]? || DEFAULT_COMMAND when "build" targets, build_options = parse_args(args[1..-1]) check_and_install_dependencies(path) Commands::Build.run(path, targets, build_options) when "run" targets, run_options = parse_args(args[1..-1]) check_and_install_dependencies(path) Commands::Run.run(path, targets, run_options, options) when "check" Commands::Check.run(path) when "init" Commands::Init.run(path) when "install" Commands::Install.run( path ) when "list" Commands::List.run(path, tree: args.includes?("--tree")) when "lock" Commands::Lock.run( path, args[1..-1].reject(&.starts_with?("--")), print: args.includes?("--print"), update: args.includes?("--update") ) when "outdated" Commands::Outdated.run( path, prereleases: args.includes?("--pre") ) when "prune" Commands::Prune.run(path) when "update" Commands::Update.run( path, args[1..-1].reject(&.starts_with?("--")) ) when "version" Commands::Version.run(args[1]? || path) else program_name = "shards-#{args[0]}" if program_path = Process.find_executable(program_name) run_shards_subcommand(program_path, cli_options) else display_help_and_exit(opts) end end if display_help display_help_and_exit(opts) end exit end end end def self.cli_options shards_opts : Array(String) {% if compare_versions(Crystal::VERSION, "1.0.0-0") > 0 %} shards_opts = Process.parse_arguments(ENV.fetch("SHARDS_OPTS", "")) {% else %} shards_opts = ENV.fetch("SHARDS_OPTS", "").split {% end %} ARGV.dup.concat(shards_opts) end def self.run_shards_subcommand(process_name, args) Process.exec( command: process_name, args: args[1..], ) end def self.parse_args(args) targets = [] of String options = [] of String args.each do |arg| if arg.starts_with?('-') options << arg else targets << arg end end {targets, options} end def self.check_and_install_dependencies(path) Commands::Check.run(path) rescue Commands::Install.run(path) end end begin Shards.run rescue ex : OptionParser::InvalidOption Shards::Log.fatal { ex.message } exit 1 rescue ex : Shards::ParseError ex.to_s(STDERR) exit 1 rescue ex : Shards::Error Shards::Log.error { ex.message } exit 1 end shards-0.19.0/src/commands/000077500000000000000000000000001473060476400154705ustar00rootroot00000000000000shards-0.19.0/src/commands/build.cr000066400000000000000000000030511473060476400171140ustar00rootroot00000000000000require "./command" module Shards module Commands class Build < Command def run(targets, options) if spec.targets.empty? raise Error.new("Targets not defined in #{SPEC_FILENAME}") end unless Dir.exists?(Shards.bin_path) Log.debug { "mkdir #{Shards.bin_path}" } Dir.mkdir(Shards.bin_path) end if targets.empty? targets = spec.targets.map(&.name) end targets.each do |name| if target = spec.targets.find { |t| t.name == name } build(target, options) else raise Error.new("Error target #{name} was not found in #{SPEC_FILENAME}.") end end end private def build(target, options) Log.info { "Building: #{target.name}" } args = [ "build", "-o", File.join(Shards.bin_path, target.name), target.main, ] unless Shards.colors? args << "--no-color" end if Shards::Log.level <= ::Log::Severity::Debug args << "--verbose" end options.each { |option| args << option } Log.debug { "#{Shards.crystal_bin} #{args.join(' ')}" } error = IO::Memory.new status = Process.run(Shards.crystal_bin, args: args, output: Process::Redirect::Inherit, error: error) if status.success? STDERR.puts error unless error.empty? else raise Error.new("Error target #{target.name} failed to compile:\n#{error}") end end end end end shards-0.19.0/src/commands/check.cr000066400000000000000000000035061473060476400170770ustar00rootroot00000000000000require "./command" require "../versions" module Shards module Commands class Check < Command def run if has_dependencies? locks # ensures that lockfile exists verify(spec.dependencies) verify(spec.development_dependencies) if Shards.with_development? end Log.info { "Dependencies are satisfied" } end private def has_dependencies? spec.dependencies.any? || (Shards.with_development? && spec.development_dependencies.any?) end private def verify(dependencies) apply_overrides(dependencies).each do |dependency| Log.debug { "#{dependency.name}: checking..." } unless installed?(dependency) raise Error.new("Dependencies aren't satisfied. Install them with 'shards install'") end end end private def installed?(dependency) unless lock = locks.shards.find { |d| d.name == dependency.name } Log.debug { "#{dependency.name}: not locked" } return false end if dependency.resolver != lock.resolver Log.debug { "#{dependency.name}: source changed" } return false elsif !dependency.matches?(lock.version) Log.debug { "#{dependency.name}: lock conflict" } return false else return false unless lock.installed? verify(lock.spec.dependencies) return true end end # FIXME: duplicates MolinilloSolver#on_override def on_override(dependency : Dependency) : Dependency? override.try(&.dependencies.find { |o| o.name == dependency.name }) end # FIXME: duplicates MolinilloSolver#apply_overrides def apply_overrides(deps : Array(Dependency)) deps.map { |dep| on_override(dep) || dep } end end end end shards-0.19.0/src/commands/command.cr000066400000000000000000000063101473060476400174340ustar00rootroot00000000000000require "../lock" require "../spec" require "../override" module Shards abstract class Command getter path : String getter spec_path : String getter lockfile_path : String getter override_path : String? @spec : Spec? @locks : Lock? @override : Override? def initialize(path) if File.directory?(path) @path = path @spec_path = File.join(path, SPEC_FILENAME) else @path = File.dirname(path) @spec_path = path end @lockfile_path = File.join(@path, LOCK_FILENAME) # If global override is defined via SHARDS_OVERRIDE env var we use that. # Otherwise we check if the is a shard.override.yml file next to the shard.yml @override_path = Shards.global_override_filename unless @override_path local_override = File.join(@path, OVERRIDE_FILENAME) @override_path = File.exists?(local_override) ? local_override : nil end end def self.run(path, *args, **kwargs) new(path).run(*args, **kwargs) end def spec @spec ||= if File.exists?(spec_path) Spec.from_file(spec_path) else raise Error.new("Missing #{spec_filename}. Please run 'shards init'") end end def spec_filename File.basename(spec_path) end def locks @locks ||= if lockfile? Shards::Lock.from_file(lockfile_path) else raise Error.new("Missing #{LOCK_FILENAME}. Please run 'shards install'") end end def lockfile? File.exists?(lockfile_path) end def override @override ||= override_path.try { |p| Shards::Override.from_file(p) } end def write_lockfile(packages) Log.info { "Writing #{LOCK_FILENAME}" } override_path = @override_path override_path = File.basename(override_path) if override_path && File.dirname(override_path) == @path Shards::Lock.write(packages, override_path, LOCK_FILENAME) end def handle_resolver_errors(&) yield rescue e : Molinillo::ResolverError Log.error { e.message } raise Shards::Error.new("Failed to resolve dependencies") end def check_crystal_version(packages) crystal_version = Shards::Version.new Shards.crystal_version packages.each do |package| crystal_req = MolinilloSolver.crystal_version_req(package.spec) if !Shards::Versions.matches?(crystal_version, crystal_req) Log.warn { "Shard \"#{package.name}\" may be incompatible with Crystal #{Shards.crystal_version}" } end end end def check_symlink_privilege {% if flag?(:win32) %} return if Shards::Helpers.developer_mode? return if Shards::Helpers.privilege_enabled?("SeCreateSymbolicLinkPrivilege") raise Shards::Error.new(<<-EOS) Shards needs symlinks to work. Please enable Developer Mode, or run Shards with elevated rights: https://learn.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development EOS {% end %} end def touch_install_path Dir.mkdir_p(Shards.install_path) File.touch(Shards.install_path) end end end shards-0.19.0/src/commands/init.cr000066400000000000000000000014161473060476400167630ustar00rootroot00000000000000require "./command" require "ecr/macros" module Shards module Commands class Init < Command def run if File.exists?(shard_path) raise Error.new("#{SPEC_FILENAME} already exists") end File.write(shard_path, String.build do |__str__| ECR.embed "#{__DIR__}/../templates/shard.yml.ecr", "__str__" end) Log.info { "Created #{SPEC_FILENAME}" } end private def name File.basename(path) .gsub(/[^-_a-zA-Z0-9]+/, '-') .gsub("crystal", "") .gsub(/[-_]{2,}/, '-') .gsub(/^[-_]|[-_]$/, "") end private def version "0.1.0" end private def shard_path File.join(path, Shards::SPEC_FILENAME) end end end end shards-0.19.0/src/commands/install.cr000066400000000000000000000060431473060476400174670ustar00rootroot00000000000000require "./command" require "../molinillo_solver" module Shards module Commands class Install < Command def run if Shards.frozen? && !lockfile? raise Error.new("Missing shard.lock") end check_symlink_privilege Log.info { "Resolving dependencies" } solver = MolinilloSolver.new(spec, override) if lockfile? # install must be as conservative as possible: solver.locks = locks.shards end solver.prepare(development: Shards.with_development?) packages = handle_resolver_errors { solver.solve } if Shards.frozen? validate(packages) end install(packages) if generate_lockfile?(packages) write_lockfile(packages) elsif !Shards.frozen? # Touch lockfile so its mtime is bigger than that of shard.yml File.touch(lockfile_path) end # Touch install path so its mtime is bigger than that of the lockfile touch_install_path check_crystal_version(packages) end private def validate(packages) packages.each do |package| if lock = locks.shards.find { |d| d.name == package.name } if lock.resolver != package.resolver raise LockConflict.new("#{package.name} source changed") else validate_locked_version(package, lock.version) end else raise LockConflict.new("can't install new dependency #{package.name} in production") end end end private def validate_locked_version(package, version) return if package.version == version raise LockConflict.new("#{package.name} requirements changed") end private def install(packages : Array(Package)) # packages are returned by the solver in reverse topological order, # so transitive dependencies are installed first packages.each do |package| # first install the dependency: next unless install(package) # then execute the postinstall script # (with access to all transitive dependencies): package.postinstall # always install executables because the path resolver never actually # installs dependencies: package.install_executables end end private def install(package : Package) if package.installed? Log.info { "Using #{package.name} (#{package.report_version})" } return end Log.info { "Installing #{package.name} (#{package.report_version})" } package.install package end private def generate_lockfile?(packages) !Shards.frozen? && (!lockfile? || outdated_lockfile?(packages)) end private def outdated_lockfile?(packages) return true if locks.version != Shards::Lock::CURRENT_VERSION return true if packages.size != locks.shards.size packages.index_by(&.name) != locks.shards.index_by(&.name) end end end end shards-0.19.0/src/commands/list.cr000066400000000000000000000020701473060476400167700ustar00rootroot00000000000000require "./command" module Shards module Commands class List < Command @tree = false def run(@tree = false) return unless has_dependencies? puts "Shards installed:" list(spec.dependencies) list(spec.development_dependencies) if Shards.with_development? end private def list(dependencies, level = 1) dependencies.each do |dependency| package = Shards.info.installed[dependency.name]? unless package Log.debug { "#{dependency.name}: not installed" } raise Error.new("Dependencies aren't satisfied. Install them with 'shards install'") end indent = " " * level puts "#{indent}* #{package}" indent_level = @tree ? level + 1 : level list(package.spec.dependencies, indent_level) end end # FIXME: duplicates Check#has_dependencies? private def has_dependencies? spec.dependencies.any? || (Shards.with_development? && spec.development_dependencies.any?) end end end end shards-0.19.0/src/commands/lock.cr000066400000000000000000000021061473060476400167450ustar00rootroot00000000000000require "./command" require "../molinillo_solver" module Shards module Commands class Lock < Command def run(shards : Array(String), print = false, update = false) check_symlink_privilege Log.info { "Resolving dependencies" } solver = MolinilloSolver.new(spec, override) if lockfile? if update # update selected dependencies to latest possible versions, but # avoid to update unspecified dependencies, if possible: unless shards.empty? solver.locks = locks.shards.reject { |d| shards.includes?(d.name) } end else # install must be as conservative as possible: solver.locks = locks.shards end end solver.prepare(development: Shards.with_development?) packages = handle_resolver_errors { solver.solve } return if packages.empty? if print Shards::Lock.write(packages, @override_path, STDOUT) else write_lockfile(packages) end end end end end shards-0.19.0/src/commands/outdated.cr000066400000000000000000000074271473060476400176410ustar00rootroot00000000000000require "./command" module Shards module Commands class Outdated < Command @prereleases = false @up_to_date = true @output = IO::Memory.new def run(@prereleases = false) check_symlink_privilege return unless has_dependencies? Log.info { "Resolving dependencies" } solver = MolinilloSolver.new(spec, override, prereleases: @prereleases) solver.prepare(development: Shards.with_development?) packages = handle_resolver_errors { solver.solve } packages.each { |package| analyze(package) } if @up_to_date Log.info { "Dependencies are up to date!" } else @output.rewind Log.warn { "Outdated dependencies:" } puts @output.to_s end end private def analyze(package) unless installed_dep = Shards.info.installed[package.name]? Log.warn { "#{package.name}: not installed" } return end if installed_dep.resolver != package.resolver raise LockConflict.new("#{package.name} source changed") end resolver = package.resolver installed = installed_dep.version dependency = dependency_by_name package.name if dependency && !dependency.matches?(installed) raise LockConflict.new("#{package.name} requirements changed") end releases = if @prereleases resolver.available_releases else Versions.without_prereleases(resolver.available_releases) end releases = Versions.sort(releases) latest_release = releases.first? available_version = package.version requirement = dependency.try(&.requirement) case requirement when GitBranchRef, GitHeadRef requirement_branch = requirement else requirement_branch = nil end latest_ref_version = resolver.latest_version_for_ref(requirement_branch) if installed == latest_ref_version # If branch HEAD is installed, it is automatically the most recent for # that requirement. # We still need to check if a tagged release with a higher version # is available. if latest_release return if Versions.compare(installed, latest_release) <= 0 else return end end case requirement when GitBranchRef, GitHeadRef # On branch requirement that branch's HEAD should be reported as # available version available_version = latest_ref_version when GitTagRef, GitCommitRef # TODO: Check if pinned commit is an ancestor of HEAD else # already the latest version? return if latest_release == installed end @up_to_date = false @output << " * " << package.name @output << " (installed: " << resolver.report_version(installed) unless installed == available_version @output << ", available: " << resolver.report_version(available_version) end # also report latest version: if latest_release && Versions.compare(latest_release, available_version) < 0 @output << ", latest: " << resolver.report_version(latest_release) end @output.puts ')' end # FIXME: duplicates Check#has_dependencies? private def has_dependencies? spec.dependencies.any? || (Shards.with_development? && spec.development_dependencies.any?) end private def dependency_by_name(name : String) override.try(&.dependencies.find { |o| o.name == name }) || spec.dependencies.find { |o| o.name == name } || spec.development_dependencies.find { |o| o.name == name } end end end end shards-0.19.0/src/commands/prune.cr000066400000000000000000000013311473060476400171450ustar00rootroot00000000000000require "file_utils" require "./command" require "../helpers" module Shards module Commands class Prune < Command def run return unless lockfile? && Dir.exists?(Shards.install_path) Dir.each_child(Shards.install_path) do |name| path = File.join(Shards.install_path, name) next unless File.directory?(path) if locks.shards.none? { |d| d.name == name } Log.debug { "rm -rf '#{Process.quote(path)}'" } Shards::Helpers.rm_rf(path) Shards.info.installed.delete(name) Log.info { "Pruned #{File.join(File.basename(Shards.install_path), name)}" } end end Shards.info.save end end end end shards-0.19.0/src/commands/run.cr000066400000000000000000000025621473060476400166270ustar00rootroot00000000000000require "./command" module Shards module Commands class Run < Command def run(targets, options, run_options) if spec.targets.empty? raise Error.new("Targets not defined in #{SPEC_FILENAME}") end # when more than one target was specified if targets.size > 1 raise Error.new("Error please specify only one target. If you meant to pass arguments you may use 'shards run target -- args'") end # when no target was specified if targets.empty? if spec.targets.size > 1 raise Error.new("Error please specify the target with 'shards run target'") else name = spec.targets.first.name end else name = targets.first end if target = spec.targets.find { |t| t.name == name } Commands::Build.run(path, [target.name], options) Log.info { "Executing: #{target.name} #{run_options.join(' ')}" } status = Process.run(File.join(Shards.bin_path, target.name), args: run_options, input: Process::Redirect::Inherit, output: Process::Redirect::Inherit, error: Process::Redirect::Inherit) unless status.success? exit status.exit_code end else raise Error.new("Error target #{name} was not found in #{SPEC_FILENAME}") end end end end end shards-0.19.0/src/commands/update.cr000066400000000000000000000036731473060476400173110ustar00rootroot00000000000000require "./command" require "../molinillo_solver" module Shards module Commands class Update < Command def run(shards : Array(String)) check_symlink_privilege Log.info { "Resolving dependencies" } solver = MolinilloSolver.new(spec, override) if lockfile? && !shards.empty? # update selected dependencies to latest possible versions, but # avoid to update unspecified dependencies, if possible: solver.locks = locks.shards.reject { |d| shards.includes?(d.name) } end solver.prepare(development: Shards.with_development?) packages = handle_resolver_errors { solver.solve } install(packages) if generate_lockfile?(packages) write_lockfile(packages) else # Touch lockfile so its mtime is bigger than that of shard.yml File.touch(lockfile_path) end # Touch install path so its mtime is bigger than that of the lockfile touch_install_path check_crystal_version(packages) end private def install(packages : Array(Package)) # first install all dependencies: installed = packages.compact_map { |package| install(package) } # then execute the postinstall script of installed dependencies (with # access to all transitive dependencies): installed.each(&.postinstall) # always install executables because the path resolver never actually # installs dependencies: packages.each(&.install_executables) end private def install(package : Package) if package.installed? Log.info { "Using #{package.name} (#{package.report_version})" } return end Log.info { "Installing #{package.name} (#{package.report_version})" } package.install package end private def generate_lockfile?(packages) !Shards.frozen? end end end end shards-0.19.0/src/commands/version.cr000066400000000000000000000012421473060476400175020ustar00rootroot00000000000000require "./command" module Shards module Commands class Version < Command def self.run(path) path = lookup_path(path) new(path).run end def run puts spec.version end # look up for `SPEC_FILENAME` in *path* or up private def self.lookup_path(path) previous = nil current = File.expand_path(path) until !File.directory?(current) || current == previous shard_file = File.join(current, SPEC_FILENAME) break if File.exists?(shard_file) previous = current current = File.dirname(current) end current end end end end shards-0.19.0/src/config.cr000066400000000000000000000064431473060476400154710ustar00rootroot00000000000000require "./info" module Shards SPEC_FILENAME = "shard.yml" LOCK_FILENAME = "shard.lock" OVERRIDE_FILENAME = "shard.override.yml" INSTALL_DIR = "lib" DEFAULT_COMMAND = "install" DEFAULT_VERSION = "0" VERSION_REFERENCE = /^v?\d+[-.][-.a-zA-Z\d]+$/ VERSION_TAG = /^v(\d+[-.][-.a-zA-Z\d]+)$/ VERSION_AT_GIT_COMMIT = /^(\d+[-.][-.a-zA-Z\d]+)\+git\.commit\.([0-9a-f]+)$/ VERSION_AT_HG_COMMIT = /^(\d+[-.][-.a-zA-Z\d]+)\+hg\.commit\.([0-9a-f]+)$/ VERSION_AT_FOSSIL_COMMIT = /^(\d+[-.][-.a-zA-Z\d]+)\+fossil\.commit\.([0-9a-f]+)$/ def self.cache_path @@cache_path ||= find_or_create_cache_path end private def self.find_or_create_cache_path candidates = {% begin %} [ ENV["SHARDS_CACHE_PATH"]?, {% if flag?(:windows) %} ENV["LOCALAPPDATA"]?.try { |dir| File.join(dir, "shards", "cache") }, ENV["USERPROFILE"]?.try { |home| File.join(home, ".cache", "shards") }, ENV["USERPROFILE"]?.try { |home| File.join(home, ".shards") }, {% else %} ENV["XDG_CACHE_HOME"]?.try { |cache| File.join(cache, "shards") }, ENV["HOME"]?.try { |home| File.join(home, ".cache", "shards") }, ENV["HOME"]?.try { |home| File.join(home, ".cache", ".shards") }, {% end %} File.join(Dir.current, ".shards"), ] {% end %} candidates.each do |candidate| next unless candidate path = File.expand_path(candidate) return path if File.exists?(path) begin Dir.mkdir_p(path) return path rescue File::Error end end raise Error.new("Failed to find or create cache directory") end def self.cache_path=(@@cache_path : String) end def self.install_path @@install_path ||= begin ENV.fetch("SHARDS_INSTALL_PATH") { File.join(Dir.current, INSTALL_DIR) } end end def self.install_path=(@@install_path : String) end def self.info @@info ||= Info.new end def self.bin_path @@bin_path ||= ENV.fetch("SHARDS_BIN_PATH") { File.join(Dir.current, "bin") } end def self.bin_path=(@@bin_path : String) end def self.crystal_bin @@crystal_bin ||= ENV.fetch("CRYSTAL", "crystal") end def self.crystal_bin=(@@crystal_bin : String) end def self.global_override_filename ENV["SHARDS_OVERRIDE"]?.try { |p| File.expand_path(p) } end def self.crystal_version @@crystal_version ||= without_prerelease(ENV["CRYSTAL_VERSION"]? || begin output = IO::Memory.new error = IO::Memory.new status = begin Process.run(crystal_bin, {"env", "CRYSTAL_VERSION"}, output: output, error: error) rescue e raise Error.new("Could not execute '#{crystal_bin}': #{e.message}") end raise Error.new("Error executing crystal:\n#{error}") unless status.success? output.to_s.strip end) end def self.crystal_version=(@@crystal_version : String) end private def self.without_prerelease(version) if version =~ /^(\d+)\.(\d+)\.(\d+)([^\w]\w+)$/ "#{$1}.#{$2}.#{$3}" else version end end class_property? frozen = false class_property? with_development = true class_property? local = false class_property? skip_postinstall = false class_property? skip_executables = false class_property jobs : Int32 = 8 end shards-0.19.0/src/dependency.cr000066400000000000000000000054111473060476400163340ustar00rootroot00000000000000require "./ext/yaml" require "./requirement" require "./resolvers/resolver" module Shards class Dependency property name : String property resolver : Resolver property requirement : Requirement def initialize(@name : String, @resolver : Resolver, @requirement : Requirement = Any) end def self.from_yaml(pull : YAML::PullParser) mapping_start = pull.location name = pull.read_scalar pull.read_mapping do resolver_data = nil params = Hash(String, String).new until pull.kind.mapping_end? location = pull.location key, value = pull.read_scalar, pull.read_scalar if type = Resolver.find_class(key) if resolver_data raise YAML::ParseException.new("Duplicate resolver mapping for dependency #{name.inspect}", *location) else resolver_data = {type: type, key: key, source: value} end else params[key] = value end end unless resolver_data raise YAML::ParseException.new("Missing resolver for dependency #{name.inspect}", *mapping_start) end resolver = resolver_data[:type].find_resolver(resolver_data[:key], name, resolver_data[:source]) requirement = resolver.parse_requirement(params) Dependency.new(name, resolver, requirement) end end def to_yaml(yaml : YAML::Builder) yaml.scalar name yaml.mapping do yaml.scalar resolver.class.key yaml.scalar resolver.source requirement.to_yaml(yaml) end end def as_package? version = case req = @requirement when VersionReq then Version.new(req.to_s) else # This conversion is used to keep compatibility # with old versions (1.0) of lock files. versions = @resolver.versions_for(req) unless versions.size == 1 return end versions.first end Package.new(@name, @resolver, version) end def_equals @name, @resolver, @requirement def prerelease? case req = requirement when Version req.prerelease? when VersionReq req.prerelease? else false end end private def report_requirement case req = requirement when Version resolver.report_version(req) else req.to_s end end def to_s(io) io << name << " (" << report_requirement << ")" end def matches?(version : Version) case req = requirement when Ref resolver.matches_ref?(req, version) when Version req == version when VersionReq Versions.matches?(version, req) when Any true end end end end shards-0.19.0/src/errors.cr000066400000000000000000000031411473060476400155300ustar00rootroot00000000000000module Shards class Error < ::Exception end class Conflict < Error getter package def initialize(@package : Package) super "Error resolving #{package.name} (#{package.requirements.join(", ")})" end end class LockConflict < Error def initialize(message) super "Outdated #{LOCK_FILENAME} (#{message}). Please run shards update instead." end end class InvalidLock < Error def initialize super "Unsupported #{LOCK_FILENAME}. It was likely generated from a newer version of Shards." end end class ParseError < Error getter input : String getter filename : String getter line_number : Int32 getter column_number : Int32 property resolver : Resolver? def initialize(message, @input, @filename, line_number, column_number) @line_number = line_number.to_i @column_number = column_number.to_i super message end def to_s(io) io << "Error in " if resolver = self.resolver resolver.name.inspect_unquoted(io) io << ':' end filename = self.filename filename = Path[filename].relative_to Dir.current io.puts "#{filename}: #{message}" io.puts lines = input.split('\n') from = line_number - 3 from = 0 if from < 0 lines[from...line_number].each_with_index do |line, i| io.puts " #{from + i + 1}. #{line}" end arrow = String.build do |s| s << " " (column_number - 1).times { s << ' ' } s << '^' end io.puts arrow.colorize(:green).bold io.puts io.flush end end end shards-0.19.0/src/ext/000077500000000000000000000000001473060476400144675ustar00rootroot00000000000000shards-0.19.0/src/ext/yaml.cr000066400000000000000000000014111473060476400157540ustar00rootroot00000000000000require "yaml" module YAML class PullParser # Iterates a sequence, yielding on each new entry until the sequence is # terminated. def each_in_sequence(&) : Nil read_sequence_start until kind == YAML::EventKind::SEQUENCE_END yield end read_sequence_end end # Iterates a mapping, yielding on each new entry until the mapping is # terminated. def each_in_mapping(&) : Nil read_mapping_start until kind == YAML::EventKind::MAPPING_END yield end read_mapping_end end def read_empty_or(&) if kind.scalar? case value when "", "~" # allow empty dependencies read_next return end end yield end end end shards-0.19.0/src/helpers.cr000066400000000000000000000105351473060476400156630ustar00rootroot00000000000000{% if flag?(:win32) %} lib LibC struct LUID lowPart : DWORD highPart : Long end struct LUID_AND_ATTRIBUTES luid : LUID attributes : DWORD end struct TOKEN_PRIVILEGES privilegeCount : DWORD privileges : LUID_AND_ATTRIBUTES[1] end TOKEN_QUERY = 0x0008 TOKEN_ADJUST_PRIVILEGES = 0x0020 TokenPrivileges = 3 SE_PRIVILEGE_ENABLED = 0x00000002_u32 fun OpenProcessToken(processHandle : HANDLE, desiredAccess : DWORD, tokenHandle : HANDLE*) : BOOL fun GetTokenInformation(tokenHandle : HANDLE, tokenInformationClass : Int, tokenInformation : Void*, tokenInformationLength : DWORD, returnLength : DWORD*) : BOOL fun LookupPrivilegeValueW(lpSystemName : LPWSTR, lpName : LPWSTR, lpLuid : LUID*) : BOOL fun AdjustTokenPrivileges(tokenHandle : HANDLE, disableAllPrivileges : BOOL, newState : TOKEN_PRIVILEGES*, bufferLength : DWORD, previousState : TOKEN_PRIVILEGES*, returnLength : DWORD*) : BOOL end {% end %} module Shards::Helpers def self.rm_rf(path : String) : Nil # TODO: delete this and use https://github.com/crystal-lang/crystal/pull/9903 if !File.symlink?(path) && Dir.exists?(path) Dir.each_child(path) do |entry| src = File.join(path, entry) rm_rf(src) end Dir.delete(path) else begin File.delete(path) rescue File::AccessDeniedError # To be able to delete read-only files (e.g. ones under .git/) on Windows. File.chmod(path, 0o666) File.delete(path) end end rescue File::Error end def self.rm_rf_children(dir : String) : Nil Dir.each_child(dir) do |child| rm_rf(File.join(dir, child)) end end def self.exe(name) {% if flag?(:win32) %} name + ".exe" {% else %} name {% end %} end def self.privilege_enabled?(privilege_name : String) : Bool {% if flag?(:win32) %} if LibC.LookupPrivilegeValueW(nil, privilege_name.to_utf16, out privilege_luid) == 0 return false end # if the process token already has the privilege, and the privilege is already enabled, # we don't need to do anything else if LibC.OpenProcessToken(LibC.GetCurrentProcess, LibC::TOKEN_QUERY, out token) != 0 begin LibC.GetTokenInformation(token, LibC::TokenPrivileges, nil, 0, out len) buf = Pointer(UInt8).malloc(len).as(LibC::TOKEN_PRIVILEGES*) LibC.GetTokenInformation(token, LibC::TokenPrivileges, buf, len, out _) privileges = Slice.new(pointerof(buf.value.@privileges).as(LibC::LUID_AND_ATTRIBUTES*), buf.value.privilegeCount) # if the process token doesn't have the privilege, there is no way # `AdjustTokenPrivileges` could grant or enable it privilege = privileges.find(&.luid.== privilege_luid) return false unless privilege return true if privilege.attributes.bits_set?(LibC::SE_PRIVILEGE_ENABLED) ensure LibC.CloseHandle(token) end end if LibC.OpenProcessToken(LibC.GetCurrentProcess, LibC::TOKEN_ADJUST_PRIVILEGES, out adjust_token) != 0 new_privileges = LibC::TOKEN_PRIVILEGES.new( privilegeCount: 1, privileges: StaticArray[ LibC::LUID_AND_ATTRIBUTES.new( luid: privilege_luid, attributes: LibC::SE_PRIVILEGE_ENABLED, ), ], ) if LibC.AdjustTokenPrivileges(adjust_token, 0, pointerof(new_privileges), 0, nil, nil) != 0 return true if WinError.value.error_success? end end false {% else %} raise NotImplementedError.new("Shards::Helpers.privilege_enabled?") {% end %} end def self.developer_mode? : Bool {% if flag?(:win32) %} key = %q(SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock).to_utf16 !!Crystal::System::WindowsRegistry.open?(LibC::HKEY_LOCAL_MACHINE, key) do |handle| value = uninitialized LibC::DWORD name = "AllowDevelopmentWithoutDevLicense".to_utf16 bytes = Slice.new(pointerof(value), 1).to_unsafe_bytes type, len = Crystal::System::WindowsRegistry.get_raw(handle, name, bytes) || return false return type.dword? && len == sizeof(typeof(value)) && value != 0 end {% else %} raise NotImplementedError.new("Shards::Helpers.developer_mode?") {% end %} end end shards-0.19.0/src/info.cr000066400000000000000000000020061473060476400151460ustar00rootroot00000000000000require "./lock" class Shards::Info getter install_path : String getter installed = Hash(String, Package).new def initialize(@install_path = Shards.install_path) reload end def reload path = info_path if File.exists?(path) @installed = Lock.from_file(path).shards.index_by &.name else @installed.clear end end def save Dir.mkdir_p(@install_path) unless File.exists?(info_path) Dir.each_child(@install_path) do |name| if name.ends_with?(".sha1") File.delete(File.join(@install_path, name)) end end end File.open(info_path, "w") do |file| YAML.build(file) do |yaml| yaml.mapping do yaml.scalar "version" yaml.scalar "1.0" yaml.scalar "shards" yaml.mapping do installed.each do |name, dep| dep.to_yaml(yaml) end end end end end end def info_path File.join(@install_path, ".shards.info") end end shards-0.19.0/src/lock.cr000066400000000000000000000052171473060476400151520ustar00rootroot00000000000000require "./ext/yaml" require "./dependency" require "./package" module Shards class Lock property version : String property shards : Array(Package) CURRENT_VERSION = "2.0" def initialize(@version : String, @shards : Array(Package)) end def self.from_file(path) raise Error.new("Missing #{File.basename(path)}") unless File.exists?(path) from_yaml(File.read(path)) end def self.from_yaml(str) shards = [] of Package pull = YAML::PullParser.new(str) pull.read_stream do pull.read_document do pull.read_mapping do key, version = pull.read_scalar, pull.read_scalar unless key == "version" && version.in?("1.0", "2.0") raise InvalidLock.new end case key = pull.read_scalar when "shards" pull.each_in_mapping do # Shards are parsed as dependencies to keep # compatibility with version 1.0. Calls to `as_package?` # will use the resolver to convert potential references # to explicit versions used in 2.0 format. dep = Dependency.from_yaml(pull) if package = dep.as_package? shards << package else Log.warn { "Lock for shard \"#{dep.name}\" is invalid" } end end else pull.raise "No such attribute #{key} in lock version #{version}" end Lock.new(version, shards) end end end rescue ex : YAML::ParseException raise Error.new("Invalid #{LOCK_FILENAME}. Please delete it and run install again.") ensure pull.close if pull end def self.write(packages : Array(Package), override_path : String?, path : String) File.open(path, "w") do |file| write(packages, override_path, file) end end def self.write(packages : Array(Package), override_path : String?, io : IO) if packages.any?(&.is_override) io << "# NOTICE: This lockfile contains some overrides from #{override_path}\n" end io << "version: #{CURRENT_VERSION}\n" io << "shards:" if packages.empty? io << " {}\n" else io.puts packages.sort_by!(&.name).each do |package| key = package.resolver.class.key io << " " << package.name << ":#{package.is_override ? " # Overridden" : nil}\n" io << " " << key << ": " << package.resolver.source << '\n' io << " version: " << package.version.value << '\n' io << '\n' end end end end end shards-0.19.0/src/logger.cr000066400000000000000000000020041473060476400154700ustar00rootroot00000000000000require "colorize" require "log" module Shards class_property? colors : Bool = Colorize.on_tty_only! end Log.setup_from_env( default_sources: "shards.*", backend: Log::IOBackend.new(formatter: Shards::FORMATTER) ) module Shards Log = ::Log.for(self) def self.set_warning_log_level Log.level = ::Log::Severity::Warn end def self.set_debug_log_level Log.level = ::Log::Severity::Debug end LOGGER_COLORS = { ::Log::Severity::Error => :red, ::Log::Severity::Warn => :yellow, ::Log::Severity::Info => :green, ::Log::Severity::Debug => :light_gray, } FORMATTER = ::Log::Formatter.new do |entry, io| message = entry.message if @@colors io << if color = LOGGER_COLORS[entry.severity]? if idx = message.index(' ') message[0...idx].colorize(color).to_s + message[idx..-1] else message.colorize(color) end else message end else io << entry.severity.label[0] << ": " << message end end end shards-0.19.0/src/molinillo_solver.cr000066400000000000000000000174031473060476400176120ustar00rootroot00000000000000require "molinillo" require "./package" module Shards class MolinilloSolver setter locks : Array(Package)? @solution : Array(Package)? @prereleases : Bool include Molinillo::SpecificationProvider(Shards::Dependency, Shards::Spec) include Molinillo::UI def initialize(@spec : Spec, @override : Override? = nil, *, prereleases = false) @prereleases = prereleases end def prepare(@development = true) end private def add_lock(base, lock_index, dep : Dependency) if lock = lock_index.delete(dep.name) check_single_resolver_by_name dep.resolver base.add_vertex(lock.name, Dependency.new(lock.name, dep.resolver, lock.version), true) # Use the resolver from dependencies (not lock) if available. # This is to allow changing source without bumping the version when possible. if dep.resolver != lock.resolver Log.warn { "Ignoring source of \"#{dep.name}\" on shard.lock" } end spec = begin dep.resolver.spec(lock.version) rescue ex : Shards::Error # If the locked version is not available in the changed source, # `shards update` should be used instead of `shards install`. message = String.build do |io| io << "Locked version #{lock.version} for #{dep.name} was not found in #{dep.resolver}" if dep.resolver != lock.resolver io << " (locked source is #{lock.resolver})" end io << ".\n\nPlease run `shards update`" end raise Shards::Error.new(message, cause: ex) end add_lock base, lock_index, apply_overrides(spec.dependencies) end end private def prefetch_local_caches(deps) return unless Shards.jobs > 1 count = 0 active = Atomic.new(0) ch = Channel(Exception?).new(deps.size + 1) deps.each do |dep| count += 1 active.add(1) while active.get > Shards.jobs sleep 0.1.seconds end spawn do begin dep.resolver.update_local_cache if dep.resolver.is_a? GitResolver ch.send(nil) rescue ex : Exception ch.send(ex) ensure active.sub(1) end end end count.times do obj = ch.receive raise obj if obj.is_a? Exception end end private def add_lock(base, lock_index, deps : Array(Dependency)) prefetch_local_caches(deps) deps.each do |dep| if lock = lock_index[dep.name]? next unless dep.matches?(lock.version) add_lock(base, lock_index, dep) end end end def solve : Array(Package) deps = if @development @spec.dependencies + @spec.development_dependencies else @spec.dependencies end deps = apply_overrides(deps) prefetch_local_caches(deps) base = Molinillo::DependencyGraph(Dependency, Dependency).new if locks = @locks lock_index = locks.to_h { |d| {d.name, d} } add_lock base, lock_index, deps end result = Molinillo::Resolver(Dependency, Spec) .new(self, self) .resolve(deps, base) packages = [] of Package tsort(result).each do |v| next unless v.payload spec = v.payload.as?(Spec) || raise "BUG: returned graph payload was not a Spec" next if spec.name == "crystal" v.requirements.each do |dependency| unless dependency.name == spec.name raise Error.new("Error shard name (#{spec.name}) doesn't match dependency name (#{dependency.name})") end if spec.read_from_yaml? if spec.mismatched_version? Log.warn { "Shard \"#{spec.name}\" version (#{spec.original_version.value}) doesn't match tag version (#{spec.version.value})" } end else Log.warn { "Shard \"#{spec.name}\" version (#{spec.version}) doesn't have a shard.yml file" } end end resolver = spec.resolver || raise "BUG: returned Spec has no resolver" version = spec.version packages << Package.new(spec.name, resolver, version, !on_override(spec).nil?) end packages end private def tsort(graph) sorted_vertices = typeof(graph.vertices).new graph.vertices.values.each do |vertex| if vertex.incoming_edges.empty? tsort_visit(vertex, sorted_vertices) end end sorted_vertices.values end private def tsort_visit(vertex, sorted_vertices) vertex.successors.each do |succ| unless sorted_vertices.has_key?(succ.name) tsort_visit(succ, sorted_vertices) end end sorted_vertices[vertex.name] = vertex end def name_for(spec : Shards::Spec) spec.resolver.not_nil!.name end def name_for(dependency : Shards::Dependency) dependency.name end @search_results = Hash({String, Requirement}, Array(Spec)).new @specs = Hash({String, Version}, Spec).new def search_for(dependency : R) : Array(S) check_single_resolver_by_name dependency.resolver @search_results[{dependency.name, dependency.requirement}] ||= begin resolver = dependency.resolver versions = Versions.sort(versions_for(dependency, resolver)).reverse result = versions.map do |version| @specs[{dependency.name, version}] ||= begin resolver.spec(version).tap do |spec| spec.version = version end end end result end end def on_override(dependency : Dependency | Shards::Spec) : Dependency? @override.try(&.dependencies.find { |o| o.name == dependency.name }) end def apply_overrides(deps : Array(Dependency)) deps.map { |dep| on_override(dep) || dep } end def name_for_explicit_dependency_source SPEC_FILENAME end def name_for_locking_dependency_source LOCK_FILENAME end def dependencies_for(specification : S) : Array(R) apply_overrides(specification.dependencies) end def self.crystal_version_req(specification : Shards::Spec) crystal_pattern = if crystal_version = specification.crystal if crystal_version =~ /^\d+\.\d+(\.\d+)?$/ ">= #{crystal_version}" else crystal_version end else "*" end VersionReq.new(crystal_pattern) end def requirement_satisfied_by?(dependency, activated, spec) unless @prereleases if !spec.version.has_metadata? && spec.version.prerelease? && !dependency.prerelease? vertex = activated.vertex_named(spec.name) return false if !vertex || vertex.requirements.none?(&.prerelease?) end end dependency.matches?(spec.version) end private def versions_for(dependency, resolver) : Array(Version) check_single_resolver_by_name resolver matching = resolver.versions_for(dependency.requirement) if (locks = @locks) && (locked = locks.find { |dep| dep.name == dependency.name }) && dependency.matches?(locked.version) matching << locked.version end matching.uniq end def before_resolution end def after_resolution end def indicate_progress end @used_resolvers = {} of String => Resolver private def check_single_resolver_by_name(resolver : Resolver) if used = @used_resolvers[resolver.name]? if used != resolver raise Error.new("Error shard name (#{resolver.name}) has ambiguous sources: '#{used.yaml_source_entry}' and '#{resolver.yaml_source_entry}'.") end else @used_resolvers[resolver.name] = resolver end end end end shards-0.19.0/src/override.cr000066400000000000000000000035501473060476400160370ustar00rootroot00000000000000require "colorize" require "./ext/yaml" require "./config" require "./dependency" require "./errors" require "./target" module Shards class Override def self.from_file(path, validate = false) path = File.join(path, OVERRIDE_FILENAME) if File.directory?(path) raise Error.new("Missing #{File.basename(path)}") unless File.exists?(path) from_yaml(File.read(path), path, validate) end def self.from_yaml(input, filename = OVERRIDE_FILENAME, validate = false) parser = YAML::PullParser.new(input) parser.read_stream do if parser.kind.stream_end? return new([] of Dependency) end parser.read_document do new(parser, validate) end end rescue ex : YAML::ParseException raise ParseError.new(ex.message, input, filename, ex.line_number, ex.column_number) ensure parser.close if parser end def self.new(pull : YAML::PullParser, validate = false) : self dependencies = nil pull.each_in_mapping do line, column = pull.location case key = pull.read_scalar when "dependencies" check_duplicate(dependencies, "dependencies", line, column) dependencies = [] of Dependency pull.each_in_mapping do dependencies << Dependency.from_yaml(pull) end else if validate pull.raise "unknown attribute: #{key}", line, column else pull.skip end end end new(dependencies || [] of Dependency) end private def self.check_duplicate(argument, name, line, column) unless argument.nil? raise YAML::ParseException.new("duplicate attribute #{name.inspect}", line, column) end end getter dependencies : Array(Dependency) def initialize(@dependencies : Array(Dependency)) end end end shards-0.19.0/src/package.cr000066400000000000000000000077111473060476400156160ustar00rootroot00000000000000require "file_utils" require "./helpers" module Shards class Package getter name : String getter resolver : Resolver getter version : Version getter is_override : Bool @spec : Spec? def initialize(@name, @resolver, @version, @is_override = false) end def_equals @name, @resolver, @version def report_version resolver.report_version(version) end def spec @spec ||= begin if installed? read_installed_spec else resolver.spec(version) end end end private def read_installed_spec path = File.join(install_path, SPEC_FILENAME) unless File.exists?(path) return resolver.spec(version) end begin spec = Spec.from_file(path) spec.version = version spec rescue error : ParseError error.resolver = resolver raise error end end def installed? return false unless File.exists?(install_path) if installed = Shards.info.installed[name]? installed.resolver == resolver && installed.version == version else false end end def install_path File.join(Shards.install_path, name) end def install cleanup_install_directory # install the shard: resolver.install_sources(version, install_path) # link the project's lib path as the shard's lib path, so the dependency # can access transitive dependencies: unless resolver.is_a?(PathResolver) install_lib_path end Shards.info.installed[name] = self Shards.info.save end private def install_lib_path lib_path = File.join(install_path, Shards::INSTALL_DIR) return if File.exists?(lib_path) Log.debug { "Link #{Shards.install_path} to #{lib_path}" } Dir.mkdir_p(File.dirname(lib_path)) target = File.join(Path.new(Shards::INSTALL_DIR).parts.map { ".." }) File.symlink(target, lib_path) end protected def cleanup_install_directory Log.debug { "rm -rf #{Process.quote(install_path)}" } Shards::Helpers.rm_rf(install_path) end def postinstall run_script("postinstall", Shards.skip_postinstall?) rescue ex : Script::Error cleanup_install_directory raise ex end def run_script(name, skip) if installed? && (command = spec.scripts[name]?) if !skip Log.info { "#{name.capitalize} of #{self.name}: #{command}" } Script.run(install_path, command, name, self.name) else Log.info { "#{name.capitalize} of #{self.name}: #{command} (skipped)" } end end end def install_executables return if !installed? || spec.executables.empty? || Shards.skip_executables? Dir.mkdir_p(Shards.bin_path) spec.executables.each do |name| exe_name = find_executable_file(Path[install_path], name) unless exe_name raise Shards::Error.new("Could not find executable #{name.inspect}") end Log.debug { "Install #{exe_name}" } source = File.join(install_path, exe_name) destination = File.join(Shards.bin_path, File.basename(exe_name)) if File.exists?(destination) next if File.same?(destination, source) File.delete(destination) end begin File.link(source, destination) rescue File::Error FileUtils.cp(source, destination) end end end def find_executable_file(install_path, name) each_executable_path(name) do |path| return path if File.exists?(install_path.join(path)) end end private def each_executable_path(name, &) exe = Shards::Helpers.exe(name) yield Path["bin", exe] yield Path["bin", name] unless name == exe end def to_yaml(builder) Dependency.new(name, resolver, version).to_yaml(builder) end def to_s(io) io << name << " (" << report_version << ")" end end end shards-0.19.0/src/requirement.cr000066400000000000000000000017141473060476400165600ustar00rootroot00000000000000module Shards struct VersionReq getter patterns : Array(String) def initialize(patterns) @patterns = patterns.split(',', remove_empty: true).map &.strip end def prerelease? patterns.any? do |pattern| Versions.prerelease? pattern end end def to_s(io) patterns.join(io, ", ") end def to_yaml(yaml) yaml.scalar "version" yaml.scalar to_s end end struct Version getter value : String def initialize(@value) end def has_metadata? Versions.has_metadata? @value end def prerelease? Versions.prerelease? @value end def to_s(io) io << value end def to_yaml(yaml) yaml.scalar "version" yaml.scalar value end end abstract struct Ref end module Any extend self def to_s(io) io << "*" end def to_yaml(yaml) end end alias Requirement = VersionReq | Version | Ref | Any end shards-0.19.0/src/resolvers/000077500000000000000000000000001473060476400157135ustar00rootroot00000000000000shards-0.19.0/src/resolvers/crystal.cr000066400000000000000000000010131473060476400177150ustar00rootroot00000000000000module Shards class CrystalResolver < Resolver INSTANCE = new("crystal", "") def self.key "crystal" end def available_releases : Array(Version) [Version.new Shards.crystal_version] end def read_spec(version : Version) : String? nil end def install_sources(version : Version, install_path : String) raise NotImplementedError.new("CrystalResolver#install_sources") end def report_version(version : Version) : String version.value end end end shards-0.19.0/src/resolvers/fossil.cr000066400000000000000000000345561473060476400175550ustar00rootroot00000000000000require "uri" require "./resolver" require "../versions" require "../logger" require "../helpers" module Shards abstract struct FossilRef < Ref def full_info to_s end end struct FossilBranchRef < FossilRef def initialize(@branch : String) end def to_fossil_ref @branch end def to_s(io) io << "branch " << @branch end def to_yaml(yaml) yaml.scalar "branch" yaml.scalar @branch end end struct FossilTagRef < FossilRef def initialize(@tag : String) end def to_fossil_ref @tag end def to_s(io) io << "tag " << @tag end def to_yaml(yaml) yaml.scalar "tag" yaml.scalar @tag end end struct FossilCommitRef < FossilRef getter commit : String def initialize(@commit : String) end def =~(other : FossilCommitRef) commit.starts_with?(other.commit) || other.commit.starts_with?(commit) end def to_fossil_ref @commit end def to_s(io) io << "commit " << @commit[0...7] end def full_info "commit #{@commit}" end def to_yaml(yaml) yaml.scalar "commit" yaml.scalar @commit end end struct FossilTrunkRef < FossilRef def to_fossil_ref "trunk" end def to_s(io) io << "trunk" end def to_yaml(yaml) raise NotImplementedError.new("FossilTrunkRef is for internal use only") end end class FossilResolver < Resolver @@has_fossil_command : Bool? @@fossil_version_maj : Int8? @@fossil_version_min : Int8? @@fossil_version_rev : Int8? @@fossil_version : String? @origin_url : String? @local_fossil_file : String? def self.key "fossil" end def self.normalize_key_source(key : String, source : String) : {String, String} case key when "fossil" {"fossil", source} else raise "Unknown resolver #{key}" end end protected def self.has_fossil_command? if @@has_fossil_command.nil? @@has_fossil_command = (Process.run("fossil version", shell: true).success? rescue false) end @@has_fossil_command end protected def self.fossil_version unless @@fossil_version @@fossil_version = `fossil version`[/version\s+([^\s]*)/, 1] pieces = @@fossil_version.not_nil!.split('.') @@fossil_version_maj = pieces[0].to_i8 @@fossil_version_min = pieces[1].to_i8 @@fossil_version_rev = (pieces[2]?.try &.to_i8 || 0i8) end @@fossil_version end protected def self.fossil_version_maj self.fossil_version unless @@fossil_version_maj @@fossil_version_maj.not_nil! end protected def self.fossil_version_min self.fossil_version unless @@fossil_version_min @@fossil_version_min.not_nil! end protected def self.fossil_version_rev self.fossil_version unless @@fossil_version_rev @@fossil_version_rev.not_nil! end def read_spec(version : Version) : String? update_local_cache ref = fossil_ref(version) if file_exists?(ref, SPEC_FILENAME) capture("fossil cat -R #{Process.quote(local_fossil_file)} #{Process.quote(SPEC_FILENAME)} -r #{Process.quote(ref.to_fossil_ref)}") else Log.debug { "Missing \"#{SPEC_FILENAME}\" for #{name.inspect} at #{ref}" } nil end end private def spec_at_ref(ref : FossilRef, commit) : Spec update_local_cache unless capture("fossil ls -R #{Process.quote(local_fossil_file)} -r #{Process.quote(ref.to_fossil_ref)} #{Process.quote(SPEC_FILENAME)}").strip == SPEC_FILENAME raise Error.new "No #{SPEC_FILENAME} was found for shard #{name.inspect} at commit #{commit}" end spec_yaml = capture("fossil cat -R #{Process.quote(local_fossil_file)} #{Process.quote(SPEC_FILENAME)} -r #{Process.quote(ref.to_fossil_ref)}") begin Spec.from_yaml(spec_yaml) rescue error : Error raise Error.new "Invalid #{SPEC_FILENAME} for shard #{name.inspect} at commit #{commit}: #{error.message}" end end private def spec?(version) spec(version) rescue Error end def available_releases : Array(Version) update_local_cache versions_from_tags end def latest_version_for_ref(ref : FossilRef?) : Version update_local_cache ref ||= FossilTrunkRef.new begin commit = commit_sha1_at(ref) rescue Error raise Error.new "Could not find #{ref.full_info} for shard #{name.inspect} in the repository #{source}" end if spec = spec_at_ref(ref, commit) Version.new "#{spec.version.value}+fossil.commit.#{commit}" else raise Error.new "No #{SPEC_FILENAME} was found for shard #{name.inspect} at commit #{commit}" end end def matches_ref?(ref : FossilRef, version : Version) case ref when FossilCommitRef ref =~ fossil_ref(version) when FossilBranchRef, FossilTrunkRef # TODO: check if version is the branch version.has_metadata? else # TODO: check branch and tags true end end protected def versions_from_tags capture("fossil tag list -R #{Process.quote(local_fossil_file)}") .split('\n') .compact_map { |tag| Version.new($1) if tag =~ VERSION_TAG } end def install_sources(version : Version, install_path : String) update_local_cache ref = fossil_ref(version) FileUtils.rm_r(install_path) if File.exists?(install_path) Dir.mkdir_p(install_path) Log.debug { "Local path: #{local_path}" } Log.debug { "Install path: #{install_path}" } # The --workdir argument was introduced in version 2.12, so we have to # fake it if FossilResolver.fossil_version_maj <= 2 && FossilResolver.fossil_version_min < 12 Log.debug { "Opening Fossil repo #{local_fossil_file} in directory #{install_path}" } run("fossil open #{local_fossil_file} #{Process.quote(ref.to_fossil_ref)} --nested", install_path) else run "fossil open #{local_fossil_file} #{Process.quote(ref.to_fossil_ref)} --nested --workdir #{install_path}" end end def commit_sha1_at(ref : FossilRef) # Fossil versions before 2.14 do not support the --format/-F for the # timeline command. if FossilResolver.fossil_version_maj <= 2 && FossilResolver.fossil_version_min < 14 # Capture the short artifact name from the timeline using a regex. # -W 0 = unlimited line width # -n 1 = limit results to one entry # -t ci = Display only checkins on the timeline shortShas = capture("fossil timeline #{Process.quote(ref.to_fossil_ref)} -t ci -W 0 -n 1 -R #{Process.quote(local_fossil_file)}") # We only want the lines with short artifact names retLines = shortShas.strip.split('\n').flat_map do |line| /^.+ \[(.+)\].*/.match(line).try &.[1] end # Remove empty results retLines.reject! &.nil? return "" if retLines.empty? # Call the whatis command so we can properly expand the short artifact # name to the full artifact hash. whatis = capture("fossil whatis #{retLines[0]} -R #{Process.quote(local_fossil_file)}") /artifact:\s+(.+)/.match(whatis).try &.[1] || "" else # Fossil v2.14 and newer support -F %H, so use that. capture("fossil timeline #{Process.quote(ref.to_fossil_ref)} -t ci -F %H -n 1 -R #{Process.quote(local_fossil_file)}").split('\n')[0] end end def local_path @local_path ||= begin uri = parse_uri(fossil_url) path = uri.path path = Path[path] # E.g. turns "c:\local\path.git" into "c\local\path.git". Or just drops the leading slash. if (anchor = path.anchor) path = Path[path.drive.to_s.rchop(":"), path.relative_to(anchor)] end if host = uri.host File.join(Shards.cache_path, host) else File.join(Shards.cache_path, path) end end end def local_fossil_file @local_fossil_file ||= Path[local_path].join("#{name}.fossil").normalize.to_s end def fossil_url source.strip end def parse_requirement(params : Hash(String, String)) : Requirement params.each do |key, value| case key when "branch" return FossilBranchRef.new value when "tag" return FossilTagRef.new value when "commit" return FossilCommitRef.new value else end end super end record FossilVersion, value : String, commit : String? = nil private def parse_fossil_version(version : Version) : FossilVersion case version.value when VERSION_REFERENCE FossilVersion.new version.value when VERSION_AT_FOSSIL_COMMIT FossilVersion.new $1, $2 else raise Error.new("Invalid version for fossil resolver: #{version}") end end private def fossil_ref(version : Version) : FossilRef fossil_version = parse_fossil_version(version) if commit = fossil_version.commit FossilCommitRef.new commit else FossilTagRef.new "v#{fossil_version.value}" end end def update_local_cache if cloned_repository? && origin_changed? delete_repository @updated_cache = false end return if Shards.local? || @updated_cache Log.info { "Fetching #{fossil_url}" } if cloned_repository? # repositories cloned with shards v0.8.0 won't fetch any new remote # refs; we must delete them and clone again! if valid_repository? fetch_repository else delete_repository mirror_repository end else mirror_repository end @updated_cache = true end private def mirror_repository path = local_path fossil_file = Path[path].join("#{name}.fossil").to_s Dir.mkdir_p(path) FileUtils.rm(fossil_file) if File.exists?(fossil_file) source = fossil_url # Remove a "file://" from the beginning, otherwise the path might be invalid # on Windows. source = source.lchop("file://") fossil_retry(err: "Failed to clone #{source}") do run_in_current_folder "fossil clone #{Process.quote(source)} #{Process.quote(fossil_file)}" end end private def fetch_repository fossil_retry(err: "Failed to update #{fossil_url}") do run "fossil pull -R #{Process.quote(local_fossil_file)}" end end private def fossil_retry(err = "Failed to fetch repository", &) retries = 0 loop do yield break rescue inner_err : Error retries += 1 next if retries < 3 Log.debug { inner_err } raise Error.new("#{err}: #{inner_err}") end end private def delete_repository Log.debug { "rm -rf #{Process.quote(local_path)}'" } Shards::Helpers.rm_rf(local_path) Log.debug { "rm -rf #{Process.quote(local_fossil_file)}'" } Shards::Helpers.rm_rf(local_fossil_file) @origin_url = nil end private def cloned_repository? # Check for both the local_path and the local_fossil_file, otherwise this # method can give false positives if it's looking for repositories from # the same base site. Dir.exists?(local_path) && File.exists?(local_fossil_file) end private def valid_repository? File.exists?(local_fossil_file) end protected def origin_url @origin_url ||= capture("fossil remote-url -R #{Process.quote(local_fossil_file)}").strip end # Returns whether origin URLs have differing hosts and/or paths. protected def origin_changed? return false if origin_url == fossil_url return true if origin_url.nil? || fossil_url.nil? origin_parsed = parse_uri(origin_url) fossil_parsed = parse_uri(fossil_url) (origin_parsed.host != fossil_parsed.host) || (origin_parsed.path != fossil_parsed.path) end # Parses a URI string private def parse_uri(raw_uri) # Need to check for file URIs early, otherwise generic parsing will fail on a colon. if (path = raw_uri.lchop?("file://")) return URI.new(scheme: "file", path: path) end # Try normal URI parsing first uri = URI.parse(raw_uri) return uri if uri.absolute? && !uri.opaque? # Otherwise, assume and attempt to parse the scp-style ssh URIs host, _, path = raw_uri.partition(':') if host.includes?('@') user, _, host = host.partition('@') end # Normalize leading slash, matching URI parsing unless path.starts_with?('/') path = '/' + path end URI.new(scheme: "ssh", host: host, path: path, user: user) end private def file_exists?(ref : FossilRef, path) files = capture("fossil ls -R #{Process.quote(local_fossil_file)} -r #{Process.quote(ref.to_fossil_ref)} #{Process.quote(path)}") !files.strip.empty? end private def capture(command, path = local_path) run(command, capture: true, path: path).not_nil! end private def run(command, path = local_path, capture = false) if Shards.local? && !Dir.exists?(path) dependency_name = File.basename(path, ".fossil") raise Error.new("Missing repository cache for #{dependency_name.inspect}. Please run without --local to fetch it.") end Dir.cd(path) do run_in_current_folder(command, capture) end end private def run_in_current_folder(command, capture = false) unless FossilResolver.has_fossil_command? raise Error.new("Error missing fossil command line tool. Please install Fossil first!") end Log.debug { command } STDERR.flush output = capture ? IO::Memory.new : Process::Redirect::Close error = IO::Memory.new status = Process.run(command, shell: true, output: output, error: error) if status.success? output.to_s if capture else message = error.to_s raise Error.new("Failed #{command} (#{message}). Maybe a commit, branch or file doesn't exist?") end end def report_version(version : Version) : String fossil_version = parse_fossil_version(version) if commit = fossil_version.commit "#{fossil_version.value} at #{commit[0...7]}" else version.value end end register_resolver "fossil", FossilResolver end end shards-0.19.0/src/resolvers/git.cr000066400000000000000000000312611473060476400170270ustar00rootroot00000000000000require "uri" require "./resolver" require "../versions" require "../logger" require "../helpers" module Shards abstract struct GitRef < Ref def full_info to_s end end struct GitBranchRef < GitRef def initialize(@branch : String) end def to_git_ref "refs/heads/#{@branch}" end def to_s(io) io << "branch " << @branch end def to_yaml(yaml) yaml.scalar "branch" yaml.scalar @branch end end struct GitTagRef < GitRef def initialize(@tag : String) end def to_git_ref "refs/tags/#{@tag}" end def to_s(io) io << "tag " << @tag end def to_yaml(yaml) yaml.scalar "tag" yaml.scalar @tag end end struct GitCommitRef < GitRef getter commit : String def initialize(@commit : String) end def =~(other : GitCommitRef) commit.starts_with?(other.commit) || other.commit.starts_with?(commit) end def to_git_ref @commit end def to_s(io) io << "commit " << @commit[0...7] end def full_info "commit #{@commit}" end def to_yaml(yaml) yaml.scalar "commit" yaml.scalar @commit end end struct GitHeadRef < GitRef def to_git_ref "HEAD" end def to_s(io) io << "HEAD" end def to_yaml(yaml) raise NotImplementedError.new("GitHeadRef is for internal use only") end end class GitResolver < Resolver @@has_git_command : Bool? @@git_column_never : String? @@git_version : String? @origin_url : String? def self.key "git" end private KNOWN_PROVIDERS = { "www.github.com", "github.com", "www.bitbucket.com", "bitbucket.com", "www.gitlab.com", "gitlab.com", "www.codeberg.org", "codeberg.org", } def self.normalize_key_source(key : String, source : String) : {String, String} case key when "git" uri = URI.parse(source) downcased_host = uri.host.try &.downcase scheme = uri.scheme.try &.downcase if scheme.in?("git", "http", "https") && downcased_host && downcased_host.in?(KNOWN_PROVIDERS) # browsers are requested to enforce HTTP Strict Transport Security uri.scheme = "https" downcased_path = uri.path.downcase uri.path = downcased_path.ends_with?(".git") ? downcased_path : "#{downcased_path}.git" uri.host = downcased_host.lchop("www.") {"git", uri.to_s} else {"git", source} end when "github", "bitbucket", "gitlab" {"git", "https://#{key}.com/#{source.downcase}.git"} when "codeberg" {"git", "https://#{key}.org/#{source.downcase}.git"} else raise "Unknown resolver #{key}" end end protected def self.has_git_command? if @@has_git_command.nil? @@has_git_command = (Process.run("git", ["--version"]).success? rescue false) end @@has_git_command end protected def self.git_version @@git_version ||= `git --version`.strip[12..-1] end protected def self.git_column_never @@git_column_never ||= Versions.compare(git_version, "1.7.11") < 0 ? "--column=never" : "" end def read_spec(version : Version) : String? update_local_cache ref = git_ref(version) if file_exists?(ref, SPEC_FILENAME) capture("git show #{Process.quote("#{ref.to_git_ref}:#{SPEC_FILENAME}")}") else Log.debug { "Missing \"#{SPEC_FILENAME}\" for #{name.inspect} at #{ref}" } nil end end private def spec_at_ref(ref : GitRef, commit) : Spec update_local_cache unless file_exists?(ref, SPEC_FILENAME) raise Error.new "No #{SPEC_FILENAME} was found for shard #{name.inspect} at commit #{commit}" end spec_yaml = capture("git show #{Process.quote("#{ref.to_git_ref}:#{SPEC_FILENAME}")}") begin Spec.from_yaml(spec_yaml) rescue error : Error raise Error.new "Invalid #{SPEC_FILENAME} for shard #{name.inspect} at commit #{commit}: #{error.message}" end end private def spec?(version) spec(version) rescue Error end def available_releases : Array(Version) update_local_cache versions_from_tags end def latest_version_for_ref(ref : GitRef?) : Version update_local_cache ref ||= GitHeadRef.new begin commit = commit_sha1_at(ref) rescue Error raise Error.new "Could not find #{ref.full_info} for shard #{name.inspect} in the repository #{source}" end spec = spec_at_ref(ref, commit) Version.new "#{spec.version.value}+git.commit.#{commit}" end def matches_ref?(ref : GitRef, version : Version) case ref when GitCommitRef ref =~ git_ref(version) when GitBranchRef, GitHeadRef # TODO: check if version is the branch version.has_metadata? else # TODO: check branch and tags true end end protected def versions_from_tags capture("git tag --list #{GitResolver.git_column_never}") .split('\n') .compact_map { |tag| Version.new($1) if tag =~ VERSION_TAG } end def install_sources(version : Version, install_path : String) update_local_cache ref = git_ref(version) Dir.mkdir_p(install_path) run "git --work-tree=#{Process.quote(install_path)} checkout #{Process.quote(ref.to_git_ref)} -- ." end def commit_sha1_at(ref : GitRef) capture("git log -n 1 --pretty=%H #{Process.quote(ref.to_git_ref)}").strip end def local_path @local_path ||= begin uri = parse_uri(git_url) path = uri.path path += ".git" unless path.ends_with?(".git") path = Path[path] # E.g. turns "c:\local\path.git" into "c\local\path.git". Or just drops the leading slash. if (anchor = path.anchor) path = Path[path.drive.to_s.rchop(":"), path.relative_to(anchor)] end if host = uri.host File.join(Shards.cache_path, host, path) else File.join(Shards.cache_path, path) end end end def git_url source.strip end def parse_requirement(params : Hash(String, String)) : Requirement params.each do |key, value| case key when "branch" return GitBranchRef.new value when "tag" return GitTagRef.new value when "commit" return GitCommitRef.new value else end end super end record GitVersion, value : String, commit : String? = nil private def parse_git_version(version : Version) : GitVersion case version.value when VERSION_REFERENCE GitVersion.new version.value when VERSION_AT_GIT_COMMIT GitVersion.new $1, $2 else raise Error.new("Invalid version for git resolver: #{version}") end end private def git_ref(version : Version) : GitRef git_version = parse_git_version(version) if commit = git_version.commit GitCommitRef.new commit else GitTagRef.new "v#{git_version.value}" end end def update_local_cache if cloned_repository? && origin_changed? delete_repository @updated_cache = false end return if Shards.local? || @updated_cache Log.info { "Fetching #{git_url}" } if cloned_repository? # repositories cloned with shards v0.8.0 won't fetch any new remote # refs; we must delete them and clone again! if valid_repository? fetch_repository else delete_repository mirror_repository end else mirror_repository end @updated_cache = true end private def mirror_repository # The git-config option core.askPass is set to a command that is to be # called when git needs to ask for credentials (for example on a 401 # response over HTTP). Setting the command to `true` effectively # disables the credential prompt, because `shards install` is not to # be used interactively. # This configuration can be overridden by defining the environment # variable `GIT_ASKPASS`. git_retry(err: "Failed to clone #{git_url}") do run_in_folder "git clone -c core.askPass=true -c init.templateDir= --mirror --quiet -- #{Process.quote(git_url)} #{Process.quote(local_path)}" end end private def fetch_repository git_retry(err: "Failed to update #{git_url}") do run "git fetch --all --quiet" end end private def git_retry(err = "Failed to fetch repository", &) retries = 0 loop do yield break rescue inner_err : Error retries += 1 next if retries < 3 Log.debug { inner_err } raise Error.new("#{err}: #{inner_err}") end end private def delete_repository Log.debug { "rm -rf #{Process.quote(local_path)}'" } Shards::Helpers.rm_rf(local_path) @origin_url = nil end private def cloned_repository? Dir.exists?(local_path) end private def valid_repository? command = "git config --get remote.origin.mirror" Log.debug { command } output = Process.run(command, shell: true, output: :pipe, chdir: local_path) do |process| process.output.gets_to_end end return $?.success? && output.chomp == "true" end private def origin_url @origin_url ||= capture("git ls-remote --get-url origin").strip end # Returns whether origin URLs have differing hosts and/or paths. protected def origin_changed? return false if origin_url == git_url return true if origin_url.nil? || git_url.nil? origin_parsed = parse_uri(origin_url) git_parsed = parse_uri(git_url) (origin_parsed.host != git_parsed.host) || (origin_parsed.path != git_parsed.path) end # Parses a URI string, with additional support for ssh+git URI schemes. private def parse_uri(raw_uri) # Need to check for file URIs early, otherwise generic parsing will fail on a colon. if (path = raw_uri.lchop?("file://")) return URI.new(scheme: "file", path: path) end # Try normal URI parsing first uri = URI.parse(raw_uri) return uri if uri.absolute? && !uri.opaque? # Otherwise, assume and attempt to parse the scp-style ssh URIs host, _, path = raw_uri.partition(':') if host.includes?('@') user, _, host = host.partition('@') end # Normalize leading slash, matching URI parsing unless path.starts_with?('/') path = '/' + path end URI.new(scheme: "ssh", host: host, path: path, user: user) end private def file_exists?(ref : GitRef, path) files = capture("git ls-tree -r --full-tree --name-only #{Process.quote(ref.to_git_ref)} -- #{Process.quote(path)}") !files.strip.empty? end private def capture(command, path = local_path) run(command, capture: true, path: path).not_nil! end private def run(command, path = local_path, capture = false) if Shards.local? && !Dir.exists?(path) dependency_name = File.basename(path, ".git") raise Error.new("Missing repository cache for #{dependency_name.inspect}. Please run without --local to fetch it.") end run_in_folder(command, path, capture) end # Chdir to a folder and run command. # Runs in current folder if `path` is nil. private def run_in_folder(command, path : String? = nil, capture = false) unless GitResolver.has_git_command? raise Error.new("Error missing git command line tool. Please install Git first!") end Log.debug { command } output = capture ? IO::Memory.new : Process::Redirect::Close error = IO::Memory.new status = Process.run(command, shell: true, output: output, error: error, chdir: path) if status.success? output.to_s if capture else str = error.to_s if str.starts_with?("error: ") && (idx = str.index('\n')) message = str[7...idx] raise Error.new("Failed #{command} (#{message}). Maybe a commit, branch or file doesn't exist?") else raise Error.new("Failed #{command}.\n#{str}") end end end def report_version(version : Version) : String git_version = parse_git_version(version) if commit = git_version.commit "#{git_version.value} at #{commit[0...7]}" else version.value end end register_resolver "git", GitResolver register_resolver "github", GitResolver register_resolver "gitlab", GitResolver register_resolver "bitbucket", GitResolver register_resolver "codeberg", GitResolver end end shards-0.19.0/src/resolvers/hg.cr000066400000000000000000000271111473060476400166410ustar00rootroot00000000000000require "uri" require "./resolver" require "../versions" require "../logger" require "../helpers" module Shards abstract struct HgRef < Ref def full_info to_s end end struct HgBranchRef < HgRef def initialize(@branch : String) end def to_hg_ref @branch end def to_hg_revset "branch(\"#{@branch}\") and head()" end def to_s(io) io << "branch " << @branch end def to_yaml(yaml) yaml.scalar "branch" yaml.scalar @branch end end struct HgBookmarkRef < HgRef def initialize(@bookmark : String) end def to_hg_ref @bookmark end def to_hg_revset "bookmark(\"#{@bookmark}\")" end def to_s(io) io << "bookmark " << @bookmark end def to_yaml(yaml) yaml.scalar "bookmark" yaml.scalar @bookmark end end struct HgTagRef < HgRef def initialize(@tag : String) end def to_hg_ref @tag end def to_hg_revset "tag(\"#{@tag}\")" end def to_s(io) io << "tag " << @tag end def to_yaml(yaml) yaml.scalar "tag" yaml.scalar @tag end end struct HgCommitRef < HgRef getter commit : String def initialize(@commit : String) end def =~(other : HgCommitRef) commit.starts_with?(other.commit) || other.commit.starts_with?(commit) end def to_hg_ref @commit end def to_hg_revset @commit end def to_s(io) io << "commit " << @commit[0...7] end def full_info "commit #{@commit}" end def to_yaml(yaml) yaml.scalar "commit" yaml.scalar @commit end end struct HgCurrentRef < HgRef def to_hg_revset "." end def to_hg_ref "." end def to_s(io) io << "current" end def to_yaml(yaml) raise NotImplementedError.new("HgCurrentRef is for internal use only") end end class HgResolver < Resolver @@has_hg_command : Bool? @@hg_version : String? @origin_url : String? def self.key "hg" end def self.normalize_key_source(key : String, source : String) : {String, String} case key when "hg" {"hg", source} else raise "Unknown resolver #{key}" end end protected def self.has_hg_command? if @@has_hg_command.nil? @@has_hg_command = (Process.run("hg", ["--version"]).success? rescue false) end @@has_hg_command end protected def self.hg_version @@hg_version ||= `hg --version`[/\(version\s+([^)]*)\)/, 1] end def read_spec(version : Version) : String? update_local_cache ref = hg_ref(version) if file_exists?(ref, SPEC_FILENAME) capture("hg cat -r #{Process.quote(ref.to_hg_revset)} #{Process.quote(SPEC_FILENAME)}") else Log.debug { "Missing \"#{SPEC_FILENAME}\" for #{name.inspect} at #{ref}" } nil end end private def spec_at_ref(ref : HgRef) : Spec? update_local_cache begin if file_exists?(ref, SPEC_FILENAME) spec_yaml = capture("hg cat -r #{Process.quote(ref.to_hg_revset)} #{Process.quote(SPEC_FILENAME)}") Spec.from_yaml(spec_yaml) end rescue Error nil end end private def spec?(version) spec(version) rescue Error end def available_releases : Array(Version) update_local_cache versions_from_tags end def latest_version_for_ref(ref : HgRef?) : Version update_local_cache ref ||= HgCurrentRef.new begin commit = commit_sha1_at(ref) rescue Error raise Error.new "Could not find #{ref.full_info} for shard #{name.inspect} in the repository #{source}" end if spec = spec_at_ref(ref) Version.new "#{spec.version.value}+hg.commit.#{commit}" else raise Error.new "No #{SPEC_FILENAME} was found for shard #{name.inspect} at commit #{commit}" end end def matches_ref?(ref : HgRef, version : Version) case ref when HgCommitRef ref =~ hg_ref(version) when HgBranchRef, HgBookmarkRef, HgCurrentRef # TODO: check if version is the branch version.has_metadata? else # TODO: check branch and tags true end end protected def versions_from_tags capture("hg tags --template #{Process.quote("{tag}\n")}") .lines .sort! .compact_map { |tag| Version.new($1) if tag =~ VERSION_TAG } end def install_sources(version : Version, install_path : String) update_local_cache ref = hg_ref(version) FileUtils.rm_r(install_path) if File.exists?(install_path) Dir.mkdir_p(install_path) run "hg clone --quiet -u #{Process.quote(ref.to_hg_ref)} -- #{Process.quote(local_path)} #{Process.quote(install_path)}" end def commit_sha1_at(ref : HgRef) capture("hg log -r #{Process.quote(ref.to_hg_revset)} --template #{Process.quote("{node}\n")}").strip end def local_path @local_path ||= begin uri = parse_uri(hg_url) path = uri.path path = Path[path] # E.g. turns "c:\local\path" into "c\local\path". Or just drops the leading slash. if (anchor = path.anchor) path = Path[path.drive.to_s.rchop(":"), path.relative_to(anchor)] end if host = uri.host File.join(Shards.cache_path, host, path) else File.join(Shards.cache_path, path) end end end def hg_url source.strip end def parse_requirement(params : Hash(String, String)) : Requirement params.each do |key, value| case key when "branch" return HgBranchRef.new value when "bookmark" return HgBookmarkRef.new value when "tag" return HgTagRef.new value when "commit" return HgCommitRef.new value end end super end record HgVersion, value : String, commit : String? = nil private def parse_hg_version(version : Version) : HgVersion case version.value when VERSION_REFERENCE HgVersion.new version.value when VERSION_AT_HG_COMMIT HgVersion.new $1, $2 else raise Error.new("Invalid version for hg resolver: #{version}") end end private def hg_ref(version : Version) : HgRef hg_version = parse_hg_version(version) if commit = hg_version.commit HgCommitRef.new commit else HgTagRef.new "v#{hg_version.value}" end end def update_local_cache if cloned_repository? && origin_changed? delete_repository @updated_cache = false end return if Shards.local? || @updated_cache Log.info { "Fetching #{hg_url}" } if cloned_repository? # repositories cloned with shards v0.8.0 won't fetch any new remote # refs; we must delete them and clone again! if valid_repository? fetch_repository else delete_repository mirror_repository end else mirror_repository end @updated_cache = true end private def mirror_repository path = local_path FileUtils.rm_r(path) if File.exists?(path) Dir.mkdir_p(path) source = hg_url # Remove a "file://" from the beginning, otherwise the path might be invalid # on Windows. source = source.lchop("file://") hg_retry(err: "Failed to clone #{source}") do # We checkout the working directory so that "." is meaningful. # # An alternative would be to use the `@` bookmark, but only as long # as nothing new is committed. run_in_current_folder "hg clone --quiet -- #{Process.quote(source)} #{Process.quote(path)}" end end private def fetch_repository hg_retry(err: "Failed to update #{hg_url}") do run "hg pull" end end private def hg_retry(err = "Failed to update repository", &) retries = 0 loop do return yield rescue ex : Error retries += 1 next if retries < 3 raise Error.new("#{err}: #{ex}") end end private def delete_repository Log.debug { "rm -rf #{Process.quote(local_path)}" } Shards::Helpers.rm_rf(local_path) @origin_url = nil end private def cloned_repository? Dir.exists?(local_path) end private def valid_repository? File.exists?(File.join(local_path, ".hg", "dirstate")) end private def origin_url @origin_url ||= capture("hg paths default").strip end # Returns whether origin URLs have differing hosts and/or paths. protected def origin_changed? return false if origin_url == hg_url return true if origin_url.nil? || hg_url.nil? origin_parsed = parse_uri(origin_url) hg_parsed = parse_uri(hg_url) (origin_parsed.host != hg_parsed.host) || (origin_parsed.path != hg_parsed.path) end # Parses a URI string, with additional support for ssh+git URI schemes. private def parse_uri(raw_uri) # Need to check for file URIs early, otherwise generic parsing will fail on a colon. if (path = raw_uri.lchop?("file://")) return URI.new(scheme: "file", path: path) end # Try normal URI parsing first uri = URI.parse(raw_uri) return uri if uri.absolute? && !uri.opaque? # Otherwise, assume and attempt to parse the scp-style ssh URIs host, _, path = raw_uri.partition(':') if host.includes?('@') user, _, host = host.partition('@') end # Normalize leading slash, matching URI parsing unless path.starts_with?('/') path = '/' + path end URI.new(scheme: "ssh", host: host, path: path, user: user) end private def file_exists?(ref : HgRef, path) run("hg files -r #{Process.quote(ref.to_hg_revset)} -- #{Process.quote(path)}", raise_on_fail: false) end private def capture(command, path = local_path) run(command, capture: true, path: path).as(String) end private def run(command, path = local_path, capture = false, raise_on_fail = true) if Shards.local? && !Dir.exists?(path) dependency_name = File.basename(path) raise Error.new("Missing repository cache for #{dependency_name.inspect}. Please run without --local to fetch it.") end Dir.cd(path) do run_in_current_folder(command, capture, raise_on_fail: raise_on_fail) end end private def run_in_current_folder(command, capture = false, raise_on_fail = true) unless HgResolver.has_hg_command? raise Error.new("Error missing hg command line tool. Please install Mercurial first!") end Log.debug { command } output = capture ? IO::Memory.new : Process::Redirect::Close error = IO::Memory.new status = Process.run(command, shell: true, output: output, error: error) if status.success? if capture output.to_s else true end elsif raise_on_fail str = error.to_s if str.starts_with?("abort: ") && (idx = str.index('\n')) message = str[7...idx] raise Error.new("Failed #{command} (#{message}). Maybe a commit, branch, bookmark or file doesn't exist?") else raise Error.new("Failed #{command}.\n#{str}") end end end def report_version(version : Version) : String hg_version = parse_hg_version(version) if commit = hg_version.commit "#{hg_version.value} at #{commit[0...7]}" else version.value end end register_resolver "hg", HgResolver end end shards-0.19.0/src/resolvers/path.cr000066400000000000000000000022511473060476400171750ustar00rootroot00000000000000require "./resolver" module Shards class PathResolver < Resolver def self.key "path" end def read_spec(version = nil) : String? spec_path = File.join(expanded_local_path, SPEC_FILENAME) if File.exists?(spec_path) File.read(spec_path) else raise Error.new("Missing #{SPEC_FILENAME.inspect} for #{name.inspect} at #{File.expand_path(local_path).inspect}") end end def spec(version = nil) load_spec(version) || raise Error.new("Can't read spec for #{name.inspect}") end def available_releases : Array(Version) [spec(nil).version] end def local_path source end private def expanded_local_path File.expand_path(local_path, home: true).tap do |path| raise Error.new("Failed no such path: #{path}") unless Dir.exists?(path) end end def install_sources(version, install_path) path = expanded_local_path Dir.mkdir_p(File.dirname(install_path)) File.symlink(path, install_path) end def report_version(version : Version) : String "#{version.value} at #{source}" end register_resolver "path", PathResolver end end shards-0.19.0/src/resolvers/resolver.cr000066400000000000000000000065161473060476400201120ustar00rootroot00000000000000require "file_utils" require "../spec" require "../dependency" require "../errors" require "../script" module Shards abstract class Resolver getter name : String getter source : String def initialize(@name : String, @source : String) end def self.build(key : String, name : String, source : String) _, source = self.normalize_key_source(key, source) self.new(name, source) end def self.normalize_key_source(key : String, source : String) {key, source} end def ==(other : Resolver) return true if super return false unless self.class == other.class name == other.name && source == other.source end def yaml_source_entry "#{self.class.key}: #{source}" end def to_s(io : IO) io << yaml_source_entry end def versions_for(req : Requirement) : Array(Version) case req when Version then [req] when Ref [latest_version_for_ref(req)] when VersionReq Versions.resolve(available_releases, req) when Any releases = available_releases if releases.empty? [latest_version_for_ref(nil)] else releases end else raise Error.new("Unexpected requirement type: #{req}") end end abstract def available_releases : Array(Version) def latest_version_for_ref(ref : Ref?) : Version raise "Unsupported ref type for this resolver: #{ref}" end def matches_ref?(ref : Ref, version : Version) false end def spec(version : Version) : Spec if spec = load_spec(version) spec.version = version spec else Spec.new(name, version, self) end end private def load_spec(version) if spec_yaml = read_spec(version) Spec.from_yaml(spec_yaml).tap do |spec| spec.resolver = self end end rescue error : ParseError error.resolver = self raise error end abstract def read_spec(version : Version) : String? abstract def install_sources(version : Version, install_path : String) abstract def report_version(version : Version) : String def update_local_cache end def parse_requirement(params : Hash(String, String)) : Requirement if version = params["version"]? VersionReq.new version else Any end end private record ResolverCacheKey, key : String, name : String, source : String private RESOLVER_CLASSES = {} of String => Resolver.class private RESOLVER_CACHE = {} of ResolverCacheKey => Resolver def self.register_resolver(key, resolver) RESOLVER_CLASSES[key] = resolver end def self.clear_resolver_cache RESOLVER_CACHE.clear end def self.find_class(key : String) : Resolver.class | Nil RESOLVER_CLASSES[key]? end def self.find_resolver(key : String, name : String, source : String) resolver_class = if self == Resolver RESOLVER_CLASSES[key]? || raise Error.new("Failed can't resolve dependency #{name} (unsupported resolver)") else self end key, source = resolver_class.normalize_key_source(key, source) RESOLVER_CACHE[ResolverCacheKey.new(key, name, source)] ||= begin resolver_class.build(key, name, source) end end end end require "./*" shards-0.19.0/src/script.cr000066400000000000000000000006451473060476400155260ustar00rootroot00000000000000module Shards module Script class Error < Error end def self.run(path, command, script_name, dependency_name) Dir.cd(path) do output = IO::Memory.new status = Process.run(command, shell: true, output: output, error: output) raise Error.new("Failed #{script_name} of #{dependency_name} on #{command}:\n#{output.to_s.rstrip}") unless status.success? end end end end shards-0.19.0/src/shards.cr000066400000000000000000000001351473060476400155000ustar00rootroot00000000000000require "./config" require "./logger" require "./errors" require "./version" require "./cli" shards-0.19.0/src/spec.cr000066400000000000000000000137651473060476400151630ustar00rootroot00000000000000require "colorize" require "./ext/yaml" require "./config" require "./dependency" require "./errors" require "./target" module Shards class Spec class Author property name : String property email : String? def self.new(pull : YAML::PullParser) new(pull.read_scalar) end def initialize(name) if name =~ /\A\s*(.+?)\s*<(\s*.+?\s*)>/ @name, @email = $1, $2 else @name = name end end end class Library property soname : String property version : String def self.new(pull : YAML::PullParser) name = pull.read_scalar line, column = pull.location version = pull.read_scalar.strip if pull.kind.scalar? if !version || version.try(&.empty?) pull.raise "library version for #{name} can't be empty, use * for any version", line, column end new(name, version) end def initialize(@soname, @version) end end def to_s(io) io << name << " " << version end def self.from_file(path, validate = false) path = File.join(path, SPEC_FILENAME) if File.directory?(path) raise Error.new("Missing #{File.basename(path)}") unless File.exists?(path) from_yaml(File.read(path), path, validate) end def self.from_yaml(input, filename = SPEC_FILENAME, validate = false) parser = YAML::PullParser.new(input) parser.read_stream do parser.read_document do new(parser, validate) end end rescue ex : YAML::ParseException raise ParseError.new(ex.message, input, filename, ex.line_number, ex.column_number) ensure parser.close if parser end def initialize(@name : String, @version : Version, @resolver : Resolver? = nil) @original_version = @version @read_from_yaml = false end getter! name : String? getter! version : Version? getter! original_version : Version? getter description : String? getter license : String? getter crystal : String? property resolver : Resolver? getter? read_from_yaml : Bool def mismatched_version? Versions.compare(version, original_version) != 0 end # :nodoc: def initialize(pull : YAML::PullParser, validate = false) pull.each_in_mapping do line, column = pull.location case key = pull.read_scalar when "name" check_duplicate(@name, "name", line, column) @name = pull.read_scalar when "version" check_duplicate(@version, "version", line, column) @original_version = @version = Version.new(pull.read_scalar) when "description" check_duplicate(@description, "description", line, column) @description = pull.read_scalar when "license" check_duplicate(@license, "license", line, column) @license = pull.read_scalar when "crystal" check_duplicate(@crystal, "crystal", line, column) @crystal = pull.read_scalar when "authors" check_duplicate(@authors, "authors", line, column) pull.read_empty_or do pull.each_in_sequence do authors << Author.new(pull.read_scalar) end end when "dependencies" check_duplicate(@dependencies, "dependencies", line, column) pull.read_empty_or do pull.each_in_mapping do dependencies << Dependency.from_yaml(pull) end end when "development_dependencies" check_duplicate(@development_dependencies, "development_dependencies", line, column) pull.read_empty_or do pull.each_in_mapping do development_dependencies << Dependency.from_yaml(pull) end end when "targets" check_duplicate(@targets, "targets", line, column) pull.read_empty_or do pull.each_in_mapping do targets << Target.new(pull) end end when "executables" check_duplicate(@executables, "executables", line, column) pull.read_empty_or do pull.each_in_sequence do executables << pull.read_scalar end end when "libraries" check_duplicate(@libraries, "libraries", line, column) pull.read_empty_or do pull.each_in_mapping do libraries << Library.new(pull) end end when "scripts" check_duplicate(@scripts, "scripts", line, column) pull.read_empty_or do pull.each_in_mapping do scripts[pull.read_scalar] = pull.read_scalar end end else if validate pull.raise "unknown attribute: #{key}", line, column else pull.skip end end end {% for attr in %w(name version) %} unless @{{ attr.id }} pull.raise "missing required attribute: {{ attr.id }}" end {% end %} @read_from_yaml = true end private def check_duplicate(argument, name, line, column) unless argument.nil? raise YAML::ParseException.new("duplicate attribute #{name.inspect}", line, column) end end def name=(@name : String) end def version=(@version : Version) end def authors @authors ||= [] of Author end def dependencies @dependencies ||= [] of Dependency end def development_dependencies @development_dependencies ||= [] of Dependency end def targets @targets ||= [] of Target end def executables @executables ||= [] of String end def libraries @libraries ||= [] of Library end def scripts @scripts ||= {} of String => String end def license_url if license = @license if license =~ %r(https?://) license else "https://spdx.org/licenses/#{license}" end end end end end shards-0.19.0/src/target.cr000066400000000000000000000012251473060476400155030ustar00rootroot00000000000000module Shards class Target property name : String property main : String def self.new(pull : YAML::PullParser) : self start_pos = pull.location name = pull.read_scalar main = nil pull.each_in_mapping do case key = pull.read_scalar when "main" main = pull.read_scalar else # ignore unknown dependency mapping for future extensions end end unless main raise YAML::ParseException.new(%(Missing property "main" for target #{name.inspect}), *start_pos) end Target.new(name, main) end def initialize(@name, @main) end end end shards-0.19.0/src/templates/000077500000000000000000000000001473060476400156655ustar00rootroot00000000000000shards-0.19.0/src/templates/shard.yml.ecr000066400000000000000000000004701473060476400202620ustar00rootroot00000000000000name: <%= name %> version: <%= version %> # authors: # - name # description: | # Short description of <%= name %> # dependencies: # pg: # github: will/crystal-pg # version: "~> 0.5" # development_dependencies: # webmock: # github: manastech/webmock.cr # license: MIT shards-0.19.0/src/version.cr000066400000000000000000000007301473060476400157020ustar00rootroot00000000000000module Shards VERSION = {{ read_file("#{__DIR__}/../VERSION").chomp }} BUILD_SHA1 = {{ env("SHARDS_CONFIG_BUILD_COMMIT") || "" }} {% if (t = env("SOURCE_DATE_EPOCH")) && !t.empty? %} BUILD_DATE = Time.unix({{t.to_i}}).to_s("%Y-%m-%d") {% else %} BUILD_DATE = "" {% end %} def self.version_string if BUILD_SHA1.empty? "Shards #{VERSION} (#{BUILD_DATE})" else "Shards #{VERSION} [#{BUILD_SHA1}] (#{BUILD_DATE})" end end end shards-0.19.0/src/versions.cr000066400000000000000000000133601473060476400160700ustar00rootroot00000000000000module Shards module Versions # :nodoc: struct Segment NON_ALPHANUMERIC = /[^a-zA-Z0-9]/ NATURAL_SORT_EXTRACT_NEXT_CHARS_AND_DIGITS = /^(\D*)(\d*)(.*)$/ protected getter! segment : String def initialize(@str : String) if index = @str.index('+') @str = @str[0...index] end end def next @segment, _, @str = @str.partition(NON_ALPHANUMERIC) segment end def empty? segment.empty? end def to_i? segment.to_i?(whitespace: false) end def <=>(b : self) natural_sort(segment, b.segment) end # Original natural sorting algorithm from: # https://github.com/sourcefrog/natsort/blob/master/natcmp.rb # Copyright (C) 2003 by Alan Davies . private def natural_sort(a, b) if (a_num = a.to_i?(whitespace: false)) && (b_num = b.to_i?(whitespace: false)) return a_num <=> b_num end loop do return 0 if a.empty? && b.empty? a =~ NATURAL_SORT_EXTRACT_NEXT_CHARS_AND_DIGITS a_chars, a_digits, a = $1, $2, $3 b =~ NATURAL_SORT_EXTRACT_NEXT_CHARS_AND_DIGITS b_chars, b_digits, b = $1, $2, $3 ret = a_chars <=> b_chars return ret unless ret == 0 a_num = a_digits.to_i?(whitespace: false) b_num = b_digits.to_i?(whitespace: false) if a_num && b_num ret = a_num.to_i <=> b_num.to_i return ret unless ret == 0 else ret = a_digits <=> b_digits return ret unless ret == 0 end end end def only_zeroes?(&) return if empty? yield unless to_i? == 0 loop do self.next return if empty? yield unless to_i? == 0 end end def prerelease? segment.each_char.any?(&.ascii_letter?) end def inspect(io) @segment.inspect(io) end end def self.sort(versions) versions.sort { |a, b| compare(a, b) } end def self.compare(a : Version, b : Version) compare(a.value, b.value) end def self.compare(a : String, b : String) if a == b return 0 end a_segment = Segment.new(a) b_segment = Segment.new(b) loop do # extract next segment from version number ("1.0.2" => "1" then "0" then "2"): a_segment.next b_segment.next # accept unbalanced version numbers ("1.0" == "1.0.0.0", "1.0" < "1.0.1") if a_segment.empty? b_segment.only_zeroes? { return b_segment.prerelease? ? -1 : 1 } return 0 end # accept unbalanced version numbers ("1.0.0.0" == "1.0", "1.0.1" > "1.0") if b_segment.empty? a_segment.only_zeroes? { return a_segment.prerelease? ? 1 : -1 } return 0 end # try to convert segments to numbers: a_num = a_segment.to_i? b_num = b_segment.to_i? ret = if a_num && b_num # compare numbers (for natural 1, 2, ..., 10, 11 ordering): b_num <=> a_num elsif a_num # b is preliminary version: a_segment.only_zeroes? do return b_segment <=> a_segment if a_segment.prerelease? return -1 end return -1 elsif b_num # a is preliminary version: b_segment.only_zeroes? do return b_segment <=> a_segment if b_segment.prerelease? return 1 end return 1 else # compare strings: b_segment <=> a_segment end # if different return the result (older or newer), otherwise continue # to the next segment: return ret unless ret == 0 end end def self.prerelease?(str : String) str.each_char do |char| return true if char.ascii_letter? break if char == '+' end false end def self.has_metadata?(str : String) str.includes? '+' end protected def self.without_prereleases(versions : Array(Version)) versions.reject { |v| prerelease?(v.value) } end def self.resolve(versions : Array(Version), requirement : VersionReq) versions.select { |version| matches?(version, requirement) } end def self.matches?(version : Version, requirement : VersionReq) requirement.patterns.all? do |pattern| matches_single_pattern?(version, pattern) end end private def self.matches_single_pattern?(version : Version, pattern : String) case pattern when "*", "" true when /~>\s*([^\s]+)\d*/ ver = if idx = $1.rindex('.') $1[0...idx] else $1 end matches_approximate?(version.value, $1, ver) when /\s*(~>|>=|<=|!=|>|<|=)\s*([^~<>=!\s]+)\s*/ matches_operator?(version.value, $1, $2) else matches_operator?(version.value, "=", pattern) end end private def self.matches_approximate?(version, requirement, ver) version.starts_with?(ver) && !version[ver.size]?.try(&.ascii_alphanumeric?) && (compare(version, requirement) <= 0) end private def self.matches_operator?(version, operator, requirement) case operator when ">=" compare(version, requirement) <= 0 when "<=" compare(version, requirement) >= 0 when ">" compare(version, requirement) < 0 when "<" compare(version, requirement) > 0 when "!=" compare(version, requirement) != 0 else compare(version, requirement) == 0 end end end end