diff --git a/.github/workflows/alpine.yml b/.github/workflows/alpine.yml index 288c9cb..2693a24 100644 --- a/.github/workflows/alpine.yml +++ b/.github/workflows/alpine.yml @@ -33,7 +33,7 @@ steps: # Checkout the repository - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build run: | diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0fac3c3..41a7a5a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,6 +23,28 @@ - github-actions jobs: + single_checks: + name: "Single sanity check" + runs-on: ubuntu-latest + steps: + - name: Install latest DMD + uses: dlang-community/setup-dlang@v1 + - name: Checkout + uses: actions/checkout@v4 + - name: Run tests + run: | + # check for trailing whitespace + TRAILING_WS_COUNT=$(find . -type f -name '*.d' -exec grep -Hn "[[:blank:]]$" {} \; | wc -l) + if [ $TRAILING_WS_COUNT -ne 0 ]; then + echo "========================================" + find . -type f -name '*.d' -exec grep -Hn "[[:blank:]]$" {} \; + echo "========================================" + echo "The files above have trailing whitespace" + exit 1 + fi + # check that the man page generation still works + dub --single -v scripts/man/gen_man.d + main: name: Run strategy: @@ -32,21 +54,23 @@ # Latest stable version, update at will os: [ macOS-11, ubuntu-20.04, windows-2019 ] dc: + # Always test latest as that is what we use to compile on release - dmd-latest - - dmd-2.100.2 - ldc-latest + # Provide some testing for upstream - dmd-master -# - ldc-master - # This is the bootstrap compiler used to compile the releases - - ldc-1.23.0 - # Some intermediate compilers for good measure - - dmd-2.095.1 + - ldc-master + # Test some intermediate versions + - ldc-1.26.0 - dmd-2.098.1 + - dmd-2.101.1 + - dmd-2.104.2 include: - - { do_test: true } - - { dc: dmd-2.095.1, do_test: false } - - { dc: dmd-2.098.1, do_test: false } - - { dc: ldc-1.23.0 , do_test: false } + - { do_test: false } + - { dc: dmd-latest, do_test: true } + - { dc: ldc-latest, do_test: true } + - { dc: dmd-master, do_test: true } + - { dc: ldc-master, do_test: true } runs-on: ${{ matrix.os }} steps: @@ -71,19 +95,17 @@ # Checkout the repository - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: '[POSIX] Test' if: runner.os != 'Windows' env: - COVERAGE: false - # The value doesn't matter as long as it's > 2.087 - FRONTEND: 2.095.0 + COVERAGE: true run: | dub build --compiler=${{ env.DC }} if [[ ${{ matrix.do_test }} == 'true' ]]; then dub run --compiler=${{ env.DC }} --single test/issue2051_running_unittests_from_dub_single_file_packages_fails.d - ./scripts/ci/travis.sh + ./scripts/ci/ci.sh fi - name: '[Windows] Test' @@ -109,3 +131,6 @@ test/run-unittest.sh fi shell: bash + + - name: Codecov + uses: codecov/codecov-action@v4 diff --git a/.github/workflows/pr_info_untrusted.yml b/.github/workflows/pr_info_untrusted.yml index c3416e5..56b8214 100644 --- a/.github/workflows/pr_info_untrusted.yml +++ b/.github/workflows/pr_info_untrusted.yml @@ -29,7 +29,7 @@ compiler: ldc-latest - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..f94737c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,113 @@ +# When a release is published, build the assets and upload them +name: Build release assets + +on: + release: + types: + - published + +jobs: + # First we define a job with a matrix that will build all relevant assets, + # and collect them in a temporary storage using `actions/upload-artifacts` + build: + name: 'Build artifacts for ${{ github.event.release.tag_name }}' + strategy: + fail-fast: false + matrix: + os: [ macOS-11, ubuntu-20.04, windows-2019 ] + arch: [ x86_64 ] + include: + - { os: windows-2019, arch: i686 } + + runs-on: ${{ matrix.os }} + steps: + ## Dependencies + - name: '[OSX] Install dependencies' + if: runner.os == 'macOS' + run: | + brew install pkg-config coreutils + echo "PKG_CONFIG_PATH=/usr/local/opt/openssl@1.1/lib/pkgconfig/" >> $GITHUB_ENV + - name: '[Linux] Install dependencies' + if: runner.os == 'Linux' + run: | + sudo apt-get update && sudo apt-get install -y libcurl4-openssl-dev + + ## Boileterplate (compiler/repo) + - name: Install compiler + uses: dlang-community/setup-dlang@v1 + with: + compiler: ldc-latest + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.tag_name }} + + ## Actually build the releases + - name: '[POSIX] Build release' + if: runner.os == 'Linux' || runner.os == 'macOS' + env: + GITVER: ${{ github.event.release.tag_name }} + DMD: "ldmd2" + ARCH_TRIPLE: ${{ matrix.arch }}-${{ runner.os == 'linux' && 'pc-linux' || 'apple-darwin' }} + run: | + ldc2 -run ./build.d -release -mtriple=${ARCH_TRIPLE} + pushd bin + if [ ${{ runner.os }} == 'Linux' ]; then + tar -c -f 'dub-${{ github.event.release.tag_name }}-linux-${{ matrix.arch }}.tar.gz' -v -z --owner=0 --group=0 dub + else + gtar -c -f 'dub-${{ github.event.release.tag_name }}-osx-${{ matrix.arch }}.tar.gz' -v -z --owner=0 --group=0 dub + fi + popd + - name: '[Windows] Build release' + if: runner.os == 'Windows' + env: + GITVER: ${{ github.event.release.tag_name }} + DMD: "ldmd2" + run: | + ldc2 -run ./build.d -release -mtriple=${{ matrix.arch }}-pc-windows-msvc + pushd bin + 7z a dub-${{ github.event.release.tag_name }}-windows-${{ matrix.arch }}.zip dub.exe + popd + + - name: 'Upload temporary binaries' + uses: actions/upload-artifact@v4 + with: + name: dub-release-${{ matrix.os }}-${{ matrix.arch }} + path: | + bin/dub-${{ github.event.release.tag_name }}-* + if-no-files-found: error + retention-days: 1 + + # Uploads collected builds to the release + release: + name: "Update release artifacts" + runs-on: ubuntu-latest + needs: + - build + + steps: + - name: Download artifacts to release + uses: actions/download-artifact@v4 + with: + path: ~/artifacts/ + + - name: List all artifacts included in the release + id: list-artifacts + shell: bash + run: | + set -euox pipefail + ls -aulR ~/artifacts + echo "artifacts_directory=$HOME/artifacts" >> $GITHUB_OUTPUT + + - name: Update release artifacts + uses: ncipollo/release-action@v1 + with: + token: "${{ secrets.GITHUB_TOKEN }}" + tag: ${{ github.event.release.tag_name }} + artifacts: ${{ steps.list-artifacts.outputs.artifacts_directory }}/*/* + # Keep the existing state of the release + allowUpdates: true + artifactErrorsFailBuild: true + omitNameDuringUpdate: true + omitBodyDuringUpdate: true + omitPrereleaseDuringUpdate: true diff --git a/.gitignore b/.gitignore index 47dcf7d..e815926 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ /bin/dub-test-library /bin/libdub.a /bin/dub-* +/bin/dub.* # Ignore files or directories created by the test suite. *.exe diff --git a/build-files.txt b/build-files.txt index 182d017..3a9ec14 100644 --- a/build-files.txt +++ b/build-files.txt @@ -6,7 +6,6 @@ source/dub/compilers/gdc.d source/dub/compilers/ldc.d source/dub/compilers/utils.d -source/dub/data/platform.d source/dub/data/settings.d source/dub/dependency.d source/dub/dependencyresolver.d diff --git a/changelog/dub-fetch.dd b/changelog/dub-fetch.dd new file mode 100644 index 0000000..a4d5556 --- /dev/null +++ b/changelog/dub-fetch.dd @@ -0,0 +1,21 @@ +The fetch command now supports multiple arguments, recursive fetch, and is project-aware + +Previously, `dub fetch` could only fetch a single package, +and was working independently of the working directory. + +With this release, support for multiple packages have +been added, such that the following is now possible: +--- +$ dub fetch vibe-d@0.9.0 vibe-d@0.9.1 vibe-d@0.9.2 +--- + +When called with no argument, `dub fetch` used to error out. +However, it will now attempt to fetch dependencies for the +current project, if any exists. + +Finally, when fetching a package, it might be useful to fetch +all its dependencies. This is done automatically for projects, +and can now be done for direct fetch as well: +--- +$ dub fetch --recursive vibe-d@0.9.0 vibe-d@0.9.1 +--- diff --git a/dub.sdl b/dub.sdl index 2ae4255..642a204 100644 --- a/dub.sdl +++ b/dub.sdl @@ -7,6 +7,13 @@ targetPath "bin" +dflags "-preview=in" platform="dmd" +dflags "-preview=in" platform="ldc" +//Disabled due to ICEs in gdc. +//dflags "-fpreview=in" platform="gdc" +// Deprecated module(s) +excludedSourceFiles "source/dub/packagesupplier.d" + configuration "application" { targetType "executable" mainSourceFile "source/app.d" diff --git a/dub.selections.json b/dub.selections.json index c675e35..3a78158 100644 --- a/dub.selections.json +++ b/dub.selections.json @@ -4,16 +4,18 @@ "botan": "1.12.19", "botan-math": "1.0.3", "diet-ng": "1.8.1", - "eventcore": "0.9.23", + "eventcore": "0.9.27", "libasync": "0.8.6", "libev": "5.0.0+4.04", "libevent": "2.0.2+2.0.16", - "memutils": "1.0.9", + "memutils": "1.0.10", "mir-linux-kernel": "1.0.1", - "openssl": "3.3.0", + "openssl": "3.3.3", + "openssl-static": "1.0.2+3.0.8", "stdx-allocator": "2.77.5", "taggedalgebraic": "0.11.22", - "vibe-core": "1.23.0", - "vibe-d": "0.9.5" + "vibe-container": "1.0.1", + "vibe-core": "2.7.1", + "vibe-d": "0.9.7" } } diff --git a/scripts/ci/ci.sh b/scripts/ci/ci.sh new file mode 100755 index 0000000..494302b --- /dev/null +++ b/scripts/ci/ci.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +set -v -e -o pipefail + +vibe_ver=$(jq -r '.versions | .["vibe-d"]' < dub.selections.json) +dub fetch vibe-d@$vibe_ver # get optional dependency +dub test --compiler=${DC} -c library-nonet + +export DMD="$(command -v $DMD)" + +./build.d -preview=dip1000 -preview=in -w -g -debug + +if [ "$COVERAGE" = true ]; then + # library-nonet fails to build with coverage (Issue 13742) + dub test --compiler=${DC} -b unittest-cov + ./build.d -cov +else + dub test --compiler=${DC} -b unittest-cov + ./build.d +fi +DUB=`pwd`/bin/dub DC=${DC} dub --single ./test/run-unittest.d +DUB=`pwd`/bin/dub DC=${DC} test/run-unittest.sh diff --git a/scripts/ci/release-windows.sh b/scripts/ci/release-windows.sh deleted file mode 100755 index 5b87df2..0000000 --- a/scripts/ci/release-windows.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash -# Build the Windows binaries under Linux -set -eux -o pipefail - -BIN_NAME=dub - -# Allow the script to be run from anywhere -DIR="$( cd "$(dirname "${BASH_SOURCE[0]}")/../../" && pwd )" -cd $DIR - -# Setup cross compiler -source scripts/ci/setup-ldc-windows.sh - -# Run LDC with cross-compilation -archiveName="$BIN_NAME-$VERSION-$OS-$ARCH_SUFFIX.zip" -echo "Building $archiveName" -mkdir -p bin -DMD=ldmd2 ldc2 -run ./build.d -release ${LDC_XDFLAGS} - -cd bin -zip "$archiveName" "${BIN_NAME}.exe" diff --git a/scripts/ci/release.sh b/scripts/ci/release.sh deleted file mode 100755 index bdc45da..0000000 --- a/scripts/ci/release.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env bash - -set -eux -o pipefail - -# Get the directory root, which is two level ahead -ROOT_DIR="$( cd "$(dirname "${BASH_SOURCE[0]}")/../../" && pwd )" -cd ${ROOT_DIR} - -VERSION=$(git describe --abbrev=0 --tags) -ARCH="${ARCH:-64}" -CUSTOM_FLAGS=() -unameOut="$(uname -s)" -case "$unameOut" in - Linux*) - OS=linux - CUSTOM_FLAGS+=("-L--export-dynamic") - ;; - Darwin*) - OS=osx - CUSTOM_FLAGS+=("-L-dead_strip") - ;; - *) echo "Unknown OS: $unameOut"; exit 1 -esac - -if [[ $(basename "$DMD") =~ ldmd.* ]] ; then - CUSTOM_FLAGS+=("-flto=full") - # ld.gold is required on Linux - if [ ${OS:-} == "linux" ] ; then - CUSTOM_FLAGS+=("-linker=gold") - fi -fi - -case "$ARCH" in - 64) ARCH_SUFFIX="x86_64";; - 32) ARCH_SUFFIX="x86";; - *) echo "Unknown ARCH: $ARCH"; exit 1 -esac - -archiveName="dub-$VERSION-$OS-$ARCH_SUFFIX.tar.gz" - -echo "Building $archiveName" -DMD="$(command -v $DMD)" ./build.d -release -m$ARCH ${CUSTOM_FLAGS[@]} -if [[ "$OSTYPE" == darwin* ]]; then - TAR=gtar -else - TAR=tar -fi - -"$TAR" cvfz "bin/$archiveName" --owner=0 --group=0 -C bin dub diff --git a/scripts/ci/setup-ldc-windows.sh b/scripts/ci/setup-ldc-windows.sh deleted file mode 100644 index e95881e..0000000 --- a/scripts/ci/setup-ldc-windows.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env bash - -# sets up LDC for cross-compilation. Source this script, s.t. the new LDC is in PATH - -# Make sure this version matches the version of LDC2 used in .travis.yml, -# otherwise the compiler and the lib used might mismatch. -LDC_VERSION="1.22.0" -ARCH=${ARCH:-32} -VERSION=$(git describe --abbrev=0 --tags) -OS=windows - -# LDC should already be installed (see .travis.yml) -# However, we need the libraries, so download them -# We can't use the downloaded ldc2 itself, because obviously it's for Windows - -if [ "${ARCH}" == 64 ]; then - ARCH_SUFFIX='x86_64' - ZIP_ARCH_SUFFIX='x64' -else - ARCH_SUFFIX='i686' - ZIP_ARCH_SUFFIX='x86' -fi - -LDC_DIR_PATH="$(pwd)/ldc2-${LDC_VERSION}-windows-${ZIP_ARCH_SUFFIX}" -LDC_XDFLAGS="-conf=${LDC_DIR_PATH}/etc/ldc2.conf -mtriple=${ARCH_SUFFIX}-pc-windows-msvc" - -# Step 1: download the LDC Windows release -# Check if the user already have it (e.g. building locally) -if [ ! -d ${LDC_DIR_PATH} ]; then - if [ ! -d "ldc2-${LDC_VERSION}-windows-${ZIP_ARCH_SUFFIX}.7z" ]; then - wget "https://github.com/ldc-developers/ldc/releases/download/v${LDC_VERSION}/ldc2-${LDC_VERSION}-windows-${ZIP_ARCH_SUFFIX}.7z" - fi - 7z x "ldc2-${LDC_VERSION}-windows-${ZIP_ARCH_SUFFIX}.7z" > /dev/null -fi - -# Step 2: Generate a config file with the proper path -cat > ${LDC_DIR_PATH}/etc/ldc2.conf <<EOF -default: -{ - switches = [ - "-defaultlib=phobos2-ldc,druntime-ldc", - "-link-defaultlib-shared=false", - ]; - post-switches = [ - "-I${LDC_DIR_PATH}/import", - ]; - lib-dirs = [ - "${LDC_DIR_PATH}/lib/", - "${LDC_DIR_PATH}/lib/mingw/", - ]; -}; -EOF diff --git a/scripts/ci/travis.sh b/scripts/ci/travis.sh deleted file mode 100755 index a811e75..0000000 --- a/scripts/ci/travis.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash - -set -v -e -o pipefail - -vibe_ver=$(jq -r '.versions | .["vibe-d"]' < dub.selections.json) -dub fetch vibe-d@$vibe_ver # get optional dependency -dub test --compiler=${DC} -c library-nonet - -export DMD="$(command -v $DMD)" - -if [ "$FRONTEND" \> 2.087.z ]; then - ./build.d -preview=dip1000 -preview=in -w -g -debug -fi - -function clean() { - # Hard reset of the DUB local folder is necessary as some tests - # currently don't properly clean themselves - rm -rf ~/.dub - git clean -dxf -- test -} - -if [ "$COVERAGE" = true ]; then - # library-nonet fails to build with coverage (Issue 13742) - dub test --compiler=${DC} -b unittest-cov - ./build.d -cov - - wget https://codecov.io/bash -O codecov.sh - bash codecov.sh -else - ./build.d - DUB=`pwd`/bin/dub DC=${DC} dub --single ./test/run-unittest.d - DUB=`pwd`/bin/dub DC=${DC} test/run-unittest.sh -fi - -## Checks that only need to be done once per CI run -## Here the `COVERAGE` variable is abused for this purpose, -## as it's only defined once in the whole Travis matrix -if [ "$COVERAGE" = true ]; then - # run tests with different compilers - DUB=`pwd`/bin/dub DC=${DC} test/run-unittest.sh - clean - - export FRONTEND=2.077 - source $(~/dlang/install.sh ldc-1.7.0 --activate) - DUB=`pwd`/bin/dub DC=${DC} test/run-unittest.sh - deactivate - clean - - export FRONTEND=2.068 - source $(~/dlang/install.sh gdc-4.8.5 --activate) - DUB=`pwd`/bin/dub DC=${DC} test/run-unittest.sh - deactivate - - # check for trailing whitespace - find . -type f -name '*.d' -exec grep -Hn "[[:blank:]]$" {} \; - # check that the man page generation still works - source $(~/dlang/install.sh dmd --activate) - source $(~/dlang/install.sh dub --activate) - dub --single -v scripts/man/gen_man.d -fi diff --git a/scripts/man/gen_man.d b/scripts/man/gen_man.d index 3d4a127..eb95930 100755 --- a/scripts/man/gen_man.d +++ b/scripts/man/gen_man.d @@ -404,9 +404,9 @@ manFile.mode == ManWriter.Mode.markdown ? "\n\n" : "\n" )); } - } - - + } + + writeln(manFile.header("COMMON OPTIONS")); manFile.writeArgs("-", args); diff --git a/source/dub/commandline.d b/source/dub/commandline.d index 587095e..92092ad 100644 --- a/source/dub/commandline.d +++ b/source/dub/commandline.d @@ -82,44 +82,58 @@ args = a list of string arguments that will be processed Returns: - A structure with two members. `value` is the command name - `remaining` is a list of unprocessed arguments + The command name that was found (may be null). */ -auto extractCommandNameArgument(string[] args) +string commandNameArgument(ref string[] args) { - struct Result { - string value; - string[] remaining; - } - if (args.length >= 1 && !args[0].startsWith("-") && !args[0].canFind(":")) { - return Result(args[0], args[1 .. $]); + const result = args[0]; + args = args[1 .. $]; + return result; } - - return Result(null, args); + return null; } /// test extractCommandNameArgument usage unittest { - /// It returns an empty string on when there are no args - assert(extractCommandNameArgument([]).value == ""); - assert(extractCommandNameArgument([]).remaining == []); + { + string[] args; + /// It returns an empty string on when there are no args + assert(commandNameArgument(args) is null); + assert(!args.length); + } - /// It returns the first argument when it does not start with `-` - assert(extractCommandNameArgument(["test"]).value == "test"); + { + string[] args = [ "test" ]; + /// It returns the first argument when it does not start with `-` + assert(commandNameArgument(args) == "test"); + /// There is nothing to extract when the arguments only contain the `test` cmd + assert(!args.length); + } - /// There is nothing to extract when the arguments only contain the `test` cmd - assert(extractCommandNameArgument(["test"]).remaining == []); + { + string[] args = [ "-a", "-b" ]; + /// It extracts two arguments when they are not a command + assert(commandNameArgument(args) is null); + assert(args == ["-a", "-b"]); + } - /// It extracts two arguments when they are not a command - assert(extractCommandNameArgument(["-a", "-b"]).remaining == ["-a", "-b"]); + { + string[] args = [ "-test" ]; + /// It returns the an empty string when it starts with `-` + assert(commandNameArgument(args) is null); + assert(args.length == 1); + } - /// It returns the an empty string when it starts with `-` - assert(extractCommandNameArgument(["-test"]).value == ""); - - // Sub package names are ignored as command names - assert(extractCommandNameArgument(["foo:bar"]).value == ""); - assert(extractCommandNameArgument([":foo"]).value == ""); + { + string[] args = [ "foo:bar" ]; + // Sub package names are ignored as command names + assert(commandNameArgument(args) is null); + assert(args.length == 1); + args[0] = ":foo"; + assert(commandNameArgument(args) is null); + assert(args.length == 1); + } } /** Handles the Command Line options and commands. @@ -139,13 +153,13 @@ */ string[] commandNames() { - return commandGroups.map!(g => g.commands.map!(c => c.name).array).join; + return commandGroups.map!(g => g.commands).joiner.map!(c => c.name).array; } /** Parses the general options and sets up the log level and the root_path */ - void prepareOptions(CommandArgs args) { + string[] prepareOptions(CommandArgs args) { LogLevel loglevel = LogLevel.info; options.prepare(args); @@ -186,6 +200,7 @@ setLoggingColorsEnabled(false); // disable colors, no matter what break; } + return args.extractAllRemainingArgs(); } /** Get an instance of the requested command. @@ -405,7 +420,6 @@ } auto handler = CommandLineHandler(getCommands()); - auto commandNames = handler.commandNames(); // Special syntaxes need to be handled before regular argument parsing if (args.length >= 2) @@ -435,7 +449,7 @@ // We have to assume it isn't, and to reduce the risk of false positive // we only consider the case where the file name is the first argument, // as the shell invocation cannot be controlled. - else if (!commandNames.canFind(args[1]) && !args[1].startsWith("-")) { + else if (handler.getCommand(args[1]) is null && !args[1].startsWith("-")) { if (exists(args[1])) { auto path = getTempFile("app", ".d"); copy(args[1], path.toNativeString()); @@ -448,7 +462,8 @@ auto common_args = new CommandArgs(args[1..$]); - try handler.prepareOptions(common_args); + try + args = handler.prepareOptions(common_args); catch (Exception e) { logError("Error processing arguments: %s", e.msg); logDiagnostic("Full exception: %s", e.toString().sanitize); @@ -462,16 +477,12 @@ return 0; } - // extract the command - args = common_args.extractAllRemainingArgs(); - - auto command_name_argument = extractCommandNameArgument(args); - - auto command_args = new CommandArgs(command_name_argument.remaining); + const command_name = commandNameArgument(args); + auto command_args = new CommandArgs(args); Command cmd; try { - cmd = handler.prepareCommand(command_name_argument.value, command_args); + cmd = handler.prepareCommand(command_name, command_args); } catch (Exception e) { logError("Error processing arguments: %s", e.msg); logDiagnostic("Full exception: %s", e.toString().sanitize); @@ -482,14 +493,14 @@ if (cmd is null) { logInfoNoTag("USAGE: dub [--version] [<command>] [<options...>] [-- [<application arguments...>]]"); logInfoNoTag(""); - logError("Unknown command: %s", command_name_argument.value); + logError("Unknown command: %s", command_name); import std.algorithm.iteration : filter; import std.uni : toUpper; foreach (CommandGroup key; handler.commandGroups) { foreach (Command command; key.commands) { - if (levenshteinDistance(command_name_argument.value, command.name) < 4) { + if (levenshteinDistance(command_name, command.name) < 4) { logInfo("Did you mean '%s'?", command.name); } } @@ -885,7 +896,7 @@ private bool loadCwdPackage(Dub dub, bool warn_missing_package) { - auto filePath = Package.findPackageFile(dub.rootPath); + auto filePath = dub.packageManager.findPackageFile(dub.rootPath); if (filePath.empty) { if (warn_missing_package) { @@ -1005,14 +1016,15 @@ static string input(string caption, string default_value) { - writef("%s [%s]: ", caption.color(Mode.bold), default_value); - stdout.flush(); + import dub.internal.colorize; + cwritef("%s [%s]: ", caption.color(Mode.bold), default_value); auto inp = readln(); return inp.length > 1 ? inp[0 .. $-1] : default_value; } static string select(string caption, bool free_choice, string default_value, const string[] options...) { + import dub.internal.colorize.cwrite; assert(options.length); import std.math : floor, log10; auto ndigits = (size_t val) => log10(cast(double) val).floor.to!uint + 1; @@ -1035,17 +1047,17 @@ auto user_to_idx = (size_t i) => cast(uint)i - 1; assert(default_idx >= 0); - writeln((free_choice ? "Select or enter " : "Select ").color(Mode.bold), caption.color(Mode.bold), ":".color(Mode.bold)); + cwriteln((free_choice ? "Select or enter " : "Select ").color(Mode.bold), caption.color(Mode.bold), ":".color(Mode.bold)); foreach (i, option; options_matrix) { - if (i != 0 && (i % num_columns) == 0) writeln(); + if (i != 0 && (i % num_columns) == 0) cwriteln(); if (!option.length) continue; auto user_id = idx_to_user(option); - writef("%*u)".color(Color.cyan, Mode.bold) ~ " %s", ndigits(options.length), user_id, + cwritef("%*u)".color(Color.cyan, Mode.bold) ~ " %s", ndigits(options.length), user_id, leftJustifier(option, max_width)); } - writeln(); + cwriteln(); immutable default_choice = (default_idx + 1).to!string; while (true) { @@ -1294,11 +1306,11 @@ protected void setupVersionPackage(Dub dub, string str_package_info, string default_build_type = "debug") { - PackageAndVersion package_info = splitPackageName(str_package_info); - setupPackage(dub, package_info.name, default_build_type, package_info.version_); + UserPackageDesc udesc = UserPackageDesc.fromString(str_package_info); + setupPackage(dub, udesc, default_build_type); } - protected void setupPackage(Dub dub, string package_name, string default_build_type = "debug", string ver = "") + protected void setupPackage(Dub dub, UserPackageDesc udesc, string default_build_type = "debug") { if (!m_compilerName.length) m_compilerName = dub.defaultCompiler; if (!m_arch.length) m_arch = dub.defaultArchitecture; @@ -1318,7 +1330,7 @@ this.baseSettings.buildSettings.addVersions(m_dVersions); m_defaultConfig = null; - enforce (loadSpecificPackage(dub, package_name, ver), "Failed to load package."); + enforce(loadSpecificPackage(dub, udesc), "Failed to load package."); if (this.baseSettings.config.length != 0 && !dub.configurations.canFind(this.baseSettings.config) && @@ -1355,35 +1367,34 @@ } } - private bool loadSpecificPackage(Dub dub, string package_name, string ver) + private bool loadSpecificPackage(Dub dub, UserPackageDesc udesc) { if (this.baseSettings.single) { - enforce(package_name.length, "Missing file name of single-file package."); - dub.loadSingleFilePackage(package_name); + enforce(udesc.name.length, "Missing file name of single-file package."); + dub.loadSingleFilePackage(udesc.name); return true; } - bool from_cwd = package_name.length == 0 || package_name.startsWith(":"); + bool from_cwd = udesc.name.length == 0 || udesc.name.startsWith(":"); // load package in root_path to enable searching for sub packages if (loadCwdPackage(dub, from_cwd)) { - if (package_name.startsWith(":")) + if (udesc.name.startsWith(":")) { - auto pack = dub.packageManager.getSubPackage(dub.project.rootPackage, package_name[1 .. $], false); + auto pack = dub.packageManager.getSubPackage( + dub.project.rootPackage, udesc.name[1 .. $], false); dub.loadPackage(pack); return true; } if (from_cwd) return true; } - enforce(package_name.length, "No valid root package found - aborting."); + enforce(udesc.name.length, "No valid root package found - aborting."); - const vers = ver.length ? VersionRange.fromString(ver) : VersionRange.Any; - auto pack = dub.packageManager.getBestPackage(package_name, vers); + auto pack = dub.packageManager.getBestPackage( + PackageName(udesc.name), udesc.range); - enforce(pack, format!"Failed to find a package named '%s%s' locally."(package_name, - ver == "" ? "" : ("@" ~ ver) - )); + enforce(pack, format!"Failed to find package '%s' locally."(udesc)); logInfo("Building package %s in %s", pack.name, pack.path.toNativeString()); dub.loadPackage(pack); return true; @@ -1530,16 +1541,15 @@ if (!m_nonInteractive) { - const packageParts = splitPackageName(free_args[0]); + const packageParts = UserPackageDesc.fromString(free_args[0]); if (auto rc = fetchMissingPackages(dub, packageParts)) return rc; } return super.execute(dub, free_args, app_args); } - private int fetchMissingPackages(Dub dub, in PackageAndVersion packageParts) + private int fetchMissingPackages(Dub dub, in UserPackageDesc packageParts) { - static bool input(string caption, bool default_value = true) { writef("%s [%s]: ", caption, default_value ? "Y/n" : "y/N"); auto inp = readln(); @@ -1556,42 +1566,39 @@ } } - VersionRange dep; - - if (packageParts.version_.length > 0) { - // the user provided a version manually - dep = VersionRange.fromString(packageParts.version_); - } else if (packageParts.name.startsWith(":")) { - // Sub-packages are always assumed to be present + // Local subpackages are always assumed to be present + if (packageParts.name.startsWith(":")) return 0; - } else if (dub.packageManager.getBestPackage(packageParts.name)) { - // found locally + + const baseName = PackageName(packageParts.name).main; + // Found locally + if (dub.packageManager.getBestPackage(baseName, packageParts.range)) return 0; - } else { - // search for the package and filter versions for exact matches - auto basePackageName = getBasePackageName(packageParts.name); - auto search = dub.searchPackages(basePackageName) - .map!(tup => tup[1].find!(p => p.name == basePackageName)) - .filter!(ps => !ps.empty); - if (search.empty) { - logWarn("Package '%s' was neither found locally nor online.", packageParts.name); - return 2; - } - const p = search.front.front; - logInfo("Package '%s' was not found locally but is available online:", packageParts.name); - logInfo("---"); - logInfo("Description: %s", p.description); - logInfo("Version: %s", p.version_); - logInfo("---"); - - const answer = m_yes ? true : input("Do you want to fetch '%s' now?".format(packageParts.name)); - if (!answer) - return 0; - dep = VersionRange.fromString(p.version_); + // Non-interactive, either via flag, or because a version was provided + if (m_yes || !packageParts.range.matchesAny()) { + dub.fetch(baseName, packageParts.range); + return 0; + } + // Otherwise we go the long way of asking the user. + // search for the package and filter versions for exact matches + auto search = dub.searchPackages(baseName.toString()) + .map!(tup => tup[1].find!(p => p.name == baseName.toString())) + .filter!(ps => !ps.empty); + if (search.empty) { + logWarn("Package '%s' was neither found locally nor online.", packageParts); + return 2; } - dub.fetch(packageParts.name, dep, dub.defaultPlacementLocation, FetchOptions.none); + const p = search.front.front; + logInfo("Package '%s' was not found locally but is available online:", packageParts); + logInfo("---"); + logInfo("Description: %s", p.description); + logInfo("Version: %s", p.version_); + logInfo("---"); + + if (input("Do you want to fetch '%s@%s' now?".format(packageParts, p.version_))) + dub.fetch(baseName, VersionRange.fromString(p.version_)); return 0; } } @@ -2122,64 +2129,140 @@ } class FetchCommand : FetchRemoveCommand { + private enum FetchStatus + { + /// Package is already present and on the right version + Present = 0, + /// Package was fetched from the registry + Fetched = 1, + /// Attempts at fetching the package failed + Failed = 2, + } + + protected bool recursive; + protected size_t[FetchStatus.max + 1] result; + this() @safe pure nothrow { this.name = "fetch"; this.argumentsPattern = "<package>[@<version-spec>]"; - this.description = "Manually retrieves and caches a package"; + this.description = "Explicitly retrieves and caches packages"; this.helpText = [ - "Note: Use \"dub add <dependency>\" if you just want to use a certain package as a dependency, you don't have to explicitly fetch packages.", + "When run with one or more arguments, regardless of the location it is run in,", + "it will fetch the packages matching the argument(s).", + "Examples:", + "$ dub fetch vibe-d", + "$ dub fetch vibe-d@v0.9.0 --cache=local --recursive", "", - "Explicit retrieval/removal of packages is only needed when you want to put packages in a place where several applications can share them. If you just have a dependency to add, use the `add` command. Dub will do the rest for you.", + "When run in a project with no arguments, it will fetch all dependencies for that project.", + "If the project doesn't have set dependencies (no 'dub.selections.json'), it will also perform dependency resolution.", + "Example:", + "$ cd myProject && dub fetch", "", - "Without specified options, placement/removal will default to a user wide shared location.", - "", - "Complete applications can be retrieved and run easily by e.g.", - "$ dub fetch vibelog --cache=local", - "$ dub run vibelog --cache=local", - "", - "This will grab all needed dependencies and compile and run the application.", + "Note that the 'build', 'run', and any other command that need packages will automatically perform fetch,", + "hence it is not generally necessary to run this command before any other." ]; } override void prepare(scope CommandArgs args) { + args.getopt("r|recursive", &this.recursive, [ + "Also fetches dependencies of specified packages", + ]); super.prepare(args); } override int execute(Dub dub, string[] free_args, string[] app_args) { - enforceUsage(free_args.length == 1, "Expecting exactly one argument."); enforceUsage(app_args.length == 0, "Unexpected application arguments."); - auto location = dub.defaultPlacementLocation; - - auto name = free_args[0]; - - FetchOptions fetchOpts; - fetchOpts |= FetchOptions.forceBranchUpgrade; - if (m_version.length) { // remove then --version removed - enforceUsage(!name.canFindVersionSplitter, "Double version spec not allowed."); + // remove then --version removed + if (m_version.length) { + enforceUsage(free_args.length == 1, "Expecting exactly one argument when using --version."); + const name = free_args[0]; logWarn("The '--version' parameter was deprecated, use %s@%s. Please update your scripts.", name, m_version); - dub.fetch(name, VersionRange.fromString(m_version), location, fetchOpts); - } else if (name.canFindVersionSplitter) { - const parts = name.splitPackageName; - dub.fetch(parts.name, VersionRange.fromString(parts.version_), location, fetchOpts); - } else { - try { - dub.fetch(name, VersionRange.Any, location, fetchOpts); - logInfo("Finished", Color.green, "%s fetched", name.color(Mode.bold)); - logInfo( - "Please note that you need to use `dub run <pkgname>` " ~ - "or add it to dependencies of your package to actually use/run it. " - ); - } - catch(Exception e){ - logInfo("Getting a release version failed: %s", e.msg); - return 1; - } + enforceUsage(!name.canFindVersionSplitter, "Double version spec not allowed."); + this.fetchPackage(dub, UserPackageDesc(name, VersionRange.fromString(m_version))); + return this.result[FetchStatus.Failed] ? 1 : 0; } - return 0; + + // Fetches dependencies of the project + // This is obviously mutually exclusive with the below foreach + if (!free_args.length) { + if (!this.loadCwdPackage(dub, true)) + return 1; + // retrieve missing packages + if (!dub.project.hasAllDependencies) { + logInfo("Resolving", Color.green, "missing dependencies for project"); + dub.upgrade(UpgradeOptions.select); + } + else + logInfo("All %s dependencies are already present locally", + dub.project.dependencies.length); + return 0; + } + + // Fetches packages named explicitly + foreach (name; free_args) { + const udesc = UserPackageDesc.fromString(name); + this.fetchPackage(dub, udesc); + } + // Note that this does not include packages indirectly fetched. + // Hence it is not currently displayed in the no-argument version, + // and will only include directly mentioned packages in the arg version. + logInfoNoTag("%s packages fetched, %s already present, %s failed", + this.result[FetchStatus.Fetched], this.result[FetchStatus.Present], + this.result[FetchStatus.Failed]); + return this.result[FetchStatus.Failed] ? 1 : 0; + } + + /// Shell around `fetchSinglePackage` with logs and recursion support + private void fetchPackage(Dub dub, UserPackageDesc udesc) + { + auto r = this.fetchSinglePackage(dub, udesc); + this.result[r] += 1; + final switch (r) { + case FetchStatus.Failed: + // Error displayed in `fetchPackage` as it has more information + // However we need to return here as we can't recurse. + return; + case FetchStatus.Present: + logInfo("Existing", Color.green, "package %s found locally", udesc); + break; + case FetchStatus.Fetched: + logInfo("Fetched", Color.green, "package %s successfully", udesc); + break; + } + if (this.recursive) { + auto pack = dub.packageManager.getBestPackage( + PackageName(udesc.name), udesc.range); + auto proj = new Project(dub.packageManager, pack); + if (!proj.hasAllDependencies) { + logInfo("Resolving", Color.green, "missing dependencies for project"); + dub.loadPackage(pack); + dub.upgrade(UpgradeOptions.select); + } + } + } + + /// Implementation for argument version + private FetchStatus fetchSinglePackage(Dub dub, UserPackageDesc udesc) + { + auto fspkg = dub.packageManager.getBestPackage( + PackageName(udesc.name), udesc.range); + // Avoid dub fetch if the package is present on the filesystem. + if (fspkg !is null && udesc.range.isExactVersion()) + return FetchStatus.Present; + + try { + auto pkg = dub.fetch(PackageName(udesc.name), udesc.range, + FetchOptions.forceBranchUpgrade); + assert(pkg !is null, "dub.fetch returned a null Package"); + return pkg is fspkg ? FetchStatus.Present : FetchStatus.Fetched; + } catch (Exception e) { + logError("Fetching %s failed: %s", udesc, e.msg); + return FetchStatus.Failed; + } } } @@ -2242,13 +2325,15 @@ if (!m_version.empty) { // remove then --version removed enforceUsage(!package_id.canFindVersionSplitter, "Double version spec not allowed."); logWarn("The '--version' parameter was deprecated, use %s@%s. Please update your scripts.", package_id, m_version); - dub.remove(package_id, m_version, location); + dub.remove(PackageName(package_id), m_version, location); } else { - const parts = package_id.splitPackageName; - if (m_nonInteractive || parts.version_.length) { - dub.remove(parts.name, parts.version_, location); + const parts = UserPackageDesc.fromString(package_id); + const explicit = package_id.canFindVersionSplitter; + if (m_nonInteractive || explicit || parts.range != VersionRange.Any) { + const str = parts.range.matchesAny() ? "*" : parts.range.toString(); + dub.remove(PackageName(parts.name), str, location); } else { - dub.remove(package_id, location, &resolveVersion); + dub.remove(PackageName(package_id), location, &resolveVersion); } } return 0; @@ -2267,8 +2352,8 @@ override void prepare(scope CommandArgs args) { args.getopt("system", &m_system, [ - "Register system-wide instead of user-wide" - ]); + "DEPRECATED: Use --cache=system instead" + ], true); } abstract override int execute(Dub dub, string[] free_args, string[] app_args); @@ -2295,7 +2380,12 @@ override int execute(Dub dub, string[] free_args, string[] app_args) { enforceUsage(free_args.length == 1, "Missing search path."); - dub.addSearchPath(free_args[0], m_system); + enforceUsage(!this.m_system || dub.defaultPlacementLocation == PlacementLocation.user, + "Cannot use both --system and --cache, prefer --cache"); + if (this.m_system) + dub.addSearchPath(free_args[0], PlacementLocation.system); + else + dub.addSearchPath(free_args[0], dub.defaultPlacementLocation); return 0; } } @@ -2312,7 +2402,12 @@ override int execute(Dub dub, string[] free_args, string[] app_args) { enforceUsage(free_args.length == 1, "Expected one argument."); - dub.removeSearchPath(free_args[0], m_system); + enforceUsage(!this.m_system || dub.defaultPlacementLocation == PlacementLocation.user, + "Cannot use both --system and --cache, prefer --cache"); + if (this.m_system) + dub.removeSearchPath(free_args[0], PlacementLocation.system); + else + dub.removeSearchPath(free_args[0], dub.defaultPlacementLocation); return 0; } } @@ -2334,9 +2429,16 @@ override int execute(Dub dub, string[] free_args, string[] app_args) { - enforceUsage(free_args.length == 1 || free_args.length == 2, "Expecting one or two arguments."); + enforceUsage(free_args.length == 1 || free_args.length == 2, + "Expecting one or two arguments."); + enforceUsage(!this.m_system || dub.defaultPlacementLocation == PlacementLocation.user, + "Cannot use both --system and --cache, prefer --cache"); + string ver = free_args.length == 2 ? free_args[1] : null; - dub.addLocalPackage(free_args[0], ver, m_system); + if (this.m_system) + dub.addLocalPackage(free_args[0], ver, PlacementLocation.system); + else + dub.addLocalPackage(free_args[0], ver, dub.defaultPlacementLocation); return 0; } } @@ -2353,8 +2455,15 @@ override int execute(Dub dub, string[] free_args, string[] app_args) { enforceUsage(free_args.length >= 1, "Missing package path argument."); - enforceUsage(free_args.length <= 1, "Expected the package path to be the only argument."); - dub.removeLocalPackage(free_args[0], m_system); + enforceUsage(free_args.length <= 1, + "Expected the package path to be the only argument."); + enforceUsage(!this.m_system || dub.defaultPlacementLocation == PlacementLocation.user, + "Cannot use both --system and --cache, prefer --cache"); + + if (this.m_system) + dub.removeLocalPackage(free_args[0], PlacementLocation.system); + else + dub.removeLocalPackage(free_args[0], dub.defaultPlacementLocation); return 0; } } @@ -2376,13 +2485,12 @@ override int execute(Dub dub, string[] free_args, string[] app_args) { enforceUsage(free_args.length <= 1, "Expecting zero or one extra arguments."); - const pinfo = free_args.length ? splitPackageName(free_args[0]) : PackageAndVersion("","*"); + const pinfo = free_args.length ? UserPackageDesc.fromString(free_args[0]) : UserPackageDesc("",VersionRange.Any); const pname = pinfo.name; - const pvlim = Dependency(pinfo.version_ == "" ? "*" : pinfo.version_); enforceUsage(app_args.length == 0, "The list command supports no application arguments."); logInfoNoTag("Packages present in the system and known to dub:"); foreach (p; dub.packageManager.getPackageIterator()) { - if ((pname == "" || pname == p.name) && pvlim.matches(p.version_)) + if ((pname == "" || pname == p.name) && pinfo.range.matches(p.version_)) logInfoNoTag(" %s %s: %s", p.name.color(Mode.bold), p.version_, p.path.toNativeString()); } logInfo(""); @@ -2538,7 +2646,7 @@ logInfoNoTag("# %s", caption); foreach (ovr; overrides) ovr.target.match!( - t => logInfoNoTag("%s %s => %s", ovr.package_.color(Mode.bold), ovr.version_, t)); + t => logInfoNoTag("%s %s => %s", ovr.package_.color(Mode.bold), ovr.source, t)); } printList(dub.packageManager.getOverrides_(PlacementLocation.user), "User wide overrides"); printList(dub.packageManager.getOverrides_(PlacementLocation.system), "System wide overrides"); @@ -2638,7 +2746,7 @@ import std.format : formattedWrite; if (m_testPackage.length) { - setupPackage(dub, m_testPackage); + setupPackage(dub, UserPackageDesc(m_testPackage)); m_defaultConfig = dub.project.getDefaultConfiguration(this.baseSettings.platform); GeneratorSettings gensettings = this.baseSettings; @@ -2667,7 +2775,7 @@ if (!path.absolute) path = getWorkingDirectory() ~ path; enforceUsage(!path.startsWith(dub.rootPath), "Destination path must not be a sub directory of the tested package!"); - setupPackage(dub, null); + setupPackage(dub, UserPackageDesc.init); auto prj = dub.project; if (this.baseSettings.config.empty) this.baseSettings.config = prj.getDefaultConfiguration(this.baseSettings.platform); @@ -2690,11 +2798,10 @@ } } - static void fixPathDependency(string pack, ref Dependency dep) { + static void fixPathDependency(in PackageName name, ref Dependency dep) { dep.visit!( (NativePath path) { - auto mainpack = getBasePackageName(pack); - dep = Dependency(NativePath("../") ~ mainpack); + dep = Dependency(NativePath("../") ~ name.main.toString()); }, (any) { /* Nothing to do */ }, ); @@ -2703,11 +2810,11 @@ void fixPathDependencies(ref PackageRecipe recipe, NativePath base_path) { foreach (name, ref dep; recipe.buildSettings.dependencies) - fixPathDependency(name, dep); + fixPathDependency(PackageName(name), dep); foreach (ref cfg; recipe.configurations) foreach (name, ref dep; cfg.buildSettings.dependencies) - fixPathDependency(name, dep); + fixPathDependency(PackageName(name), dep); foreach (ref subp; recipe.subPackages) if (subp.path.length) { @@ -3026,11 +3133,9 @@ private bool addDependency(Dub dub, ref PackageRecipe recipe, string depspec) { Dependency dep; - const parts = splitPackageName(depspec); - const depname = parts.name; - if (parts.version_) - dep = Dependency(parts.version_); - else + const parts = UserPackageDesc.fromString(depspec); + const depname = PackageName(parts.name); + if (parts.range == VersionRange.Any) { try { const ver = dub.getLatestVersion(depname); @@ -3041,52 +3146,88 @@ return false; } } - recipe.buildSettings.dependencies[depname] = dep; + else + dep = Dependency(parts.range); + recipe.buildSettings.dependencies[depname.toString()] = dep; logInfo("Adding dependency %s %s", depname, dep.toString()); return true; } -private struct PackageAndVersion +/** + * A user-provided package description + * + * User provided package description currently only covers packages + * referenced by their name with an associated version. + * Hence there is an implicit assumption that they are in the registry. + * Future improvements could support `Dependency` instead of `VersionRange`. + */ +private struct UserPackageDesc { string name; - string version_; -} + VersionRange range = VersionRange.Any; -/* Split <package>=<version-specifier> and <package>@<version-specifier> - into `name` and `version_`. */ -private PackageAndVersion splitPackageName(string packageName) -{ - // split <package>@<version-specifier> - auto parts = packageName.findSplit("@"); - if (parts[1].empty) { - // split <package>=<version-specifier> - parts = packageName.findSplit("="); + /// Provides a string representation for the user + public string toString() const + { + if (this.range.matchesAny()) + return this.name; + return format("%s@%s", this.name, range); } - PackageAndVersion p; - p.name = parts[0]; - if (!parts[1].empty) - p.version_ = parts[2]; - return p; + /** + * Breaks down a user-provided string into its name and version range + * + * User-provided strings (via the command line) are either in the form + * `<package>=<version-specifier>` or `<package>@<version-specifier>`. + * As it is more explicit, we recommend the latter (the `@` version + * is not used by names or `VersionRange`, but `=` is). + * + * If no version range is provided, the returned struct has its `range` + * property set to `VersionRange.Any` as this is the most usual usage + * in the command line. Some cakkers may want to distinguish between + * user-provided version and implicit version, but this is discouraged. + * + * Params: + * str = User-provided string + * + * Returns: + * A populated struct. + */ + static UserPackageDesc fromString(string packageName) + { + // split <package>@<version-specifier> + auto parts = packageName.findSplit("@"); + if (parts[1].empty) { + // split <package>=<version-specifier> + parts = packageName.findSplit("="); + } + + UserPackageDesc p; + p.name = parts[0]; + p.range = !parts[1].empty + ? VersionRange.fromString(parts[2]) + : VersionRange.Any; + return p; + } } unittest { // https://github.com/dlang/dub/issues/1681 - assert(splitPackageName("") == PackageAndVersion("", null)); + assert(UserPackageDesc.fromString("") == UserPackageDesc("", VersionRange.Any)); - assert(splitPackageName("foo") == PackageAndVersion("foo", null)); - assert(splitPackageName("foo=1.0.1") == PackageAndVersion("foo", "1.0.1")); - assert(splitPackageName("foo@1.0.1") == PackageAndVersion("foo", "1.0.1")); - assert(splitPackageName("foo@==1.0.1") == PackageAndVersion("foo", "==1.0.1")); - assert(splitPackageName("foo@>=1.0.1") == PackageAndVersion("foo", ">=1.0.1")); - assert(splitPackageName("foo@~>1.0.1") == PackageAndVersion("foo", "~>1.0.1")); - assert(splitPackageName("foo@<1.0.1") == PackageAndVersion("foo", "<1.0.1")); + assert(UserPackageDesc.fromString("foo") == UserPackageDesc("foo", VersionRange.Any)); + assert(UserPackageDesc.fromString("foo=1.0.1") == UserPackageDesc("foo", VersionRange.fromString("1.0.1"))); + assert(UserPackageDesc.fromString("foo@1.0.1") == UserPackageDesc("foo", VersionRange.fromString("1.0.1"))); + assert(UserPackageDesc.fromString("foo@==1.0.1") == UserPackageDesc("foo", VersionRange.fromString("==1.0.1"))); + assert(UserPackageDesc.fromString("foo@>=1.0.1") == UserPackageDesc("foo", VersionRange.fromString(">=1.0.1"))); + assert(UserPackageDesc.fromString("foo@~>1.0.1") == UserPackageDesc("foo", VersionRange.fromString("~>1.0.1"))); + assert(UserPackageDesc.fromString("foo@<1.0.1") == UserPackageDesc("foo", VersionRange.fromString("<1.0.1"))); } private ulong canFindVersionSplitter(string packageName) { - // see splitPackageName + // see UserPackageDesc.fromString return packageName.canFind("@", "="); } diff --git a/source/dub/compilers/compiler.d b/source/dub/compilers/compiler.d index 6a0014e..4dba3db 100644 --- a/source/dub/compilers/compiler.d +++ b/source/dub/compilers/compiler.d @@ -9,7 +9,7 @@ public import dub.compilers.buildsettings; deprecated("Please `import dub.dependency : Dependency` instead") public import dub.dependency : Dependency; -public import dub.data.platform : BuildPlatform, matchesSpecification; +public import dub.platform : BuildPlatform, matchesSpecification; import dub.internal.vibecompat.inet.path; import dub.internal.vibecompat.core.file; @@ -177,24 +177,21 @@ args = arguments for the probe compilation arch_override = special handler for x86_mscoff */ - protected final BuildPlatform probePlatform(string compiler_binary, string[] args, - string arch_override) + protected final BuildPlatform probePlatform(string compiler_binary, string[] args, string arch_override) { - import dub.compilers.utils : generatePlatformProbeFile, readPlatformJsonProbe; + import dub.compilers.utils : generatePlatformProbeFile, readPlatformSDLProbe; import std.string : format, strip; - auto fil = generatePlatformProbeFile(); + NativePath fil = generatePlatformProbeFile(); auto result = execute(compiler_binary ~ args ~ fil.toNativeString()); enforce!CompilerInvocationException(result.status == 0, format("Failed to invoke the compiler %s to determine the build platform: %s", compiler_binary, result.output)); - - auto build_platform = readPlatformJsonProbe(result.output); + BuildPlatform build_platform = readPlatformSDLProbe(result.output); + string ver = determineVersion(compiler_binary, result.output).strip; build_platform.compilerBinary = compiler_binary; - auto ver = determineVersion(compiler_binary, result.output) - .strip; if (ver.empty) { logWarn(`Could not probe the compiler version for "%s". ` ~ `Toolchain requirements might be ineffective`, build_platform.compiler); diff --git a/source/dub/compilers/dmd.d b/source/dub/compilers/dmd.d index 78fd9d4..5b59d2b 100644 --- a/source/dub/compilers/dmd.d +++ b/source/dub/compilers/dmd.d @@ -230,6 +230,11 @@ { enforceBuildRequirements(settings); + // Keep the current dflags at the end of the array so that they will overwrite other flags. + // This allows user $DFLAGS to modify flags added by us. + const dflagsTail = settings.dflags; + settings.dflags = []; + if (!(fields & BuildSetting.options)) { foreach (t; s_options) if (settings.options & t[0]) @@ -282,6 +287,8 @@ if (platform.platform.canFind("posix") && (settings.options & BuildOption.pic)) settings.addDFlags("-fPIC"); + settings.addDFlags(dflagsTail); + assert(fields & BuildSetting.dflags); assert(fields & BuildSetting.copyFiles); } diff --git a/source/dub/compilers/gdc.d b/source/dub/compilers/gdc.d index 0d34446..3df8eda 100644 --- a/source/dub/compilers/gdc.d +++ b/source/dub/compilers/gdc.d @@ -89,6 +89,11 @@ { enforceBuildRequirements(settings); + // Keep the current dflags at the end of the array so that they will overwrite other flags. + // This allows user $DFLAGS to modify flags added by us. + const dflagsTail = settings.dflags; + settings.dflags = []; + if (!(fields & BuildSetting.options)) { foreach (t; s_options) if (settings.options & t[0]) @@ -138,6 +143,8 @@ if (settings.options & BuildOption.pic) settings.addDFlags("-fPIC"); + settings.addDFlags(dflagsTail); + assert(fields & BuildSetting.dflags); assert(fields & BuildSetting.copyFiles); } diff --git a/source/dub/compilers/ldc.d b/source/dub/compilers/ldc.d index 3cb90e5..3959df6 100644 --- a/source/dub/compilers/ldc.d +++ b/source/dub/compilers/ldc.d @@ -107,6 +107,11 @@ import std.format : format; enforceBuildRequirements(settings); + // Keep the current dflags at the end of the array so that they will overwrite other flags. + // This allows user $DFLAGS to modify flags added by us. + const dflagsTail = settings.dflags; + settings.dflags = []; + if (!(fields & BuildSetting.options)) { foreach (t; s_options) if (settings.options & t[0]) @@ -170,6 +175,8 @@ } } + settings.addDFlags(dflagsTail); + assert(fields & BuildSetting.dflags); assert(fields & BuildSetting.copyFiles); } diff --git a/source/dub/compilers/utils.d b/source/dub/compilers/utils.d index b3bb1e6..2b8addf 100644 --- a/source/dub/compilers/utils.d +++ b/source/dub/compilers/utils.d @@ -8,7 +8,7 @@ module dub.compilers.utils; import dub.compilers.buildsettings; -import dub.platform : BuildPlatform, archCheck, compilerCheck, platformCheck; +import dub.platform : BuildPlatform, archCheck, compilerCheckPragmas, platformCheck, pragmaGen; import dub.internal.vibecompat.inet.path; import dub.internal.logging; @@ -269,77 +269,101 @@ NativePath generatePlatformProbeFile() { import dub.internal.vibecompat.core.file; - import dub.internal.vibecompat.data.json; import dub.internal.utils; import std.string : format; - // try to not use phobos in the probe to avoid long import times + enum moduleInfo = q{ + module object; + alias string = const(char)[]; + }; + + // avoid druntime so that this compiles without a compiler's builtin object.d enum probe = q{ - module dub_platform_probe; - - template toString(int v) { enum toString = v.stringof; } - string stringArray(string[] ary) { - string res; - foreach (i, e; ary) { - if (i) - res ~= ", "; - res ~= '"' ~ e ~ '"'; - } - return res; - } - - pragma(msg, `%1$s` - ~ '\n' ~ `{` - ~ '\n' ~ ` "compiler": "`~ determineCompiler() ~ `",` - ~ '\n' ~ ` "frontendVersion": ` ~ toString!__VERSION__ ~ `,` - ~ '\n' ~ ` "compilerVendor": "` ~ __VENDOR__ ~ `",` - ~ '\n' ~ ` "platform": [` - ~ '\n' ~ ` ` ~ determinePlatform().stringArray - ~ '\n' ~ ` ],` - ~ '\n' ~ ` "architecture": [` - ~ '\n' ~ ` ` ~ determineArchitecture().stringArray - ~ '\n' ~ ` ],` - ~ '\n' ~ `}` - ~ '\n' ~ `%2$s`); - - string[] determinePlatform() { %3$s } - string[] determineArchitecture() { %4$s } - string determineCompiler() { %5$s } - - }.format(probeBeginMark, probeEndMark, platformCheck, archCheck, compilerCheck); + %1$s + pragma(msg, `%2$s`); + pragma(msg, `\n`); + pragma(msg, `compiler`); + %6$s + pragma(msg, `\n`); + pragma(msg, `frontendVersion "`); + pragma(msg, __VERSION__.stringof); + pragma(msg, `"\n`); + pragma(msg, `compilerVendor "`); + pragma(msg, __VENDOR__); + pragma(msg, `"\n`); + pragma(msg, `platform`); + %4$s + pragma(msg, `\n`); + pragma(msg, `architecture `); + %5$s + pragma(msg, `\n`); + pragma(msg, `%3$s`); + }.format(moduleInfo, probeBeginMark, probeEndMark, pragmaGen(platformCheck), pragmaGen(archCheck), compilerCheckPragmas); auto path = getTempFile("dub_platform_probe", ".d"); writeFile(path, probe); - return path; } + /** - Processes the JSON output generated by compiling the platform probe file. + Processes the SDL output generated by compiling the platform probe file. See_Also: `generatePlatformProbeFile`. */ -BuildPlatform readPlatformJsonProbe(string output) +BuildPlatform readPlatformSDLProbe(string output) { - import std.algorithm : map; + import std.algorithm : map, max, splitter, joiner, count, filter; import std.array : array; import std.exception : enforce; + import std.range : front; + import std.ascii : newline; import std.string; + import dub.internal.sdlang.parser; + import dub.internal.sdlang.ast; + import std.conv; // work around possible additional output of the compiler - auto idx1 = output.indexOf(probeBeginMark); - auto idx2 = output.lastIndexOf(probeEndMark); + auto idx1 = output.indexOf(probeBeginMark ~ newline ~ "\\n"); + auto idx2 = output[max(0, idx1) .. $].indexOf(probeEndMark) + idx1; enforce(idx1 >= 0 && idx1 < idx2, "Unexpected platform information output - does not contain a JSON object."); - output = output[idx1+probeBeginMark.length .. idx2]; + output = output[idx1 + probeBeginMark.length .. idx2].replace(newline, "").replace("\\n", "\n"); - import dub.internal.vibecompat.data.json; - auto json = parseJsonString(output); + output = output.splitter("\n").filter!((e) => e.length > 0) + .map!((e) { + if (e.count("\"") == 0) + { + return e ~ ` ""`; + } + return e; + }) + .joiner("\n").array().to!string; BuildPlatform build_platform; - build_platform.platform = json["platform"].get!(Json[]).map!(e => e.get!string()).array(); - build_platform.architecture = json["architecture"].get!(Json[]).map!(e => e.get!string()).array(); - build_platform.compiler = json["compiler"].get!string; - build_platform.frontendVersion = json["frontendVersion"].get!int; + Tag sdl = parseSource(output); + + foreach (n; sdl.all.tags) + { + switch (n.name) + { + default: + break; + case "platform": + build_platform.platform = n.values.map!(e => e.toString()).array(); + break; + case "architecture": + build_platform.architecture = n.values.map!(e => e.toString()).array(); + break; + case "compiler": + build_platform.compiler = n.values.front.toString(); + break; + case "frontendVersion": + build_platform.frontendVersion = n.values.front.toString() + .filter!((e) => e >= '0' && e <= '9').array().to!string + .to!int; + break; + } + } return build_platform; } diff --git a/source/dub/data/platform.d b/source/dub/data/platform.d deleted file mode 100644 index fb0870e..0000000 --- a/source/dub/data/platform.d +++ /dev/null @@ -1,128 +0,0 @@ -/******************************************************************************* - - Represent a target platform - - Platform informations can be embedded in recipe, such that some settings - only target a certain platform (e.g. sourceFiles, lflags, etc...). - The struct in this module represent that information, structured. - -*******************************************************************************/ - -module dub.data.platform; - -/// Represents a platform a package can be build upon. -struct BuildPlatform { - /// Special constant used to denote matching any build platform. - enum any = BuildPlatform(null, null, null, null, -1); - - /// Platform identifiers, e.g. ["posix", "windows"] - string[] platform; - /// CPU architecture identifiers, e.g. ["x86", "x86_64"] - string[] architecture; - /// Canonical compiler name e.g. "dmd" - string compiler; - /// Compiler binary name e.g. "ldmd2" - string compilerBinary; - /// Compiled frontend version (e.g. `2067` for frontend versions 2.067.x) - int frontendVersion; - /// Compiler version e.g. "1.11.0" - string compilerVersion; - /// Frontend version string from frontendVersion - /// e.g: 2067 => "2.067" - string frontendVersionString() const - { - import std.format : format; - - const maj = frontendVersion / 1000; - const min = frontendVersion % 1000; - return format("%d.%03d", maj, min); - } - /// - unittest - { - BuildPlatform bp; - bp.frontendVersion = 2067; - assert(bp.frontendVersionString == "2.067"); - } - - /// Checks to see if platform field contains windows - bool isWindows() const { - import std.algorithm : canFind; - return this.platform.canFind("windows"); - } - /// - unittest { - BuildPlatform bp; - bp.platform = ["windows"]; - assert(bp.isWindows); - bp.platform = ["posix"]; - assert(!bp.isWindows); - } -} - -/** Matches a platform specification string against a build platform. - - Specifications are build upon the following scheme, where each component - is optional (indicated by []), but the order is obligatory: - "[-platform][-architecture][-compiler]" - - So the following strings are valid specifications: `"-windows-x86-dmd"`, - `"-dmd"`, `"-arm"`, `"-arm-dmd"`, `"-windows-dmd"` - - Params: - platform = The build platform to match against the platform specification - specification = The specification being matched. It must either be an - empty string or start with a dash. - - Returns: - `true` if the given specification matches the build platform, `false` - otherwise. Using an empty string as the platform specification will - always result in a match. -*/ -bool matchesSpecification(in BuildPlatform platform, const(char)[] specification) -{ - import std.range : empty; - import std.string : chompPrefix, format; - import std.algorithm : canFind, splitter; - import std.exception : enforce; - - if (specification.empty) return true; - if (platform == BuildPlatform.any) return true; - - auto splitted = specification.chompPrefix("-").splitter('-'); - enforce(!splitted.empty, format("Platform specification, if present, must not be empty: \"%s\"", specification)); - - if (platform.platform.canFind(splitted.front)) { - splitted.popFront(); - if (splitted.empty) - return true; - } - if (platform.architecture.canFind(splitted.front)) { - splitted.popFront(); - if (splitted.empty) - return true; - } - if (platform.compiler == splitted.front) { - splitted.popFront(); - enforce(splitted.empty, "No valid specification! The compiler has to be the last element: " ~ specification); - return true; - } - return false; -} - -/// -unittest { - auto platform = BuildPlatform(["posix", "linux"], ["x86_64"], "dmd"); - assert(platform.matchesSpecification("")); - assert(platform.matchesSpecification("posix")); - assert(platform.matchesSpecification("linux")); - assert(platform.matchesSpecification("linux-dmd")); - assert(platform.matchesSpecification("linux-x86_64-dmd")); - assert(platform.matchesSpecification("x86_64")); - assert(!platform.matchesSpecification("windows")); - assert(!platform.matchesSpecification("ldc")); - assert(!platform.matchesSpecification("windows-dmd")); - - // Before PR#2279, a leading '-' was required - assert(platform.matchesSpecification("-x86_64")); -} diff --git a/source/dub/dependency.d b/source/dub/dependency.d index c0562fc..fc444e0 100644 --- a/source/dub/dependency.d +++ b/source/dub/dependency.d @@ -18,12 +18,89 @@ import std.exception; import std.string; +/// Represents a fully-qualified package name +public struct PackageName +{ + /// The underlying full name of the package + private string fullName; + /// Where the separator lies, if any + private size_t separator; + + /// Creates a new instance of this struct + public this(string fn) @safe pure + { + this.fullName = fn; + if (auto idx = fn.indexOf(':')) + this.separator = idx > 0 ? idx : fn.length; + else // We were given `:foo` + assert(0, "Argument to PackageName constructor needs to be " ~ + "a fully qualified string"); + } + + /// Private constructor to have nothrow / @nogc + private this(string fn, size_t sep) @safe pure nothrow @nogc + { + this.fullName = fn; + this.separator = sep; + } + + /// The base package name in which the subpackages may live + public PackageName main () const return @safe pure nothrow @nogc + { + return PackageName(this.fullName[0 .. this.separator], this.separator); + } + + /// The subpackage name, or an empty string if there isn't + public string sub () const return @safe pure nothrow @nogc + { + // Return `null` instead of an empty string so that + // it can be used in a boolean context, e.g. + // `if (name.sub)` would be true with empty string + return this.separator < this.fullName.length + ? this.fullName[this.separator + 1 .. $] + : null; + } + + /// Human readable representation + public string toString () const return scope @safe pure nothrow @nogc + { + return this.fullName; + } + + /// + public int opCmp (in PackageName other) const scope @safe pure nothrow @nogc + { + import core.internal.string : dstrcmp; + return dstrcmp(this.toString(), other.toString()); + } + + /// + public bool opEquals (in PackageName other) const scope @safe pure nothrow @nogc + { + return this.toString() == other.toString(); + } +} /** Encapsulates the name of a package along with its dependency specification. */ struct PackageDependency { + /// Backward compatibility + deprecated("Use the constructor that accepts a `PackageName` as first argument") + this(string n, Dependency s = Dependency.init) @safe pure + { + this.name = PackageName(n); + this.spec = s; + } + + // Remove once deprecated overload is gone + this(PackageName n, Dependency s = Dependency.init) @safe pure nothrow @nogc + { + this.name = n; + this.spec = s; + } + /// Name of the referenced package. - string name; + PackageName name; /// Dependency specification used to select a particular version of the package. Dependency spec; @@ -47,14 +124,19 @@ // Shortcut to create >=0.0.0 private enum ANY_IDENT = "*"; - private Value m_value; + private Value m_value = Value(VersionRange.Invalid); private bool m_optional; private bool m_default; /// A Dependency, which matches every valid version. - static @property Dependency any() @safe { return Dependency(VersionRange.Any); } + public static immutable Dependency Any = Dependency(VersionRange.Any); /// An invalid dependency (with no possible version matches). + public static immutable Dependency Invalid = Dependency(VersionRange.Invalid); + + deprecated("Use `Dependency.Any` instead") + static @property Dependency any() @safe { return Dependency(VersionRange.Any); } + deprecated("Use `Dependency.Invalid` instead") static @property Dependency invalid() @safe { return Dependency(VersionRange.Invalid); @@ -433,11 +515,11 @@ */ Dependency merge(ref const(Dependency) o) const @trusted { alias Merger = match!( - (const NativePath a, const NativePath b) => a == b ? this : invalid, + (const NativePath a, const NativePath b) => a == b ? this : Invalid, (const NativePath a, any ) => o, ( any , const NativePath b) => this, - (const Repository a, const Repository b) => a.m_ref == b.m_ref ? this : invalid, + (const Repository a, const Repository b) => a.m_ref == b.m_ref ? this : Invalid, (const Repository a, any ) => this, ( any , const Repository b) => o, @@ -447,7 +529,7 @@ VersionRange copy = a; copy.merge(b); - if (!copy.isValid()) return invalid; + if (!copy.isValid()) return Invalid; return Dependency(copy); } ); @@ -605,7 +687,7 @@ assert(a.valid); assert(a.version_ == Version("~d2test")); - a = Dependency.any; + a = Dependency.Any; assert(!a.optional); assert(a.valid); assertThrown(a.version_); diff --git a/source/dub/dependencyresolver.d b/source/dub/dependencyresolver.d index d064c61..8848814 100644 --- a/source/dub/dependencyresolver.d +++ b/source/dub/dependencyresolver.d @@ -61,7 +61,7 @@ possible configurations for the target package. */ static struct TreeNodes { - string pack; + PackageName pack; CONFIGS configs; DependencyType depType = DependencyType.required; @@ -83,7 +83,7 @@ Nodes are a combination of a package and a single package configuration. */ static struct TreeNode { - string pack; + PackageName pack; CONFIG config; size_t toHash() const nothrow @trusted { @@ -99,9 +99,9 @@ } } - CONFIG[string] resolve(TreeNode root, bool throw_on_failure = true) + CONFIG[PackageName] resolve(TreeNode root, bool throw_on_failure = true) { - auto rootbase = root.pack.basePackageName; + auto rootbase = root.pack.main; // build up the dependency graph, eliminating as many configurations/ // versions as possible @@ -124,8 +124,8 @@ return context.result; } - protected abstract CONFIG[] getAllConfigs(string pack); - protected abstract CONFIG[] getSpecificConfigs(string pack, TreeNodes nodes); + protected abstract CONFIG[] getAllConfigs(in PackageName pack); + protected abstract CONFIG[] getSpecificConfigs(in PackageName pack, TreeNodes nodes); protected abstract TreeNodes[] getChildren(TreeNode node); protected abstract bool matches(CONFIGS configs, CONFIG config); @@ -139,19 +139,19 @@ The key is the qualified name of the package (base + sub) */ - void[0][string] visited; + void[0][PackageName] visited; /// The finally chosen configurations for each package - CONFIG[string] result; + CONFIG[PackageName] result; /// The set of available configurations for each package - ResolveConfig[][string] configs; + ResolveConfig[][PackageName] configs; /// Determines if a certain package has already been processed - bool isVisited(string package_) const { return (package_ in visited) !is null; } + bool isVisited(in PackageName package_) const { return (package_ in visited) !is null; } /// Marks a package as processed - void setVisited(string package_) { visited[package_] = (void[0]).init; } + void setVisited(in PackageName package_) { visited[package_] = (void[0]).init; } /// Returns a deep clone ResolveContext clone() @@ -172,7 +172,7 @@ */ private void constrain(TreeNode n, ref ResolveContext context, ref ulong max_iterations) { - auto base = n.pack.basePackageName; + auto base = n.pack.main; assert(base in context.configs); if (context.isVisited(n.pack)) return; context.setVisited(n.pack); @@ -184,7 +184,7 @@ foreach (dep; dependencies) { // lazily load all dependency configurations - auto depbase = dep.pack.basePackageName; + auto depbase = dep.pack.main; auto di = depbase in context.configs; if (!di) { context.configs[depbase] = @@ -238,7 +238,7 @@ ~ " recipe that reproduces this error."); auto dep = &dependencies[depidx]; - auto depbase = dep.pack.basePackageName; + auto depbase = dep.pack.main; auto depconfigs = context.configs[depbase]; Exception first_err; @@ -277,18 +277,18 @@ dep.pack, dep.configs, n.pack, n.config)); } - private void purgeOptionalDependencies(TreeNode root, ref CONFIG[string] configs) + private void purgeOptionalDependencies(TreeNode root, ref CONFIG[PackageName] configs) { - bool[string] required; - bool[string] visited; + bool[PackageName] required; + bool[PackageName] visited; void markRecursively(TreeNode node) { if (node.pack in visited) return; visited[node.pack] = true; - required[node.pack.basePackageName] = true; + required[node.pack.main] = true; foreach (dep; getChildren(node).filter!(dep => dep.depType != DependencyType.optional)) - if (auto dp = dep.pack.basePackageName in configs) + if (auto dp = dep.pack.main in configs) markRecursively(TreeNode(dep.pack, *dp)); } @@ -305,23 +305,23 @@ import std.range : chain, only; import std.typecons : tuple; - string failedNode; + PackageName failedNode; this(TreeNode parent, TreeNodes dep, const scope ref ResolveContext context, string file = __FILE__, size_t line = __LINE__) { - auto m = format("Unresolvable dependencies to package %s:", dep.pack.basePackageName); + auto m = format("Unresolvable dependencies to package %s:", dep.pack.main); super(m, file, line); this.failedNode = dep.pack; - auto failbase = failedNode.basePackageName; + auto failbase = failedNode.main; // get the list of all dependencies to the failed package auto deps = context.visited.byKey - .filter!(p => p.basePackageName in context.result) - .map!(p => TreeNode(p, context.result[p.basePackageName])) + .filter!(p => !!(p.main in context.result)) + .map!(p => TreeNode(p, context.result[p.main])) .map!(n => getChildren(n) - .filter!(d => d.pack.basePackageName == failbase) + .filter!(d => d.pack.main == failbase) .map!(d => tuple(n, d)) ) .join @@ -329,7 +329,7 @@ foreach (d; deps) { // filter out trivial self-dependencies - if (d[0].pack.basePackageName == failbase + if (d[0].pack.main == failbase && matches(d[1].configs, d[0].config)) continue; msg ~= format("\n %s %s depends on %s %s", d[0].pack, d[0].config, d[1].pack, d[1].configs); @@ -359,12 +359,6 @@ optional } -private string basePackageName(string p) -{ - import std.algorithm.searching : findSplit; - return p.findSplit(":")[0]; -} - unittest { static struct IntConfig { int value; @@ -377,119 +371,130 @@ alias configs this; } static IntConfigs ics(IntConfig[] cfgs) { return IntConfigs(cfgs); } + static PackageName pn(string name) { return PackageName(name); } static class TestResolver : DependencyResolver!(IntConfigs, IntConfig) { private TreeNodes[][string] m_children; this(TreeNodes[][string] children) { super(ulong.max); m_children = children; } - protected override IntConfig[] getAllConfigs(string pack) { + protected override IntConfig[] getAllConfigs(in PackageName pack) { auto ret = appender!(IntConfig[]); - foreach (p; m_children.byKey) { - if (p.length <= pack.length+1) continue; - if (p[0 .. pack.length] != pack || p[pack.length] != ':') continue; - auto didx = p.lastIndexOf(':'); - ret ~= ic(p[didx+1 .. $].to!uint); + foreach (p_; m_children.byKey) { + // Note: We abuse subpackage notation to store configs + const p = PackageName(p_); + if (p.main != pack.main) continue; + ret ~= ic(p.sub.to!uint); } ret.data.sort!"a>b"(); return ret.data; } - protected override IntConfig[] getSpecificConfigs(string pack, TreeNodes nodes) { return null; } - protected override TreeNodes[] getChildren(TreeNode node) { return m_children.get(node.pack ~ ":" ~ node.config.to!string(), null); } + protected override IntConfig[] getSpecificConfigs(in PackageName pack, TreeNodes nodes) { return null; } + protected override TreeNodes[] getChildren(TreeNode node) { + assert(node.pack.sub.length == 0); + return m_children.get(node.pack.toString() ~ ":" ~ node.config.to!string(), null); + } protected override bool matches(IntConfigs configs, IntConfig config) { return configs.canFind(config); } } // properly back up if conflicts are detected along the way (d:2 vs d:1) with (TestResolver) { auto res = new TestResolver([ - "a:0": [TreeNodes("b", ics([ic(2), ic(1)])), TreeNodes("d", ics([ic(1)])), TreeNodes("e", ics([ic(2), ic(1)]))], - "b:1": [TreeNodes("c", ics([ic(2), ic(1)])), TreeNodes("d", ics([ic(1)]))], - "b:2": [TreeNodes("c", ics([ic(3), ic(2)])), TreeNodes("d", ics([ic(2), ic(1)]))], + "a:0": [TreeNodes(pn("b"), ics([ic(2), ic(1)])), TreeNodes(pn("d"), ics([ic(1)])), TreeNodes(pn("e"), ics([ic(2), ic(1)]))], + "b:1": [TreeNodes(pn("c"), ics([ic(2), ic(1)])), TreeNodes(pn("d"), ics([ic(1)]))], + "b:2": [TreeNodes(pn("c"), ics([ic(3), ic(2)])), TreeNodes(pn("d"), ics([ic(2), ic(1)]))], "c:1": [], "c:2": [], "c:3": [], "d:1": [], "d:2": [], "e:1": [], "e:2": [], ]); - assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(2), "c":ic(3), "d":ic(1), "e":ic(2)], format("%s", res.resolve(TreeNode("a", ic(0))))); + assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(2), pn("c"):ic(3), pn("d"):ic(1), pn("e"):ic(2)], + format("%s", res.resolve(TreeNode(pn("a"), ic(0))))); } // handle cyclic dependencies gracefully with (TestResolver) { auto res = new TestResolver([ - "a:0": [TreeNodes("b", ics([ic(1)]))], - "b:1": [TreeNodes("b", ics([ic(1)]))] + "a:0": [TreeNodes(pn("b"), ics([ic(1)]))], + "b:1": [TreeNodes(pn("b"), ics([ic(1)]))] ]); - assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(1)]); + assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(1)]); } // don't choose optional dependencies by default with (TestResolver) { auto res = new TestResolver([ - "a:0": [TreeNodes("b", ics([ic(1)]), DependencyType.optional)], + "a:0": [TreeNodes(pn("b"), ics([ic(1)]), DependencyType.optional)], "b:1": [] ]); - assert(res.resolve(TreeNode("a", ic(0))).length == 0, to!string(res.resolve(TreeNode("a", ic(0))))); + assert(res.resolve(TreeNode(pn("a"), ic(0))).length == 0, + to!string(res.resolve(TreeNode(pn("a"), ic(0))))); } // choose default optional dependencies by default with (TestResolver) { auto res = new TestResolver([ - "a:0": [TreeNodes("b", ics([ic(1)]), DependencyType.optionalDefault)], + "a:0": [TreeNodes(pn("b"), ics([ic(1)]), DependencyType.optionalDefault)], "b:1": [] ]); - assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(1)], to!string(res.resolve(TreeNode("a", ic(0))))); + assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(1)], + to!string(res.resolve(TreeNode(pn("a"), ic(0))))); } // choose optional dependency if non-optional within the dependency tree with (TestResolver) { auto res = new TestResolver([ - "a:0": [TreeNodes("b", ics([ic(1)]), DependencyType.optional), TreeNodes("c", ics([ic(1)]))], + "a:0": [TreeNodes(pn("b"), ics([ic(1)]), DependencyType.optional), TreeNodes(pn("c"), ics([ic(1)]))], "b:1": [], - "c:1": [TreeNodes("b", ics([ic(1)]))] + "c:1": [TreeNodes(pn("b"), ics([ic(1)]))] ]); - assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(1), "c":ic(1)], to!string(res.resolve(TreeNode("a", ic(0))))); + assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(1), pn("c"):ic(1)], + to!string(res.resolve(TreeNode(pn("a"), ic(0))))); } // don't choose optional dependency if non-optional outside of final dependency tree with (TestResolver) { auto res = new TestResolver([ - "a:0": [TreeNodes("b", ics([ic(1)]), DependencyType.optional)], + "a:0": [TreeNodes(pn("b"), ics([ic(1)]), DependencyType.optional)], "b:1": [], - "preset:0": [TreeNodes("b", ics([ic(1)]))] + "preset:0": [TreeNodes(pn("b"), ics([ic(1)]))] ]); - assert(res.resolve(TreeNode("a", ic(0))).length == 0, to!string(res.resolve(TreeNode("a", ic(0))))); + assert(res.resolve(TreeNode(pn("a"), ic(0))).length == 0, + to!string(res.resolve(TreeNode(pn("a"), ic(0))))); } // don't choose optional dependency if non-optional in a non-selected version with (TestResolver) { auto res = new TestResolver([ - "a:0": [TreeNodes("b", ics([ic(1), ic(2)]))], - "b:1": [TreeNodes("c", ics([ic(1)]))], - "b:2": [TreeNodes("c", ics([ic(1)]), DependencyType.optional)], + "a:0": [TreeNodes(pn("b"), ics([ic(1), ic(2)]))], + "b:1": [TreeNodes(pn("c"), ics([ic(1)]))], + "b:2": [TreeNodes(pn("c"), ics([ic(1)]), DependencyType.optional)], "c:1": [] ]); - assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(2)], to!string(res.resolve(TreeNode("a", ic(0))))); + assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(2)], + to!string(res.resolve(TreeNode(pn("a"), ic(0))))); } // make sure non-satisfiable dependencies are not a problem, even if non-optional in some dependencies with (TestResolver) { auto res = new TestResolver([ - "a:0": [TreeNodes("b", ics([ic(1), ic(2)]))], - "b:1": [TreeNodes("c", ics([ic(2)]))], - "b:2": [TreeNodes("c", ics([ic(2)]), DependencyType.optional)], + "a:0": [TreeNodes(pn("b"), ics([ic(1), ic(2)]))], + "b:1": [TreeNodes(pn("c"), ics([ic(2)]))], + "b:2": [TreeNodes(pn("c"), ics([ic(2)]), DependencyType.optional)], "c:1": [] ]); - assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(2)], to!string(res.resolve(TreeNode("a", ic(0))))); + assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(2)], + to!string(res.resolve(TreeNode(pn("a"), ic(0))))); } // check error message for multiple conflicting dependencies with (TestResolver) { auto res = new TestResolver([ - "a:0": [TreeNodes("b", ics([ic(1)])), TreeNodes("c", ics([ic(1)]))], - "b:1": [TreeNodes("d", ics([ic(1)]))], - "c:1": [TreeNodes("d", ics([ic(2)]))], + "a:0": [TreeNodes(pn("b"), ics([ic(1)])), TreeNodes(pn("c"), ics([ic(1)]))], + "b:1": [TreeNodes(pn("d"), ics([ic(1)]))], + "c:1": [TreeNodes(pn("d"), ics([ic(2)]))], "d:1": [], "d:2": [] ]); try { - res.resolve(TreeNode("a", ic(0))); + res.resolve(TreeNode(pn("a"), ic(0))); assert(false, "Expected resolve to throw."); } catch (ResolveException e) { assert(e.msg == @@ -502,10 +507,10 @@ // check error message for invalid dependency with (TestResolver) { auto res = new TestResolver([ - "a:0": [TreeNodes("b", ics([ic(1)]))] + "a:0": [TreeNodes(pn("b"), ics([ic(1)]))] ]); try { - res.resolve(TreeNode("a", ic(0))); + res.resolve(TreeNode(pn("a"), ic(0))); assert(false, "Expected resolve to throw."); } catch (DependencyLoadException e) { assert(e.msg == "Failed to find any versions for package b, referenced by a 0"); @@ -516,12 +521,12 @@ with (TestResolver) { auto res = new TestResolver([ "a:0": [ - TreeNodes("b", ics([ic(2)]), DependencyType.optional), - TreeNodes("c", ics([ic(1)])) + TreeNodes(pn("b"), ics([ic(2)]), DependencyType.optional), + TreeNodes(pn("c"), ics([ic(1)])) ], "b:1": [], "c:1": [] ]); - assert(res.resolve(TreeNode("a", ic(0))) == ["c":ic(1)]); + assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("c"):ic(1)]); } } diff --git a/source/dub/dub.d b/source/dub/dub.d index 19c97e1..69518ab 100644 --- a/source/dub/dub.d +++ b/source/dub/dub.d @@ -64,7 +64,7 @@ PackageSupplier[] defaultPackageSuppliers() { logDiagnostic("Using dub registry url '%s'", defaultRegistryURLs[0]); - return [new FallbackPackageSupplier(defaultRegistryURLs.map!getRegistryPackageSupplier.array)]; + return [new FallbackPackageSupplier(defaultRegistryURLs.map!_getRegistryPackageSupplier.array)]; } /** Returns a registry package supplier according to protocol. @@ -74,6 +74,13 @@ deprecated("This function wasn't intended for public use - open an issue with Dub if you need it") PackageSupplier getRegistryPackageSupplier(string url) { + return _getRegistryPackageSupplier(url); +} + +// Private to avoid a bug in `defaultPackageSuppliers` with `map` triggering a deprecation +// even though the context is deprecated. +private PackageSupplier _getRegistryPackageSupplier(string url) +{ switch (url.startsWith("dub+", "mvn+", "file://")) { case 1: @@ -205,7 +212,7 @@ * generally in a library setup, one may wish to provide a custom * implementation, which can be done by overriding this method. */ - protected PackageManager makePackageManager() const + protected PackageManager makePackageManager() { return new PackageManager(m_rootPath, m_dirs.userPackages, m_dirs.systemSettings, false); } @@ -361,15 +368,15 @@ import dub.test.base : TestDub; scope (exit) environment.remove("DUB_REGISTRY"); - auto dub = new TestDub(".", null, SkipPackageSuppliers.configured); + auto dub = new TestDub(null, ".", null, SkipPackageSuppliers.configured); assert(dub.packageSuppliers.length == 0); environment["DUB_REGISTRY"] = "http://example.com/"; - dub = new TestDub(".", null, SkipPackageSuppliers.configured); + dub = new TestDub(null, ".", null, SkipPackageSuppliers.configured); assert(dub.packageSuppliers.length == 1); environment["DUB_REGISTRY"] = "http://example.com/;http://foo.com/"; - dub = new TestDub(".", null, SkipPackageSuppliers.configured); + dub = new TestDub(null, ".", null, SkipPackageSuppliers.configured); assert(dub.packageSuppliers.length == 2); - dub = new TestDub(".", [new RegistryPackageSupplier(URL("http://bar.com/"))], SkipPackageSuppliers.configured); + dub = new TestDub(null, ".", [new RegistryPackageSupplier(URL("http://bar.com/"))], SkipPackageSuppliers.configured); assert(dub.packageSuppliers.length == 3); dub = new TestDub(); @@ -499,7 +506,9 @@ /// Loads the package from the specified path as the main project package. void loadPackage(NativePath path) { - m_project = new Project(m_packageManager, path); + auto pack = this.m_packageManager.getOrLoadPackage( + path, NativePath.init, false, StrictMode.Warn); + this.loadPackage(pack); } /// Loads a specific package as the main project package (can be a sub package) @@ -575,7 +584,8 @@ recipe_content = recipe_content[idx+1 .. $]; auto recipe_default_package_name = path.toString.baseName.stripExtension.strip; - auto recipe = parsePackageRecipe(recipe_content, recipe_filename, null, recipe_default_package_name); + const PackageName empty; + auto recipe = parsePackageRecipe(recipe_content, recipe_filename, empty, recipe_default_package_name); enforce(recipe.buildSettings.sourceFiles.length == 0, "Single-file packages are not allowed to specify source files."); enforce(recipe.buildSettings.sourcePaths.length == 0, "Single-file packages are not allowed to specify source paths."); enforce(recipe.buildSettings.cSourcePaths.length == 0, "Single-file packages are not allowed to specify C source paths."); @@ -621,20 +631,21 @@ if (!(options & UpgradeOptions.upgrade)) { next_pack: foreach (p; m_project.selections.selectedPackages) { - auto dep = m_project.selections.getSelectedVersion(p); + const name = PackageName(p); // Always a main package name + auto dep = m_project.selections.getSelectedVersion(name); if (!dep.path.empty) { auto path = dep.path; if (!path.absolute) path = this.rootPath ~ path; try if (m_packageManager.getOrLoadPackage(path)) continue; catch (Exception e) { logDebug("Failed to load path based selection: %s", e.toString().sanitize); } } else if (!dep.repository.empty) { - if (m_packageManager.loadSCMPackage(getBasePackageName(p), dep.repository)) + if (m_packageManager.loadSCMPackage(name, dep.repository)) continue; } else { - if (m_packageManager.getPackage(p, dep.version_)) continue; + if (m_packageManager.getPackage(name, dep.version_)) continue; foreach (ps; m_packageSuppliers) { try { - auto versions = ps.getVersions(p); + auto versions = ps.getVersions(name); if (versions.canFind!(v => dep.matches(v, VersionMatchMode.strict))) continue next_pack; } catch (Exception e) { @@ -645,23 +656,23 @@ } logWarn("Selected package %s %s doesn't exist. Using latest matching version instead.", p, dep); - m_project.selections.deselectVersion(p); + m_project.selections.deselectVersion(name); } } auto resolver = new DependencyVersionResolver( this, options, m_project.rootPackage, m_project.selections); - Dependency[string] versions = resolver.resolve(packages_to_upgrade); + Dependency[PackageName] versions = resolver.resolve(packages_to_upgrade); if (options & UpgradeOptions.dryRun) { bool any = false; - string rootbasename = getBasePackageName(m_project.rootPackage.name); + string rootbasename = PackageName(m_project.rootPackage.name).main.toString(); foreach (p, ver; versions) { if (!ver.path.empty || !ver.repository.empty) continue; - auto basename = getBasePackageName(p); - if (basename == rootbasename) continue; + auto basename = p.main; + if (basename.toString() == rootbasename) continue; if (!m_project.selections.hasSelectedVersion(basename)) { logInfo("Upgrade", Color.cyan, @@ -674,15 +685,15 @@ if (ver.version_ <= sver.version_) continue; logInfo("Upgrade", Color.cyan, "%s would be upgraded from %s to %s.", - basename.color(Mode.bold), sver, ver); + basename.toString().color(Mode.bold), sver, ver); any = true; } if (any) logInfo("Use \"%s\" to perform those changes", "dub upgrade".color(Mode.bold)); return; } - foreach (p, ver; versions) { - assert(!p.canFind(":"), "Resolved packages contain a sub package!?: "~p); + foreach (name, ver; versions) { + assert(!name.sub, "Resolved packages contain a sub package!?: " ~ name.toString()); Package pack; if (!ver.path.empty) { try pack = m_packageManager.getOrLoadPackage(ver.path); @@ -691,15 +702,15 @@ continue; } } else if (!ver.repository.empty) { - pack = m_packageManager.loadSCMPackage(p, ver.repository); + pack = m_packageManager.loadSCMPackage(name, ver.repository); } else { assert(ver.isExactVersion, "Resolved dependency is neither path, nor repository, nor exact version based!?"); - pack = m_packageManager.getPackage(p, ver.version_); + pack = m_packageManager.getPackage(name, ver.version_); if (pack && m_packageManager.isManagedPackage(pack) && ver.version_.isBranch && (options & UpgradeOptions.upgrade) != 0) { // TODO: only re-install if there is actually a new commit available - logInfo("Re-installing branch based dependency %s %s", p, ver.toString()); + logInfo("Re-installing branch based dependency %s %s", name, ver.toString()); m_packageManager.remove(pack); pack = null; } @@ -707,16 +718,16 @@ FetchOptions fetchOpts; fetchOpts |= (options & UpgradeOptions.preRelease) != 0 ? FetchOptions.usePrerelease : FetchOptions.none; - if (!pack) fetch(p, ver.version_, defaultPlacementLocation, fetchOpts, "getting selected version"); - if ((options & UpgradeOptions.select) && p != m_project.rootPackage.name) { + if (!pack) this.fetch(name, ver.version_, fetchOpts, defaultPlacementLocation, "getting selected version"); + if ((options & UpgradeOptions.select) && name.toString() != m_project.rootPackage.name) { if (!ver.repository.empty) { - m_project.selections.selectVersion(p, ver.repository); + m_project.selections.selectVersion(name, ver.repository); } else if (ver.path.empty) { - m_project.selections.selectVersion(p, ver.version_); + m_project.selections.selectVersion(name, ver.version_); } else { NativePath relpath = ver.path; if (relpath.absolute) relpath = relpath.relativeTo(m_project.rootPackage.path); - m_project.selections.selectVersion(p, relpath); + m_project.selections.selectVersion(name, relpath); } } } @@ -783,12 +794,12 @@ { if (m_dryRun) return; - auto tool = "dscanner"; + auto tool = PackageName("dscanner"); auto tool_pack = m_packageManager.getBestPackage(tool); if (!tool_pack) { - logInfo("Hint", Color.light_blue, "%s is not present, getting and storing it user wide", tool); - tool_pack = fetch(tool, VersionRange.Any, defaultPlacementLocation, FetchOptions.none); + logInfo("Hint", Color.light_blue, "%s is not present, getting and storing it locally", tool); + tool_pack = this.fetch(tool); } auto dscanner_dub = new Dub(null, m_packageSuppliers); @@ -878,7 +889,6 @@ rmdirRecurse(cache.toNativeString()); } - /// Fetches the package matching the dependency and places it in the specified location. deprecated("Use the overload that accepts either a `Version` or a `VersionRange` as second argument") Package fetch(string packageId, const Dependency dep, PlacementLocation location, FetchOptions options, string reason = "") { @@ -889,41 +899,100 @@ return this.fetch(packageId, vrange, location, options, reason); } - /// Ditto - Package fetch(string packageId, in Version vers, PlacementLocation location, FetchOptions options, string reason = "") + deprecated("Use `fetch(PackageName, Version, [FetchOptions, PlacementLocation, string])`") + Package fetch(string name, in Version vers, PlacementLocation location, FetchOptions options, string reason = "") { - return this.fetch(packageId, VersionRange(vers, vers), location, options, reason); + const n = PackageName(name); + return this.fetch(n, VersionRange(vers, vers), options, location, reason); + } + + deprecated("Use `fetch(PackageName, VersionRange, [FetchOptions, PlacementLocation, string])`") + Package fetch(string name, in VersionRange range, PlacementLocation location, FetchOptions options, string reason = "") + { + const n = PackageName(name); + return this.fetch(n, range, options, location, reason); + } + + /** + * Fetches a missing package and stores it locally + * + * This will query the configured PackageSuppliers for a package + * matching the `range` specification, store it locally, and load + * it in the `PackageManager`. Note that unlike the command line + * version, this function is not idempotent and will remove an + * existing package and re-download it. + * + * Params: + * name = Name of the package to retrieve. Subpackages will lead + * to the main package being retrieved and the subpackage + * being returned (if it exists). + * vers = For `Version` overloads, the exact version to return. + * range = The `VersionRange` to match. Default to `Any` to fetch + * the latest version. + * options = A set of options used for fetching / matching versions. + * location = Where to store the retrieved package. Default to the + * configured `defaultPlacementLocation`. + * reason = Optionally, the reason for retriving this package. + * This is used only for logging. + * + * Returns: + * The fetched or loaded `Package`, or `null` in dry-run mode. + * + * Throws: + * If the package cannot be fetched or loaded. + */ + Package fetch(in PackageName name, in Version vers, + FetchOptions options = FetchOptions.none, string reason = "") + { + return this.fetch(name, VersionRange(vers, vers), options, + this.defaultPlacementLocation, reason); } /// Ditto - Package fetch(string packageId, in VersionRange range, PlacementLocation location, FetchOptions options, string reason = "") + Package fetch(in PackageName name, in Version vers, FetchOptions options, + PlacementLocation location, string reason = "") { - auto basePackageName = getBasePackageName(packageId); + return this.fetch(name, VersionRange(vers, vers), options, + this.defaultPlacementLocation, reason); + } + + /// Ditto + Package fetch(in PackageName name, in VersionRange range = VersionRange.Any, + FetchOptions options = FetchOptions.none, string reason = "") + { + return this.fetch(name, range, options, this.defaultPlacementLocation, reason); + } + + /// Ditto + Package fetch(in PackageName name, in VersionRange range, FetchOptions options, + PlacementLocation location, string reason = "") + { Json pinfo; PackageSupplier supplier; foreach(ps; m_packageSuppliers){ try { - pinfo = ps.fetchPackageRecipe(basePackageName, range, (options & FetchOptions.usePrerelease) != 0); + pinfo = ps.fetchPackageRecipe(name.main, range, (options & FetchOptions.usePrerelease) != 0); if (pinfo.type == Json.Type.null_) continue; supplier = ps; break; } catch(Exception e) { - logWarn("Package %s not found for %s: %s", packageId, ps.description, e.msg); + logWarn("Package %s not found for %s: %s", name, ps.description, e.msg); logDebug("Full error: %s", e.toString().sanitize()); } } enforce(!pinfo.type.among(Json.Type.undefined, Json.Type.null_), - "No package " ~ packageId ~ " was found matching the dependency " ~ range.toString()); + "No package %s was found matching the dependency %s" + .format(name, range)); Version ver = Version(pinfo["version"].get!string); // always upgrade branch based versions - TODO: actually check if there is a new commit available - Package existing = m_packageManager.getPackage(packageId, ver, location); + Package existing = m_packageManager.getPackage(name, ver, location); if (options & FetchOptions.printOnly) { if (existing && existing.version_ != ver) logInfo("A new version for %s is available (%s -> %s). Run \"%s\" to switch.", - packageId.color(Mode.bold), existing, ver, - text("dub upgrade ", packageId).color(Mode.bold)); + name.toString().color(Mode.bold), existing, ver, + text("dub upgrade ", name.main).color(Mode.bold)); return null; } @@ -931,16 +1000,18 @@ if (!ver.isBranch() || !(options & FetchOptions.forceBranchUpgrade) || location == PlacementLocation.local) { // TODO: support git working trees by performing a "git pull" instead of this logDiagnostic("Package %s %s (in %s packages) is already present with the latest version, skipping upgrade.", - packageId, ver, location.toString); + name, ver, location.toString); return existing; } else { - logInfo("Removing", Color.yellow, "%s %s to prepare replacement with a new version", packageId.color(Mode.bold), ver); + logInfo("Removing", Color.yellow, "%s %s to prepare " ~ + "replacement with a new version", name.toString().color(Mode.bold), + ver); if (!m_dryRun) m_packageManager.remove(existing); } } - if (reason.length) logInfo("Fetching", Color.yellow, "%s %s (%s)", packageId.color(Mode.bold), ver, reason); - else logInfo("Fetching", Color.yellow, "%s %s", packageId.color(Mode.bold), ver); + if (reason.length) logInfo("Fetching", Color.yellow, "%s %s (%s)", name.toString().color(Mode.bold), ver, reason); + else logInfo("Fetching", Color.yellow, "%s %s", name.toString().color(Mode.bold), ver); if (m_dryRun) return null; logDebug("Acquiring package zip file"); @@ -950,15 +1021,13 @@ { import std.zip : ZipException; - auto path = getTempFile(basePackageName, ".zip"); - supplier.fetchPackage(path, basePackageName, range, (options & FetchOptions.usePrerelease) != 0); // Q: continue on fail? - scope(exit) removeFile(path); + auto data = supplier.fetchPackage(name.main, range, (options & FetchOptions.usePrerelease) != 0); // Q: continue on fail? logDiagnostic("Placing to %s...", location.toString()); try { - return m_packageManager.store(path, location, basePackageName, ver); + return m_packageManager.store(data, location, name.main, ver); } catch (ZipException e) { - logInfo("Failed to extract zip archive for %s %s...", packageId, ver); + logInfo("Failed to extract zip archive for %s@%s...", name, ver); // re-throw the exception at the end of the loop if (i == 0) throw e; @@ -998,15 +1067,17 @@ is set to `RemoveVersionWildcard`. Params: - package_id = Name of the package to be removed + name = Name of the package to be removed location = Specifies the location to look for the given package name/version. resolve_version = Callback to select package version. */ - void remove(string package_id, PlacementLocation location, - scope size_t delegate(in Package[] packages) resolve_version) + void remove(in PackageName name, PlacementLocation location, + scope size_t delegate(in Package[] packages) resolve_version) { - enforce(!package_id.empty); + enforce(name.main.toString().length); + enforce(!name.sub.length, "Cannot remove subpackage %s, remove %s instead" + .format(name, name.main)); if (location == PlacementLocation.local) { logInfo("To remove a locally placed package, make sure you don't have any data" ~ "\nleft in it's directory and then simply remove the whole directory."); @@ -1016,16 +1087,13 @@ Package[] packages; // Retrieve packages to be removed. - foreach(pack; m_packageManager.getPackageIterator(package_id)) + foreach(pack; m_packageManager.getPackageIterator(name.toString())) if (m_packageManager.isManagedPackage(pack)) packages ~= pack; // Check validity of packages to be removed. - if(packages.empty) { - throw new Exception("Cannot find package to remove. (" - ~ "id: '" ~ package_id ~ "', location: '" ~ to!string(location) ~ "'" - ~ ")"); - } + enforce(!packages.empty, "Cannot find package '%s' to remove at %s location" + .format(name, location.toString())); // Sort package list in ascending version order packages.sort!((a, b) => a.version_ < b.version_); @@ -1041,12 +1109,19 @@ try { remove(pack); } catch (Exception e) { - logError("Failed to remove %s %s: %s", package_id, pack, e.msg); + logError("Failed to remove %s %s: %s", name, pack, e.msg); logInfo("Continuing with other packages (if any)."); } } } + deprecated("Use `remove(PackageName, PlacementLocation, delegate)`") + void remove(string name, PlacementLocation location, + scope size_t delegate(in Package[] packages) resolve_version) + { + this.remove(PackageName(name), location, resolve_version); + } + /// Compatibility overload. Use the version without a `force_remove` argument instead. deprecated("Use the overload without the 3rd argument (`force_remove`) instead") void remove(string package_id, PlacementLocation location, bool force_remove, @@ -1058,7 +1133,7 @@ /** Removes a specific version of a package. Params: - package_id = Name of the package to be removed + name = Name of the package to be removed version_ = Identifying a version or a wild card. If an empty string is passed, the package will be removed from the location, if there is only one version retrieved. This will throw an @@ -1066,9 +1141,9 @@ location = Specifies the location to look for the given package name/version. */ - void remove(string package_id, string version_, PlacementLocation location) + void remove(in PackageName name, string version_, PlacementLocation location) { - remove(package_id, location, (in packages) { + remove(name, location, (in packages) { if (version_ == RemoveVersionWildcard || version_.empty) return packages.length; @@ -1076,12 +1151,17 @@ if (p.version_ == Version(version_)) return i; } - throw new Exception("Cannot find package to remove. (" - ~ "id: '" ~ package_id ~ "', version: '" ~ version_ ~ "', location: '" ~ to!string(location) ~ "'" - ~ ")"); + throw new Exception("Cannot find package '%s@%s' to remove at %s location" + .format(name, version_, location.toString())); }); } + deprecated("Use `remove(PackageName, string, PlacementLocation)`") + void remove(string name, string version_, PlacementLocation location) + { + this.remove(PackageName(name), version_, location); + } + /// Compatibility overload. Use the version without a `force_remove` argument instead. deprecated("Use the overload without force_remove instead") void remove(string package_id, string version_, PlacementLocation location, bool force_remove) @@ -1102,10 +1182,17 @@ See_Also: `removeLocalPackage` */ + deprecated("Use `addLocalPackage(string, string, PlacementLocation)` instead") void addLocalPackage(string path, string ver, bool system) { + this.addLocalPackage(path, ver, system ? PlacementLocation.system : PlacementLocation.user); + } + + /// Ditto + void addLocalPackage(string path, string ver, PlacementLocation loc) + { if (m_dryRun) return; - m_packageManager.addLocalPackage(makeAbsolute(path), ver, system ? PlacementLocation.system : PlacementLocation.user); + this.m_packageManager.addLocalPackage(makeAbsolute(path), ver, loc); } /** Removes a directory from the list of locally known packages. @@ -1119,10 +1206,17 @@ See_Also: `addLocalPackage` */ + deprecated("Use `removeLocalPackage(string, string, PlacementLocation)` instead") void removeLocalPackage(string path, bool system) { + this.removeLocalPackage(path, system ? PlacementLocation.system : PlacementLocation.user); + } + + /// Ditto + void removeLocalPackage(string path, PlacementLocation loc) + { if (m_dryRun) return; - m_packageManager.removeLocalPackage(makeAbsolute(path), system ? PlacementLocation.system : PlacementLocation.user); + this.m_packageManager.removeLocalPackage(makeAbsolute(path), loc); } /** Registers a local directory to search for packages to use for satisfying @@ -1135,10 +1229,17 @@ See_Also: `removeSearchPath` */ + deprecated("Use `addSearchPath(string, PlacementLocation)` instead") void addSearchPath(string path, bool system) { + this.addSearchPath(path, system ? PlacementLocation.system : PlacementLocation.user); + } + + /// Ditto + void addSearchPath(string path, PlacementLocation loc) + { if (m_dryRun) return; - m_packageManager.addSearchPath(makeAbsolute(path), system ? PlacementLocation.system : PlacementLocation.user); + this.m_packageManager.addSearchPath(makeAbsolute(path), loc); } /** Deregisters a local directory search path. @@ -1150,10 +1251,17 @@ See_Also: `addSearchPath` */ + deprecated("Use `removeSearchPath(string, PlacementLocation)` instead") void removeSearchPath(string path, bool system) { + this.removeSearchPath(path, system ? PlacementLocation.system : PlacementLocation.user); + } + + /// Ditto + void removeSearchPath(string path, PlacementLocation loc) + { if (m_dryRun) return; - m_packageManager.removeSearchPath(makeAbsolute(path), system ? PlacementLocation.system : PlacementLocation.user); + this.m_packageManager.removeSearchPath(makeAbsolute(path), loc); } /** Queries all package suppliers with the given query string. @@ -1189,12 +1297,11 @@ See_also: `getLatestVersion` */ - Version[] listPackageVersions(string name) + Version[] listPackageVersions(in PackageName name) { Version[] versions; - auto basePackageName = getBasePackageName(name); foreach (ps; this.m_packageSuppliers) { - try versions ~= ps.getVersions(basePackageName); + try versions ~= ps.getVersions(name); catch (Exception e) { logWarn("Failed to get versions for package %s on provider %s: %s", name, ps.description, e.msg); } @@ -1202,6 +1309,13 @@ return versions.sort().uniq.array; } + deprecated("Use `listPackageVersions(PackageName)`") + Version[] listPackageVersions(string name) + { + const n = PackageName(name); + return this.listPackageVersions(n); + } + /** Returns the latest available version for a particular package. This function returns the latest numbered version of a package. If no @@ -1209,21 +1323,30 @@ preferring "~master". Params: - package_name = The name of the package in question. + name = The name of the package in question. prefer_stable = If set to `true` (the default), returns the latest stable version, even if there are newer pre-release versions. See_also: `listPackageVersions` */ - Version getLatestVersion(string package_name, bool prefer_stable = true) + Version getLatestVersion(in PackageName name, bool prefer_stable = true) { - auto vers = listPackageVersions(package_name); - enforce(!vers.empty, "Failed to find any valid versions for a package name of '"~package_name~"'."); + auto vers = this.listPackageVersions(name); + enforce(!vers.empty, + "Failed to find any valid versions for a package name of '%s'." + .format(name)); auto final_versions = vers.filter!(v => !v.isBranch && !v.isPreRelease).array; if (prefer_stable && final_versions.length) return final_versions[$-1]; else return vers[$-1]; } + deprecated("Use `getLatestVersion(PackageName, bool)`") + Version getLatestVersion(string name, bool prefer_stable = true) + { + const n = PackageName(name); + return this.getLatestVersion(n, prefer_stable); + } + /** Initializes a directory with a package skeleton. Params: @@ -1247,8 +1370,9 @@ VersionRange[string] depVers; string[] notFound; // keep track of any failed packages in here foreach (dep; deps) { + const name = PackageName(dep); try { - Version ver = getLatestVersion(dep); + Version ver = this.getLatestVersion(name); if (ver.isBranch()) depVers[dep] = VersionRange(ver); else @@ -1277,13 +1401,19 @@ logInfo("Success", Color.green, "created empty project in %s", path.toNativeString().color(Mode.bold)); } - private void runCustomInitialization(NativePath path, string type, string[] runArgs) + /** + * Run initialization code from a template project + * + * Looks up a project, then get its `init-exec` subpackage, + * and run this to initialize the repository with a default structure. + */ + private void runCustomInitialization(NativePath path, string name, string[] runArgs) { - string packageName = type; - auto template_pack = m_packageManager.getBestPackage(packageName); + auto name_ = PackageName(name); + auto template_pack = m_packageManager.getBestPackage(name_); if (!template_pack) { - logInfo("%s is not present, getting and storing it user wide", packageName); - template_pack = fetch(packageName, VersionRange.Any, defaultPlacementLocation, FetchOptions.none); + logInfo("%s is not present, getting and storing it locally", name); + template_pack = fetch(name_); } Package initSubPackage = m_packageManager.getSubPackage(template_pack, "init-exec", false); @@ -1344,13 +1474,14 @@ if (m_dryRun) return; // allow to choose a custom ddox tool - auto tool = m_project.rootPackage.recipe.ddoxTool; - if (tool.empty) tool = "ddox"; + auto tool = m_project.rootPackage.recipe.ddoxTool.empty + ? PackageName("ddox") + : PackageName(m_project.rootPackage.recipe.ddoxTool); auto tool_pack = m_packageManager.getBestPackage(tool); if (!tool_pack) { logInfo("%s is not present, getting and storing it user wide", tool); - tool_pack = fetch(tool, VersionRange.Any, defaultPlacementLocation, FetchOptions.none); + tool_pack = this.fetch(tool); } auto ddox_dub = new Dub(null, m_packageSuppliers); @@ -1515,7 +1646,7 @@ import dub.test.base : TestDub; import std.path: buildPath, absolutePath; - auto dub = new TestDub(".", null, SkipPackageSuppliers.configured); + auto dub = new TestDub(null, ".", null, SkipPackageSuppliers.configured); immutable olddc = environment.get("DC", null); immutable oldpath = environment.get("PATH", null); immutable testdir = "test-determineDefaultCompiler"; @@ -1594,11 +1725,11 @@ protected { Dub m_dub; UpgradeOptions m_options; - Dependency[][string] m_packageVersions; + Dependency[][PackageName] m_packageVersions; Package[string] m_remotePackages; SelectedVersions m_selectedVersions; Package m_rootPackage; - bool[string] m_packagesToUpgrade; + bool[PackageName] m_packagesToUpgrade; Package[PackageDependency] m_packages; TreeNodes[][TreeNode] m_children; } @@ -1621,20 +1752,20 @@ m_selectedVersions = selected_versions; } - Dependency[string] resolve(string[] filter) + Dependency[PackageName] resolve(string[] filter) { foreach (name; filter) - m_packagesToUpgrade[name] = true; - return super.resolve(TreeNode(m_rootPackage.name, Dependency(m_rootPackage.version_)), + m_packagesToUpgrade[PackageName(name)] = true; + return super.resolve(TreeNode(PackageName(m_rootPackage.name), Dependency(m_rootPackage.version_)), (m_options & UpgradeOptions.dryRun) == 0); } - protected bool isFixedPackage(string pack) + protected bool isFixedPackage(in PackageName pack) { return m_packagesToUpgrade !is null && pack !in m_packagesToUpgrade; } - protected override Dependency[] getAllConfigs(string pack) + protected override Dependency[] getAllConfigs(in PackageName pack) { if (auto pvers = pack in m_packageVersions) return *pvers; @@ -1648,7 +1779,7 @@ logDiagnostic("Search for versions of %s (%s package suppliers)", pack, m_dub.m_packageSuppliers.length); Version[] versions; - foreach (p; m_dub.packageManager.getPackageIterator(pack)) + foreach (p; m_dub.packageManager.getPackageIterator(pack.toString())) versions ~= p.version_; foreach (ps; m_dub.m_packageSuppliers) { @@ -1690,7 +1821,7 @@ return ret; } - protected override Dependency[] getSpecificConfigs(string pack, TreeNodes nodes) + protected override Dependency[] getSpecificConfigs(in PackageName pack, TreeNodes nodes) { if (!nodes.configs.path.empty || !nodes.configs.repository.empty) { if (getPackage(nodes.pack, nodes.configs)) return [nodes.configs]; @@ -1722,18 +1853,18 @@ auto basepack = pack.basePackage; foreach (d; pack.getAllDependenciesRange()) { - auto dbasename = getBasePackageName(d.name); + auto dbasename = d.name.main.toString(); // detect dependencies to the root package (or sub packages thereof) if (dbasename == basepack.name) { auto absdeppath = d.spec.mapToPath(pack.path).path; absdeppath.endsWithSlash = true; - auto subpack = m_dub.m_packageManager.getSubPackage(basepack, getSubPackageName(d.name), true); + auto subpack = m_dub.m_packageManager.getSubPackage(basepack, d.name.sub, true); if (subpack) { auto desireddeppath = basepack.path; desireddeppath.endsWithSlash = true; - auto altdeppath = d.name == dbasename ? basepack.path : subpack.path; + auto altdeppath = d.name == d.name.main ? basepack.path : subpack.path; altdeppath.endsWithSlash = true; if (!d.spec.path.empty && absdeppath != desireddeppath) @@ -1757,14 +1888,14 @@ Dependency dspec = d.spec.mapToPath(pack.path); // if not upgrading, use the selected version - if (!(m_options & UpgradeOptions.upgrade) && m_selectedVersions.hasSelectedVersion(dbasename)) - dspec = m_selectedVersions.getSelectedVersion(dbasename); + if (!(m_options & UpgradeOptions.upgrade) && m_selectedVersions.hasSelectedVersion(d.name.main)) + dspec = m_selectedVersions.getSelectedVersion(d.name.main); // keep selected optional dependencies and avoid non-selected optional-default dependencies by default if (!m_selectedVersions.bare) { - if (dt == DependencyType.optionalDefault && !m_selectedVersions.hasSelectedVersion(dbasename)) + if (dt == DependencyType.optionalDefault && !m_selectedVersions.hasSelectedVersion(d.name.main)) dt = DependencyType.optional; - else if (dt == DependencyType.optional && m_selectedVersions.hasSelectedVersion(dbasename)) + else if (dt == DependencyType.optional && m_selectedVersions.hasSelectedVersion(d.name.main)) dt = DependencyType.optionalDefault; } @@ -1779,7 +1910,7 @@ return configs.merge(config).valid; } - private Package getPackage(string name, Dependency dep) + private Package getPackage(PackageName name, Dependency dep) { auto key = PackageDependency(name, dep); if (auto pp = key in m_packages) @@ -1789,42 +1920,30 @@ return p; } - private Package getPackageRaw(string name, Dependency dep) + private Package getPackageRaw(in PackageName name, Dependency dep) { - auto basename = getBasePackageName(name); + import dub.recipe.json; // for sub packages, first try to get them from the base package - if (basename != name) { - auto subname = getSubPackageName(name); - auto basepack = getPackage(basename, dep); + if (name.main != name) { + auto subname = name.sub; + auto basepack = getPackage(name.main, dep); if (!basepack) return null; - if (auto sp = m_dub.m_packageManager.getSubPackage(basepack, subname, true)) { + if (auto sp = m_dub.m_packageManager.getSubPackage(basepack, subname, true)) return sp; - } else if (!basepack.subPackages.canFind!(p => p.path.length)) { - // note: external sub packages are handled further below - auto spr = basepack.getInternalSubPackage(subname); - if (!spr.isNull) { - auto sp = new Package(spr.get, basepack.path, basepack); - m_remotePackages[sp.name] = sp; - return sp; - } else { - logDiagnostic("Sub package %s doesn't exist in %s %s.", name, basename, dep); - return null; - } - } else { - logDiagnostic("External sub package %s %s not found.", name, dep); - return null; - } + logDiagnostic("Subpackage %s@%s not found.", name, dep); + return null; } // shortcut if the referenced package is the root package - if (basename == m_rootPackage.basePackage.name) + if (name.main.toString() == m_rootPackage.basePackage.name) return m_rootPackage.basePackage; if (!dep.repository.empty) { auto ret = m_dub.packageManager.loadSCMPackage(name, dep.repository); return ret !is null && dep.matches(ret.version_) ? ret : null; - } else if (!dep.path.empty) { + } + if (!dep.path.empty) { try { return m_dub.packageManager.getOrLoadPackage(dep.path); } catch (Exception e) { @@ -1838,21 +1957,21 @@ if (auto ret = m_dub.m_packageManager.getBestPackage(name, vers)) return ret; - auto key = name ~ ":" ~ vers.toString(); + auto key = name.toString() ~ ":" ~ vers.toString(); if (auto ret = key in m_remotePackages) return *ret; auto prerelease = (m_options & UpgradeOptions.preRelease) != 0; - auto rootpack = name.split(":")[0]; - foreach (ps; m_dub.m_packageSuppliers) { - if (rootpack == name) { + if (name.main == name) { try { auto desc = ps.fetchPackageRecipe(name, VersionRange(vers, vers), prerelease); if (desc.type == Json.Type.null_) continue; - auto ret = new Package(desc); + PackageRecipe recipe; + parseJson(recipe, desc); + auto ret = new Package(recipe); m_remotePackages[key] = ret; return ret; } catch (Exception e) { @@ -1864,16 +1983,16 @@ try { FetchOptions fetchOpts; fetchOpts |= prerelease ? FetchOptions.usePrerelease : FetchOptions.none; - m_dub.fetch(rootpack, vers, m_dub.defaultPlacementLocation, fetchOpts, "need sub package description"); + m_dub.fetch(name.main, vers, fetchOpts, m_dub.defaultPlacementLocation, "need sub package description"); auto ret = m_dub.m_packageManager.getBestPackage(name, vers); if (!ret) { - logWarn("Package %s %s doesn't have a sub package %s", rootpack, dep, name); + logWarn("Package %s %s doesn't have a sub package %s", name.main, dep, name); return null; } m_remotePackages[key] = ret; return ret; } catch (Exception e) { - logDiagnostic("Package %s could not be downloaded from %s: %s", rootpack, ps.description, e.msg); + logDiagnostic("Package %s could not be downloaded from %s: %s", name.main, ps.description, e.msg); logDebug("Full error: %s", e.toString().sanitize); } } diff --git a/source/dub/generators/build.d b/source/dub/generators/build.d index ed0f5f1..c893fb0 100644 --- a/source/dub/generators/build.d +++ b/source/dub/generators/build.d @@ -216,7 +216,7 @@ auto cwd = settings.toolWorkingDirectory; bool generate_binary = !(buildsettings.options & BuildOption.syntaxOnly); - auto build_id = buildsettings.computeBuildID(config, settings); + auto build_id = buildsettings.computeBuildID(pack.path, config, settings); // make all paths relative to shrink the command line string makeRelative(string path) { return shrinkPath(NativePath(path), cwd); } @@ -321,7 +321,7 @@ const dbPathStr = dbPath.toNativeString(); Json db; if (exists(dbPathStr)) { - const text = stripUTF8Bom(cast(string)readFile(dbPath)); + const text = readText(dbPath); db = parseJsonString(text, dbPathStr); enforce(db.type == Json.Type.array, "Expected a JSON array in " ~ dbPathStr); } diff --git a/source/dub/generators/generator.d b/source/dub/generators/generator.d index 88d719e..7402438 100644 --- a/source/dub/generators/generator.d +++ b/source/dub/generators/generator.d @@ -828,7 +828,8 @@ * library-debug-Z7qINYX4IxM8muBSlyNGrw * ``` */ -package(dub) string computeBuildID(in BuildSettings buildsettings, string config, GeneratorSettings settings) +package(dub) string computeBuildID(in BuildSettings buildsettings, + in NativePath packagePath, string config, GeneratorSettings settings) { import std.conv : to; @@ -843,6 +844,8 @@ settings.platform.architecture, [ (cast(uint)(buildsettings.options & ~BuildOption.color)).to!string, // exclude color option from id + // Needed for things such as `__FULL_FILE_PATH__` + packagePath.toNativeString(), settings.platform.compilerBinary, settings.platform.compiler, settings.platform.compilerVersion, diff --git a/source/dub/generators/targetdescription.d b/source/dub/generators/targetdescription.d index 077126e..646fa1e 100644 --- a/source/dub/generators/targetdescription.d +++ b/source/dub/generators/targetdescription.d @@ -45,7 +45,7 @@ d.packages = ti.packages.map!(p => p.name).array; d.rootConfiguration = ti.config; d.buildSettings = ti.buildSettings.dup; - const buildId = computeBuildID(d.buildSettings, ti.config, settings); + const buildId = computeBuildID(d.buildSettings, ti.pack.path, ti.config, settings); const filename = settings.compiler.getTargetFileName(d.buildSettings, settings.platform); d.cacheArtifactPath = (targetCacheDir(settings.cache, ti.pack, buildId) ~ filename).toNativeString(); d.dependencies = ti.dependencies.dup; diff --git a/source/dub/internal/configy/Attributes.d b/source/dub/internal/configy/Attributes.d index 7829064..474d33a 100644 --- a/source/dub/internal/configy/Attributes.d +++ b/source/dub/internal/configy/Attributes.d @@ -289,6 +289,19 @@ return Converter!RType(func); } +/******************************************************************************* + + Interface that is passed to `fromYAML` hook + + The `ConfigParser` exposes the raw YAML node (`see `node` method), + the path within the file (`path` method), and a simple ability to recurse + via `parseAs`. + + Params: + T = The type of the structure which defines a `fromYAML` hook + +*******************************************************************************/ + public interface ConfigParser (T) { import dub.internal.dyaml.node; @@ -301,7 +314,20 @@ /// Returns: current location we are parsing public string path () const @safe pure nothrow @nogc; - /// + /*************************************************************************** + + Parse this struct as another type + + This allows implementing union-like behavior, where a `struct` which + implements `fromYAML` can parse a simple representation as one type, + and one more advanced as another type. + + Params: + OtherType = The type to parse as + defaultValue = The instance to use as a default value for fields + + ***************************************************************************/ + public final auto parseAs (OtherType) (auto ref OtherType defaultValue = OtherType.init) { diff --git a/source/dub/internal/configy/Exceptions.d b/source/dub/internal/configy/Exceptions.d index 00a6349..7209df3 100644 --- a/source/dub/internal/configy/Exceptions.d +++ b/source/dub/internal/configy/Exceptions.d @@ -380,3 +380,38 @@ sink(this.next.message); } } + +/// Thrown when an array read from config does not match a static array size +public class ArrayLengthException : ConfigException +{ + private size_t actual; + private size_t expected; + + /// Constructor + public this (size_t actual, size_t expected, + string path, string key, Mark position, + string file = __FILE__, size_t line = __LINE__) + @safe pure nothrow @nogc + { + assert(actual != expected); + this.actual = actual; + this.expected = expected; + super(path, key, position, file, line); + } + + /// Format the message with or without colors + protected override void formatMessage ( + scope SinkType sink, in FormatSpec!char spec) + const scope @trusted + { + import core.internal.string : unsignedToTempString; + + char[20] buffer = void; + sink("Too "); + sink((this.actual > this.expected) ? "many" : "few"); + sink(" entries for sequence: Expected "); + sink(unsignedToTempString(this.expected, buffer)); + sink(", got "); + sink(unsignedToTempString(this.actual, buffer)); + } +} diff --git a/source/dub/internal/configy/FieldRef.d b/source/dub/internal/configy/FieldRef.d index 1af4b67..1c8ce8d 100644 --- a/source/dub/internal/configy/FieldRef.d +++ b/source/dub/internal/configy/FieldRef.d @@ -73,11 +73,20 @@ /// Evaluates to `true` if this field is to be considered optional /// (does not need to be present in the YAML document) - public enum Optional = forceOptional || - hasUDA!(Ref, CAOptional) || - is(immutable(Type) == immutable(bool)) || - is(Type : SetInfo!FT, FT) || - (Default != Type.init); + static if (forceOptional || hasUDA!(Ref, CAOptional)) + public enum Optional = true; + // Booleans are always optional + else static if (is(immutable(Type) == immutable(bool))) + public enum Optional = true; + // A mandatory SetInfo would not make sense + else static if (is(Type : SetInfo!FT, FT)) + public enum Optional = true; + // Use `is` to avoid calling `opEquals` which might not be CTFEable, + // except for static arrays as that triggers a deprecation warning. + else static if (is(Type : E[k], E, size_t k)) + public enum Optional = (Default[] !is Type.init[]); + else + public enum Optional = (Default !is Type.init); } unittest @@ -118,7 +127,14 @@ static assert(FieldRefTuple!(FieldRefTuple!(Config3)[0].Type)[1].Name == "notStr2"); } -/// A pseudo `FieldRef` used for structs which are not fields (top-level) +/** + * A pseudo `FieldRef` used for structs which are not fields (top-level) + * + * Params: + * ST = Type for which this pseudo-FieldRef is + * DefaultName = A name to give to this FieldRef, default to `null`, + * but required to prevent forward references in `parseAs`. + */ package template StructFieldRef (ST, string DefaultName = null) { /// diff --git a/source/dub/internal/configy/Read.d b/source/dub/internal/configy/Read.d index 44afcf1..71f877f 100644 --- a/source/dub/internal/configy/Read.d +++ b/source/dub/internal/configy/Read.d @@ -21,14 +21,17 @@ To mark a field as optional even with its default value, use the `Optional` UDA: `@Optional int count = 0;`. - Converter: - Because config structs may contain complex types such as - a Phobos type, a user-defined `Amount`, or Vibe.d's `URL`, - one may need to apply a converter to a struct's field. - Converters are functions that take a YAML `Node` as argument - and return a type that is implicitly convertible to the field type - (usually just the field type). They offer the most power to users, - as they can inspect the YAML structure, but should be used as a last resort. + fromYAML: + Because config structs may contain complex types outside of the project's + control (e.g. a Phobos type, Vibe.d's `URL`, etc...) or one may want + the config format to be more dynamic (e.g. by exposing union-like behavior), + one may need to apply more custom logic than what Configy does. + For this use case, one can define a `fromYAML` static method in the type: + `static S fromYAML(scope ConfigParser!S parser)`, where `S` is the type of + the enclosing structure. Structs with `fromYAML` will have this method + called instead of going through the normal parsing rules. + The `ConfigParser` exposes the current path of the field, as well as the + raw YAML `Node` itself, allowing for maximum flexibility. Composite_Types: Processing starts from a `struct` at the top level, and recurse into @@ -400,8 +403,8 @@ fullyQualifiedName!T, strict == StrictMode.Warn ? strict.paint(Yellow) : strict.paintIf(!!strict, Green, Red)); - return node.parseMapping!(StructFieldRef!T)( - null, T.init, const(Context)(cmdln, strict), null); + return node.parseField!(StructFieldRef!T)( + null, T.init, const(Context)(cmdln, strict)); case NodeID.sequence: case NodeID.scalar: case NodeID.invalid: @@ -791,13 +794,28 @@ if (node.nodeID != NodeID.sequence) throw new TypeConfigException(node, "sequence (array)", path); + typeof(return) validateLength (E[] res) + { + static if (is(FR.Type : E_[k], E_, size_t k)) + { + if (res.length != k) + throw new ArrayLengthException( + res.length, k, path, null, node.startMark()); + return res[0 .. k]; + } + else + return res; + } + // We pass `E.init` as default value as it is not going to be used: // Either there is something in the YAML document, and that will be // converted, or `sequence` will not iterate. - return node.sequence.enumerate.map!( + return validateLength( + node.sequence.enumerate.map!( kv => kv.value.parseField!(NestedFieldRef!(E, FR))( format("%s[%s]", path, kv.index), E.init, ctx)) - .array(); + .array() + ); } else { diff --git a/source/dub/internal/configy/Test.d b/source/dub/internal/configy/Test.d index a334b6d..bab92c1 100644 --- a/source/dub/internal/configy/Test.d +++ b/source/dub/internal/configy/Test.d @@ -692,3 +692,238 @@ catch (Exception exc) assert(exc.toString() == `/dev/null(2:6): data.array[0]: Parsing failed!`); } + +/// Test for error message: Has to be versioned out, uncomment to check manually +unittest +{ + static struct Nested + { + int field1; + + private this (string arg) {} + } + + static struct Config + { + Nested nested; + } + + static struct Config2 + { + Nested nested; + alias nested this; + } + + version(none) auto c1 = parseConfigString!Config(null, null); + version(none) auto c2 = parseConfigString!Config2(null, null); +} + +/// Test support for `fromYAML` hook +unittest +{ + static struct PackageDef + { + string name; + @Optional string target; + int build = 42; + } + + static struct Package + { + string path; + PackageDef def; + + public static Package fromYAML (scope ConfigParser!Package parser) + { + if (parser.node.nodeID == NodeID.mapping) + return Package(null, parser.parseAs!PackageDef); + else + return Package(parser.parseAs!string); + } + } + + static struct Config + { + string name; + Package[] deps; + } + + auto c = parseConfigString!Config( +` +name: myPkg +deps: + - /foo/bar + - name: foo + target: bar + build: 24 + - name: fur + - /one/last/path +`, "/dev/null"); + assert(c.name == "myPkg"); + assert(c.deps.length == 4); + assert(c.deps[0] == Package("/foo/bar")); + assert(c.deps[1] == Package(null, PackageDef("foo", "bar", 24))); + assert(c.deps[2] == Package(null, PackageDef("fur", null, 42))); + assert(c.deps[3] == Package("/one/last/path")); +} + +/// Test top level hook (fromYAML / fromString) +unittest +{ + static struct Version1 { + uint fileVersion; + uint value; + } + + static struct Version2 { + uint fileVersion; + string str; + } + + static struct Config + { + uint fileVersion; + union { + Version1 v1; + Version2 v2; + } + static Config fromYAML (scope ConfigParser!Config parser) + { + static struct OnlyVersion { uint fileVersion; } + auto vers = parseConfig!OnlyVersion( + CLIArgs.init, parser.node, StrictMode.Ignore); + switch (vers.fileVersion) { + case 1: + return Config(1, parser.parseAs!Version1); + case 2: + Config conf = Config(2); + conf.v2 = parser.parseAs!Version2; + return conf; + default: + assert(0); + } + } + } + + auto v1 = parseConfigString!Config("fileVersion: 1\nvalue: 42", "/dev/null"); + auto v2 = parseConfigString!Config("fileVersion: 2\nstr: hello world", "/dev/null"); + + assert(v1.fileVersion == 1); + assert(v1.v1.fileVersion == 1); + assert(v1.v1.value == 42); + + assert(v2.fileVersion == 2); + assert(v2.v2.fileVersion == 2); + assert(v2.v2.str == "hello world"); +} + +/// Don't call `opCmp` / `opEquals` as they might not be CTFEable +/// Also various tests around static arrays +unittest +{ + static struct NonCTFEAble + { + int value; + + public bool opEquals (const NonCTFEAble other) const scope + { + assert(0); + } + + public bool opEquals (const ref NonCTFEAble other) const scope + { + assert(0); + } + + public int opCmp (const NonCTFEAble other) const scope + { + assert(0); + } + + public int opCmp (const ref NonCTFEAble other) const scope + { + assert(0); + } + } + + static struct Config + { + NonCTFEAble fixed; + @Name("static") NonCTFEAble[3] static_; + NonCTFEAble[] dynamic; + } + + auto c = parseConfigString!Config(`fixed: + value: 42 +static: + - value: 84 + - value: 126 + - value: 168 +dynamic: + - value: 420 + - value: 840 +`, "/dev/null"); + + assert(c.fixed.value == 42); + assert(c.static_[0].value == 84); + assert(c.static_[1].value == 126); + assert(c.static_[2].value == 168); + assert(c.dynamic.length == 2); + assert(c.dynamic[0].value == 420); + assert(c.dynamic[1].value == 840); + + try parseConfigString!Config(`fixed: + value: 42 +dynamic: + - value: 420 + - value: 840 +`, "/dev/null"); + catch (ConfigException e) + assert(e.toString() == "/dev/null(0:0): static: Required key was not found in configuration or command line arguments"); + + try parseConfigString!Config(`fixed: + value: 42 +static: + - value: 1 + - value: 2 +dynamic: + - value: 420 + - value: 840 +`, "/dev/null"); + catch (ConfigException e) + assert(e.toString() == "/dev/null(3:2): static: Too few entries for sequence: Expected 3, got 2"); + + try parseConfigString!Config(`fixed: + value: 42 +static: + - value: 1 + - value: 2 + - value: 3 + - value: 4 +dynamic: + - value: 420 + - value: 840 +`, "/dev/null"); + catch (ConfigException e) + assert(e.toString() == "/dev/null(3:2): static: Too many entries for sequence: Expected 3, got 4"); + + // Check that optional static array work + static struct ConfigOpt + { + NonCTFEAble fixed; + @Name("static") NonCTFEAble[3] static_ = [ + NonCTFEAble(69), + NonCTFEAble(70), + NonCTFEAble(71), + ]; + } + + auto c1 = parseConfigString!ConfigOpt(`fixed: + value: 1100 +`, "/dev/null"); + + assert(c1.fixed.value == 1100); + assert(c1.static_[0].value == 69); + assert(c1.static_[1].value == 70); + assert(c1.static_[2].value == 71); +} diff --git a/source/dub/internal/tinyendian.d b/source/dub/internal/tinyendian.d index d9b227a..a6e9681 100644 --- a/source/dub/internal/tinyendian.d +++ b/source/dub/internal/tinyendian.d @@ -122,7 +122,7 @@ static immutable Endian[5] bomEndian = [ endian, Endian.littleEndian, Endian.bigEndian, - Endian.littleEndian, + Endian.littleEndian, Endian.bigEndian ]; // Documented in function ddoc. diff --git a/source/dub/internal/utils.d b/source/dub/internal/utils.d index b9109cc..d0026c9 100644 --- a/source/dub/internal/utils.d +++ b/source/dub/internal/utils.d @@ -92,7 +92,7 @@ Json jsonFromFile(NativePath file, bool silent_fail = false) { if( silent_fail && !existsFile(file) ) return Json.emptyObject; - auto text = stripUTF8Bom(cast(string)readFile(file)); + auto text = readText(file); return parseJsonString(text, file.toNativeString()); } @@ -207,7 +207,7 @@ Note: Timeouts are only implemented when curl is used (DubUseCurl). */ -void download(string url, string filename, uint timeout = 8) +private void download(string url, string filename, uint timeout = 8) { version(DubUseCurl) { auto conn = HTTP(); @@ -226,18 +226,18 @@ } else assert(false); } /// ditto -void download(URL url, NativePath filename, uint timeout = 8) +private void download(URL url, NativePath filename, uint timeout = 8) { download(url.toString(), filename.toNativeString(), timeout); } /// ditto -ubyte[] download(string url, uint timeout = 8) +private ubyte[] download(string url, uint timeout = 8) { version(DubUseCurl) { auto conn = HTTP(); setupHTTPClient(conn, timeout); logDebug("Getting %s...", url); - return cast(ubyte[])get(url, conn); + return get!(HTTP, ubyte)(url, conn); } else version (Have_vibe_d_http) { import vibe.inet.urltransfer; import vibe.stream.operations; @@ -247,7 +247,7 @@ } else assert(false); } /// ditto -ubyte[] download(URL url, uint timeout = 8) +private ubyte[] download(URL url, uint timeout = 8) { return download(url.toString(), timeout); } @@ -319,13 +319,15 @@ catch(HTTPStatusException e) { if (e.status == 404) throw e; else { - logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount); + logDebug("Failed to download %s (Attempt %s of %s): %s", + url, i + 1, retryCount, e.message); if (i == retryCount - 1) throw e; else continue; } } catch(CurlException e) { - logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount); + logDebug("Failed to download %s (Attempt %s of %s): %s", + url, i + 1, retryCount, e.message); continue; } } @@ -451,7 +453,7 @@ } } -string stripUTF8Bom(string str) +private string stripUTF8Bom(string str) { if( str.length >= 3 && str[0 .. 3] == [0xEF, 0xBB, 0xBF] ) return str[3 ..$]; diff --git a/source/dub/internal/vibecompat/core/file.d b/source/dub/internal/vibecompat/core/file.d index 64faf3b..75a94cf 100644 --- a/source/dub/internal/vibecompat/core/file.d +++ b/source/dub/internal/vibecompat/core/file.d @@ -34,6 +34,12 @@ return cast(ubyte[]) std.file.read(path.toNativeString()); } +/// Returns the content of a file as text +public string readText(NativePath path) +{ + return std.file.readText(path.toNativeString()); +} + /** Moves or renames a file. */ @@ -101,7 +107,7 @@ copyFile(NativePath(from), NativePath(to)); } -version (Windows) extern(Windows) int CreateHardLinkW(in wchar* to, in wchar* from, void* attr=null); +version (Windows) extern(Windows) int CreateHardLinkW(const(wchar)* to, const(wchar)* from, void* attr=null); // guess whether 2 files are identical, ignores filename and content private bool sameFile(NativePath a, NativePath b) @@ -287,9 +293,6 @@ /// Time of the last modification SysTime timeModified; - /// Time of creation (not available on all operating systems/file systems) - SysTime timeCreated; - /// True if this is a symlink to an actual file bool isSymlink; @@ -326,8 +329,6 @@ ret.isDirectory = ent.isDir; ret.size = ent.size; ret.timeModified = ent.timeLastModified; - version(Windows) ret.timeCreated = ent.timeCreated; - else ret.timeCreated = ent.timeLastModified; } catch (Exception e) { logDiagnostic("Failed to get extended file information for %s: %s", ret.name, e.msg); } diff --git a/source/dub/internal/vibecompat/inet/path.d b/source/dub/internal/vibecompat/inet/path.d index 745ebc0..7277e76 100644 --- a/source/dub/internal/vibecompat/inet/path.d +++ b/source/dub/internal/vibecompat/inet/path.d @@ -47,7 +47,7 @@ } /// Constructs a path object from a list of PathEntry objects. - this(immutable(PathEntry)[] nodes, bool absolute) + this(immutable(PathEntry)[] nodes, bool absolute = false) { m_nodes = nodes; m_absolute = absolute; @@ -185,6 +185,8 @@ /// The parent path @property NativePath parentPath() const { return this[0 .. length-1]; } + /// Forward compatibility with vibe-d + @property bool hasParentPath() const { return length > 1; } /// The list of path entries of which this path is composed @property immutable(PathEntry)[] nodes() const { return m_nodes; } diff --git a/source/dub/package_.d b/source/dub/package_.d index 7e2eef9..94d2597 100644 --- a/source/dub/package_.d +++ b/source/dub/package_.d @@ -80,9 +80,10 @@ /** Represents a package, including its sub packages. */ class Package { + // `package` visibility as it is set from the PackageManager + package NativePath m_infoFile; private { NativePath m_path; - NativePath m_infoFile; PackageRecipe m_info; PackageRecipe m_rawRecipe; Package m_parentPackage; @@ -100,6 +101,7 @@ instead of the one declared in the package recipe, or the one determined by invoking the VCS (GIT currently). */ + deprecated("Provide an already parsed PackageRecipe instead of a JSON object") this(Json json_recipe, NativePath root = NativePath(), Package parent = null, string version_override = "") { import dub.recipe.json; @@ -151,6 +153,7 @@ Returns the full path to the package file, if any was found. Otherwise returns an empty path. */ + deprecated("Use `PackageManager.findPackageFile`") static NativePath findPackageFile(NativePath directory) { foreach (file; packageInfoFiles) { @@ -173,6 +176,7 @@ determined by invoking the VCS (GIT currently). mode = Whether to issue errors, warning, or ignore unknown keys in dub.json */ + deprecated("Use `PackageManager.getOrLoadPackage` instead of loading packages directly") static Package load(NativePath root, NativePath recipe_file = NativePath.init, Package parent = null, string version_override = "", StrictMode mode = StrictMode.Ignore) @@ -309,13 +313,7 @@ writeJsonFile(filename, m_info.toJson()); } - /** Returns the package recipe of a non-path-based sub package. - - For sub packages that are declared within the package recipe of the - parent package, this function will return the corresponding recipe. Sub - packages declared using a path must be loaded manually (or using the - `PackageManager`). - */ + deprecated("Use `PackageManager.getSubPackage` instead") Nullable!PackageRecipe getInternalSubPackage(string name) { foreach (ref p; m_info.subPackages) @@ -419,10 +417,6 @@ */ void addBuildTypeSettings(ref BuildSettings settings, in BuildPlatform platform, string build_type) const { - import std.process : environment; - string dflags = environment.get("DFLAGS", ""); - settings.addDFlags(dflags.split()); - if (auto pbt = build_type in m_info.buildTypes) { logDiagnostic("Using custom build type '%s'.", build_type); pbt.getPlatformSettings(settings, platform, this.path); @@ -447,6 +441,11 @@ case "syntax": settings.addOptions(syntaxOnly); break; } } + + // Add environment DFLAGS last so that user specified values are not overriden by us. + import std.process : environment; + string dflags = environment.get("DFLAGS", ""); + settings.addDFlags(dflags.split()); } /** Returns the selected configuration for a certain dependency. @@ -580,7 +579,7 @@ this.recipe.configurations.map!(c => c.buildSettings.dependencies.byKeyValue) ) .joiner() - .map!(d => PackageDependency(d.key, d.value)); + .map!(d => PackageDependency(PackageName(d.key), d.value)); } diff --git a/source/dub/packagemanager.d b/source/dub/packagemanager.d index d7bff62..90eec57 100644 --- a/source/dub/packagemanager.d +++ b/source/dub/packagemanager.d @@ -9,12 +9,13 @@ import dub.dependency; import dub.internal.utils; -import dub.internal.vibecompat.core.file; +import dub.internal.vibecompat.core.file : FileInfo; import dub.internal.vibecompat.data.json; import dub.internal.vibecompat.inet.path; import dub.internal.logging; import dub.package_; import dub.recipe.io; +import dub.recipe.selection; import dub.internal.configy.Exceptions; public import dub.internal.configy.Read : StrictMode; @@ -188,11 +189,11 @@ * Returns: * A `Package` if one was found, `null` if none exists. */ - protected Package lookup (string name, Version vers) { + protected Package lookup (in PackageName name, in Version vers) { if (!this.m_initialized) this.refresh(); - if (auto pkg = this.m_internal.lookup(name, vers, this)) + if (auto pkg = this.m_internal.lookup(name, vers)) return pkg; foreach (ref location; this.m_repositories) @@ -219,12 +220,12 @@ Returns: The matching package or null if no match was found. */ - Package getPackage(string name, Version ver, bool enable_overrides = true) + Package getPackage(in PackageName name, in Version ver, bool enable_overrides = true) { if (enable_overrides) { foreach (ref repo; m_repositories) foreach (ovr; repo.overrides) - if (ovr.package_ == name && ovr.source.matches(ver)) { + if (ovr.package_ == name.toString() && ovr.source.matches(ver)) { Package pack = ovr.target.match!( (NativePath path) => getOrLoadPackage(path), (Version vers) => getPackage(name, vers, false), @@ -243,6 +244,12 @@ return this.lookup(name, ver); } + deprecated("Use the overload that accepts a `PackageName` instead") + Package getPackage(string name, Version ver, bool enable_overrides = true) + { + return this.getPackage(PackageName(name), ver, enable_overrides); + } + /// ditto deprecated("Use the overload that accepts a `Version` as second argument") Package getPackage(string name, string ver, bool enable_overrides = true) @@ -263,8 +270,15 @@ } /// Ditto + deprecated("Use the overload that accepts a `PackageName` instead") Package getPackage(string name, Version ver, PlacementLocation loc) { + return this.getPackage(PackageName(name), ver, loc); + } + + /// Ditto + Package getPackage(in PackageName name, in Version ver, PlacementLocation loc) + { // Bare mode if (loc >= this.m_repositories.length) return null; @@ -332,11 +346,76 @@ foreach (p; this.m_internal.fromPath) if (p.path == path && (!p.parentPackage || (allow_sub_packages && p.parentPackage.path != p.path))) return p; - auto pack = Package.load(path, recipe_path, null, null, mode); + auto pack = this.load(path, recipe_path, null, null, mode); addPackages(this.m_internal.fromPath, pack); return pack; } + /** + * Loads a `Package` from the filesystem + * + * This is called when a `Package` needs to be loaded from the path. + * This does not change the internal state of the `PackageManager`, + * it simply loads the `Package` and returns it - it is up to the caller + * to call `addPackages`. + * + * Throws: + * If no package can be found at the `path` / with the `recipe`. + * + * Params: + * path = The directory in which the package resides. + * recipe = Optional path to the package recipe file. If left empty, + * the `path` directory will be searched for a recipe file. + * parent = Reference to the parent package, if the new package is a + * sub package. + * version_ = Optional version to associate to the package instead of + * the one declared in the package recipe, or the one + * determined by invoking the VCS (GIT currently). + * mode = Whether to issue errors, warning, or ignore unknown keys in + * dub.json + * + * Returns: A populated `Package`. + */ + protected Package load(NativePath path, NativePath recipe = NativePath.init, + Package parent = null, string version_ = null, + StrictMode mode = StrictMode.Ignore) + { + if (recipe.empty) + recipe = this.findPackageFile(path); + + enforce(!recipe.empty, + "No package file found in %s, expected one of %s" + .format(path.toNativeString(), + packageInfoFiles.map!(f => cast(string)f.filename).join("/"))); + + const PackageName pname = parent + ? PackageName(parent.name) : PackageName.init; + string text = this.readText(recipe); + auto content = parsePackageRecipe( + text, recipe.toNativeString(), pname, null, mode); + auto ret = new Package(content, path, parent, version_); + ret.m_infoFile = recipe; + return ret; + } + + /** Searches the given directory for package recipe files. + * + * Params: + * directory = The directory to search + * + * Returns: + * Returns the full path to the package file, if any was found. + * Otherwise returns an empty path. + */ + public NativePath findPackageFile(NativePath directory) + { + foreach (file; packageInfoFiles) { + auto filename = directory ~ file.filename; + if (this.existsFile(filename)) return filename; + } + return NativePath.init; + } + /** For a given SCM repository, returns the corresponding package. An SCM repository is provided as its remote URL, the repository is cloned @@ -353,13 +432,7 @@ The package loaded from the given SCM repository or null if the package couldn't be loaded. */ - deprecated("Use the overload that accepts a `dub.dependency : Repository`") - Package loadSCMPackage(string name, Dependency dependency) - in { assert(!dependency.repository.empty); } - do { return this.loadSCMPackage(name, dependency.repository); } - - /// Ditto - Package loadSCMPackage(string name, Repository repo) + Package loadSCMPackage(in PackageName name, in Repository repo) in { assert(!repo.empty); } do { Package pack; @@ -367,39 +440,60 @@ final switch (repo.kind) { case repo.Kind.git: - pack = loadGitPackage(name, repo); + return this.loadGitPackage(name, repo); } - if (pack !is null) { - addPackages(this.m_internal.fromPath, pack); - } - return pack; } - private Package loadGitPackage(string name, in Repository repo) - { - import dub.internal.git : cloneRepository; + deprecated("Use the overload that accepts a `dub.dependency : Repository`") + Package loadSCMPackage(string name, Dependency dependency) + in { assert(!dependency.repository.empty); } + do { return this.loadSCMPackage(name, dependency.repository); } + deprecated("Use `loadSCMPackage(PackageName, Repository)`") + Package loadSCMPackage(string name, Repository repo) + { + return this.loadSCMPackage(PackageName(name), repo); + } + + private Package loadGitPackage(in PackageName name, in Repository repo) + { if (!repo.ref_.startsWith("~") && !repo.ref_.isGitHash) { return null; } string gitReference = repo.ref_.chompPrefix("~"); NativePath destination = this.getPackagePath(PlacementLocation.user, name, repo.ref_); - // For libraries leaking their import path - destination ~= name; - destination.endsWithSlash = true; - foreach (p; getPackageIterator(name)) { + foreach (p; getPackageIterator(name.toString())) { if (p.path == destination) { return p; } } - if (!cloneRepository(repo.remote, gitReference, destination.toNativeString())) { + if (!this.gitClone(repo.remote, gitReference, destination)) return null; - } - return Package.load(destination); + Package result = this.load(destination); + if (result !is null) + this.addPackages(this.m_internal.fromPath, result); + return result; + } + + /** + * Perform a `git clone` operation at `dest` using `repo` + * + * Params: + * remote = The remote to clone from + * gitref = The git reference to use + * dest = Where the result of git clone operation is to be stored + * + * Returns: + * Whether or not the clone operation was successfull. + */ + protected bool gitClone(string remote, string gitref, in NativePath dest) + { + static import dub.internal.git; + return dub.internal.git.cloneRepository(remote, gitref, dest.toNativeString()); } /** @@ -407,7 +501,7 @@ * * See `Location.getPackagePath`. */ - package(dub) NativePath getPackagePath (PlacementLocation base, string name, string vers) + package(dub) NativePath getPackagePath(PlacementLocation base, in PackageName name, string vers) { assert(this.m_repositories.length == 3, "getPackagePath called in bare mode"); return this.m_repositories[base].getPackagePath(name, vers); @@ -428,14 +522,28 @@ * Returns: * The best package matching the parameters, or `null` if none was found. */ + deprecated("Use the overload that accepts a `PackageName` instead") Package getBestPackage(string name, Version vers) { + return this.getBestPackage(PackageName(name), vers); + } + + /// Ditto + Package getBestPackage(in PackageName name, in Version vers) + { return this.getBestPackage(name, VersionRange(vers, vers)); } /// Ditto + deprecated("Use the overload that accepts a `PackageName` instead") Package getBestPackage(string name, VersionRange range = VersionRange.Any) { + return this.getBestPackage(PackageName(name), range); + } + + /// Ditto + Package getBestPackage(in PackageName name, in VersionRange range = VersionRange.Any) + { return this.getBestPackage_(name, Dependency(range)); } @@ -450,14 +558,15 @@ deprecated("`getBestPackage` should only be used with a `Version` or `VersionRange` argument") Package getBestPackage(string name, Dependency version_spec, bool enable_overrides = true) { - return this.getBestPackage_(name, version_spec, enable_overrides); + return this.getBestPackage_(PackageName(name), version_spec, enable_overrides); } // TODO: Merge this into `getBestPackage(string, VersionRange)` - private Package getBestPackage_(string name, Dependency version_spec, bool enable_overrides = true) + private Package getBestPackage_(in PackageName name, in Dependency version_spec, + bool enable_overrides = true) { Package ret; - foreach (p; getPackageIterator(name)) { + foreach (p; getPackageIterator(name.toString())) { auto vmm = isManagedPackage(p) ? VersionMatchMode.strict : VersionMatchMode.standard; if (version_spec.matches(p.version_, vmm) && (!ret || p.version_ > ret.version_)) ret = p; @@ -472,9 +581,6 @@ /** Gets the a specific sub package. - In contrast to `Package.getSubPackage`, this function supports path - based sub packages. - Params: base_package = The package from which to get a sub package sub_name = Name of the sub package (not prefixed with the base @@ -510,11 +616,9 @@ */ bool isManagedPath(NativePath path) const { - foreach (rep; m_repositories) { - NativePath rpath = rep.packagePath; - if (path.startsWith(rpath)) + foreach (rep; m_repositories) + if (rep.isManaged(path)) return true; - } return false; } @@ -585,14 +689,14 @@ void addOverride(PlacementLocation scope_, string package_, Dependency version_spec, Version target) { m_repositories[scope_].overrides ~= PackageOverride(package_, version_spec, target); - m_repositories[scope_].writeOverrides(); + m_repositories[scope_].writeOverrides(this); } /// ditto deprecated("Use the overload that accepts a `VersionRange` as 3rd argument") void addOverride(PlacementLocation scope_, string package_, Dependency version_spec, NativePath target) { m_repositories[scope_].overrides ~= PackageOverride(package_, version_spec, target); - m_repositories[scope_].writeOverrides(); + m_repositories[scope_].writeOverrides(this); } /// Ditto @@ -612,13 +716,13 @@ package(dub) void addOverride_(PlacementLocation scope_, string package_, VersionRange source, Version target) { m_repositories[scope_].overrides ~= PackageOverride_(package_, source, target); - m_repositories[scope_].writeOverrides(); + m_repositories[scope_].writeOverrides(this); } // Non deprecated version that is used by `commandline`. Do not use! package(dub) void addOverride_(PlacementLocation scope_, string package_, VersionRange source, NativePath target) { m_repositories[scope_].overrides ~= PackageOverride_(package_, source, target); - m_repositories[scope_].writeOverrides(); + m_repositories[scope_].writeOverrides(this); } /** Removes an existing package override. @@ -645,7 +749,7 @@ if (ovr.package_ != package_ || ovr.source != src) continue; rep.overrides = rep.overrides[0 .. i] ~ rep.overrides[i+1 .. $]; - (*rep).writeOverrides(); + (*rep).writeOverrides(this); return; } throw new Exception(format("No override exists for %s %s", package_, src)); @@ -654,7 +758,10 @@ deprecated("Use `store(NativePath source, PlacementLocation dest, string name, Version vers)`") Package storeFetchedPackage(NativePath zip_file_path, Json package_info, NativePath destination) { - return this.store_(zip_file_path, destination, package_info["name"].get!string, + import dub.internal.vibecompat.core.file; + + return this.store_(readFile(zip_file_path), destination, + PackageName(package_info["name"].get!string), Version(package_info["version"].get!string)); } @@ -679,43 +786,58 @@ * If the package cannot be loaded / the zip is corrupted / the package * already exists, etc... */ + deprecated("Use the overload that accepts a `PackageName` instead") Package store(NativePath src, PlacementLocation dest, string name, Version vers) { + return this.store(src, dest, PackageName(name), vers); + } + + /// Ditto + Package store(NativePath src, PlacementLocation dest, in PackageName name, + in Version vers) + { + import dub.internal.vibecompat.core.file; + + auto data = readFile(src); + return this.store(data, dest, name, vers); + } + + /// Ditto + Package store(ubyte[] data, PlacementLocation dest, + in PackageName name, in Version vers) + { + import dub.internal.vibecompat.core.file; + + assert(!name.sub.length, "Cannot store a subpackage, use main package instead"); NativePath dstpath = this.getPackagePath(dest, name, vers.toString()); - ensureDirectory(dstpath); - // For libraries leaking their import path - dstpath = dstpath ~ name; + ensureDirectory(dstpath.parentPath()); + const lockPath = dstpath.parentPath() ~ ".lock"; // possibly wait for other dub instance import core.time : seconds; - auto lock = lockFile(dstpath.toNativeString() ~ ".lock", 30.seconds); - if (dstpath.existsFile()) { + auto lock = lockFile(lockPath.toNativeString(), 30.seconds); + if (this.existsFile(dstpath)) { return this.getPackage(name, vers, dest); } - return this.store_(src, dstpath, name, vers); + return this.store_(data, dstpath, name, vers); } /// Backward-compatibility for deprecated overload, simplify once `storeFetchedPatch` /// is removed - private Package store_(NativePath src, NativePath destination, string name, Version vers) + private Package store_(ubyte[] data, NativePath destination, + in PackageName name, in Version vers) { + import dub.internal.vibecompat.core.file; import std.range : walkLength; - logDebug("Placing package '%s' version '%s' to location '%s' from file '%s'", - name, vers, destination.toNativeString(), src.toNativeString()); + logDebug("Placing package '%s' version '%s' to location '%s'", + name, vers, destination.toNativeString()); - if( existsFile(destination) ){ - throw new Exception(format("%s (%s) needs to be removed from '%s' prior placement.", - name, vers, destination)); - } + enforce(!this.existsFile(destination), + "%s (%s) needs to be removed from '%s' prior placement." + .format(name, vers, destination)); - // open zip file - ZipArchive archive; - { - logDebug("Opening file %s", src); - archive = new ZipArchive(readFile(src)); - } - + ZipArchive archive = new ZipArchive(data); logDebug("Extracting from zip."); // In a GitHub zip, the actual contents are in a sub-folder @@ -759,7 +881,7 @@ auto dst_path = destination ~ cleanedPath; logDebug("Creating %s", cleanedPath); - if( dst_path.endsWithSlash ){ + if (dst_path.endsWithSlash) { ensureDirectory(dst_path); } else { ensureDirectory(dst_path.parentPath); @@ -778,7 +900,7 @@ } } - writeFile(dst_path, archive.expand(a)); + this.writeFile(dst_path, archive.expand(a)); setAttributes(dst_path.toNativeString(), a); symlink_exit: ++countFiles; @@ -787,7 +909,7 @@ logDebug("%s file(s) copied.", to!string(countFiles)); // overwrite dub.json (this one includes a version field) - auto pack = Package.load(destination, NativePath.init, null, vers.toString()); + auto pack = this.load(destination, NativePath.init, null, vers.toString()); if (pack.recipePath.head != defaultPackageFilename) // Storeinfo saved a default file, this could be different to the file from the zip. @@ -802,6 +924,7 @@ { logDebug("Remove %s, version %s, path '%s'", pack.name, pack.version_, pack.path); enforce(!pack.path.empty, "Cannot remove package "~pack.name~" without a path."); + enforce(pack.parentPackage is null, "Cannot remove subpackage %s".format(pack.name)); // remove package from repositories' list bool found = false; @@ -850,7 +973,7 @@ this.refresh(); path.endsWithSlash = true; - auto pack = Package.load(path); + auto pack = this.load(path); enforce(pack.name.length, "The package has no name, defined in: " ~ path.toString()); if (verName.length) pack.version_ = Version(verName); @@ -867,7 +990,7 @@ addPackages(*packs, pack); - this.m_repositories[type].writeLocalPackageList(); + this.m_repositories[type].writeLocalPackageList(this); logInfo("Registered package: %s (version: %s)", pack.name, pack.version_); return pack; @@ -897,7 +1020,7 @@ .filter!(en => !to_remove.canFind(en.index)) .map!(en => en.value).array; - this.m_repositories[type].writeLocalPackageList(); + this.m_repositories[type].writeLocalPackageList(this); foreach(ver, name; removed) logInfo("Deregistered package: %s (version: %s)", name, ver); @@ -907,14 +1030,14 @@ void addSearchPath(NativePath path, PlacementLocation type) { m_repositories[type].searchPath ~= path; - this.m_repositories[type].writeLocalPackageList(); + this.m_repositories[type].writeLocalPackageList(this); } /// Removes a search path from the given type. void removeSearchPath(NativePath path, PlacementLocation type) { m_repositories[type].searchPath = m_repositories[type].searchPath.filter!(p => p != path)().array(); - this.m_repositories[type].writeLocalPackageList(); + this.m_repositories[type].writeLocalPackageList(this); } deprecated("Use `refresh()` without boolean argument(same as `refresh(false)`") @@ -943,7 +1066,7 @@ repository.scan(this, refresh); foreach (ref repository; this.m_repositories) - repository.loadOverrides(); + repository.loadOverrides(this); this.m_initialized = true; } @@ -953,6 +1076,8 @@ /// .svn folders) Hash hashPackage(Package pack) { + import dub.internal.vibecompat.core.file; + string[] ignored_directories = [".git", ".dub", ".svn"]; // something from .dub_ignore or what? string[] ignored_files = []; @@ -978,9 +1103,70 @@ return digest[].dup; } + /** + * Writes the selections file (`dub.selections.json`) + * + * The selections file is only used for the root package / project. + * However, due to it being a filesystem interaction, it is managed + * from the `PackageManager`. + * + * Params: + * project = The root package / project to read the selections file for. + * selections = The `SelectionsFile` to write. + * overwrite = Whether to overwrite an existing selections file. + * True by default. + */ + public void writeSelections(in Package project, in Selections!1 selections, + bool overwrite = true) + { + const path = project.path ~ "dub.selections.json"; + if (!overwrite && this.existsFile(path)) + return; + this.writeFile(path, selectionsToString(selections)); + } + + /// Package function to avoid code duplication with deprecated + /// SelectedVersions.save, merge with `writeSelections` in + /// the future. + package static string selectionsToString (in Selections!1 s) + { + Json json = selectionsToJSON(s); + assert(json.type == Json.Type.object); + assert(json.length == 2); + assert(json["versions"].type != Json.Type.undefined); + + auto result = appender!string(); + result.put("{\n\t\"fileVersion\": "); + result.writeJsonString(json["fileVersion"]); + result.put(",\n\t\"versions\": {"); + auto vers = json["versions"].get!(Json[string]); + bool first = true; + foreach (k; vers.byKey.array.sort()) { + if (!first) result.put(","); + else first = false; + result.put("\n\t\t"); + result.writeJsonString(Json(k)); + result.put(": "); + result.writeJsonString(vers[k]); + } + result.put("\n\t}\n}\n"); + return result.data; + } + + /// Ditto + package static Json selectionsToJSON (in Selections!1 s) + { + Json serialized = Json.emptyObject; + serialized["fileVersion"] = s.fileVersion; + serialized["versions"] = Json.emptyObject; + foreach (p, dep; s.versions) + serialized["versions"][p] = dep.toJson(true); + return serialized; + } + /// Adds the package and scans for sub-packages. - private void addPackages(ref Package[] dst_repos, Package pack) - const { + protected void addPackages(ref Package[] dst_repos, Package pack) + { // Add the main package. dst_repos ~= pack; @@ -995,7 +1181,7 @@ p.normalize(); enforce(!p.absolute, "Sub package paths must be sub paths of the parent package."); auto path = pack.path ~ p; - sp = Package.load(path, NativePath.init, pack); + sp = this.load(path, NativePath.init, pack); } else sp = new Package(spr.recipe, pack.path, pack); // Add the sub-package. @@ -1008,6 +1194,58 @@ } } } + + /// Used for dependency injection + protected bool existsDirectory(NativePath path) + { + static import dub.internal.vibecompat.core.file; + return dub.internal.vibecompat.core.file.existsDirectory(path); + } + + /// Ditto + protected void ensureDirectory(NativePath path) + { + static import dub.internal.vibecompat.core.file; + return dub.internal.vibecompat.core.file.ensureDirectory(path); + } + + /// Ditto + protected bool existsFile(NativePath path) + { + static import dub.internal.vibecompat.core.file; + return dub.internal.vibecompat.core.file.existsFile(path); + } + + /// Ditto + protected void writeFile(NativePath path, const(ubyte)[] data) + { + static import dub.internal.vibecompat.core.file; + return dub.internal.vibecompat.core.file.writeFile(path, data); + } + + /// Ditto + protected void writeFile(NativePath path, const(char)[] data) + { + static import dub.internal.vibecompat.core.file; + return dub.internal.vibecompat.core.file.writeFile(path, data); + } + + /// Ditto + protected string readText(NativePath path) + { + static import dub.internal.vibecompat.core.file; + return dub.internal.vibecompat.core.file.readText(path); + } + + /// Ditto + protected alias IterateDirDg = int delegate(scope int delegate(ref FileInfo)); + + /// Ditto + protected IterateDirDg iterateDirectory(NativePath path) + { + static import dub.internal.vibecompat.core.file; + return dub.internal.vibecompat.core.file.iterateDirectory(path); + } } deprecated(OverrideDepMsg) @@ -1147,15 +1385,17 @@ this.packagePath = path; } - void loadOverrides() + void loadOverrides(PackageManager mgr) { this.overrides = null; auto ovrfilepath = this.packagePath ~ LocalOverridesFilename; - if (existsFile(ovrfilepath)) { + if (mgr.existsFile(ovrfilepath)) { logWarn("Found local override file: %s", ovrfilepath); logWarn(OverrideDepMsg); logWarn("Replace with a path-based dependency in your project or a custom cache path"); - foreach (entry; jsonFromFile(ovrfilepath)) { + const text = mgr.readText(ovrfilepath); + auto json = parseJsonString(text, ovrfilepath.toNativeString()); + foreach (entry; json) { PackageOverride_ ovr; ovr.package_ = entry["name"].get!string; ovr.source = VersionRange.fromString(entry["version"].get!string); @@ -1166,7 +1406,7 @@ } } - private void writeOverrides() + private void writeOverrides(PackageManager mgr) { Json[] newlist; foreach (ovr; this.overrides) { @@ -1180,11 +1420,13 @@ newlist ~= jovr; } auto path = this.packagePath; - ensureDirectory(path); - writeJsonFile(path ~ LocalOverridesFilename, Json(newlist)); + mgr.ensureDirectory(path); + auto app = appender!string(); + app.writePrettyJsonString(Json(newlist)); + mgr.writeFile(path ~ LocalOverridesFilename, app.data); } - private void writeLocalPackageList() + private void writeLocalPackageList(PackageManager mgr) { Json[] newlist; foreach (p; this.searchPath) { @@ -1204,8 +1446,10 @@ } NativePath path = this.packagePath; - ensureDirectory(path); - writeJsonFile(path ~ LocalPackagesFilename, Json(newlist)); + mgr.ensureDirectory(path); + auto app = appender!string(); + app.writePrettyJsonString(Json(newlist)); + mgr.writeFile(path ~ LocalPackagesFilename, app.data); } // load locally defined packages @@ -1216,10 +1460,12 @@ NativePath[] paths; try { auto local_package_file = list_path ~ LocalPackagesFilename; - if (!existsFile(local_package_file)) return; + if (!manager.existsFile(local_package_file)) return; logDiagnostic("Loading local package map at %s", local_package_file.toNativeString()); - auto packlist = jsonFromFile(local_package_file); + const text = manager.readText(local_package_file); + auto packlist = parseJsonString( + text, local_package_file.toNativeString()); enforce(packlist.type == Json.Type.array, LocalPackagesFilename ~ " must contain an array."); foreach (pentry; packlist) { try { @@ -1240,8 +1486,8 @@ } if (!pp) { - auto infoFile = Package.findPackageFile(path); - if (!infoFile.empty) pp = Package.load(path, infoFile); + auto infoFile = manager.findPackageFile(path); + if (!infoFile.empty) pp = manager.load(path, infoFile); else { logWarn("Locally registered package %s %s was not found. Please run 'dub remove-local \"%s\"'.", name, ver, path.toNativeString()); @@ -1292,20 +1538,20 @@ void scanPackageFolder(NativePath path, PackageManager mgr, Package[] existing_packages) { - if (!path.existsDirectory()) + if (!mgr.existsDirectory(path)) return; void loadInternal (NativePath pack_path, NativePath packageFile) { - Package p; + import std.algorithm.searching : find; + + // If the package has already been loaded, no need to re-load it. + auto rng = existing_packages.find!(pp => pp.path == pack_path); + if (!rng.empty) + return mgr.addPackages(this.fromPath, rng.front); + try { - foreach (pp; existing_packages) - if (pp.path == pack_path) { - p = pp; - break; - } - if (!p) p = Package.load(pack_path, packageFile); - mgr.addPackages(this.fromPath, p); + mgr.addPackages(this.fromPath, mgr.load(pack_path, packageFile)); } catch (ConfigException exc) { // Configy error message already include the path logError("Invalid recipe for local package: %S", exc); @@ -1316,34 +1562,45 @@ } logDebug("iterating dir %s", path.toNativeString()); - try foreach (pdir; iterateDirectory(path)) { + try foreach (pdir; mgr.iterateDirectory(path)) { logDebug("iterating dir %s entry %s", path.toNativeString(), pdir.name); if (!pdir.isDirectory) continue; - // Old / flat directory structure, used in non-standard path - // Packages are stored in $ROOT/$SOMETHING/` const pack_path = path ~ (pdir.name ~ "/"); - auto packageFile = Package.findPackageFile(pack_path); - if (!packageFile.empty) { - // Deprecated unmanaged directory structure - logWarn("Package at path '%s' should be under '%s'", - pack_path.toNativeString().color(Mode.bold), - (pack_path ~ "$VERSION" ~ pdir.name).toNativeString().color(Mode.bold)); - logWarn("The package will no longer be detected starting from v1.42.0"); - loadInternal(pack_path, packageFile); - } + auto packageFile = mgr.findPackageFile(pack_path); - // Managed structure: $ROOT/$NAME/$VERSION/$NAME - // This is the most common code path - else { - // Iterate over versions of a package - foreach (versdir; iterateDirectory(pack_path)) { - if (!versdir.isDirectory) continue; - auto vers_path = pack_path ~ versdir.name ~ (pdir.name ~ "/"); - if (!vers_path.existsDirectory()) continue; - packageFile = Package.findPackageFile(vers_path); - loadInternal(vers_path, packageFile); + if (isManaged(path)) { + // Old / flat directory structure, used in non-standard path + // Packages are stored in $ROOT/$SOMETHING/` + if (!packageFile.empty) { + // Deprecated flat managed directory structure + logWarn("Package at path '%s' should be under '%s'", + pack_path.toNativeString().color(Mode.bold), + (pack_path ~ "$VERSION" ~ pdir.name).toNativeString().color(Mode.bold)); + logWarn("The package will no longer be detected starting from v1.42.0"); + loadInternal(pack_path, packageFile); + } else { + // New managed structure: $ROOT/$NAME/$VERSION/$NAME + // This is the most common code path + + // Iterate over versions of a package + foreach (versdir; mgr.iterateDirectory(pack_path)) { + if (!versdir.isDirectory) continue; + auto vers_path = pack_path ~ versdir.name ~ (pdir.name ~ "/"); + if (!mgr.existsDirectory(vers_path)) continue; + packageFile = mgr.findPackageFile(vers_path); + loadInternal(vers_path, packageFile); + } } + } else { + // Unmanaged directories (dub add-path) are always stored as a + // flat list of packages, as these are the working copies managed + // by the user. The nested structure should not be supported, + // even optionally, because that would lead to bogus "no package + // file found" errors in case the internal directory structure + // accidentally matches the $NAME/$VERSION/$NAME scheme + if (!packageFile.empty) + loadInternal(pack_path, packageFile); } } catch (Exception e) @@ -1364,13 +1621,15 @@ * Returns: * A `Package` if one was found, `null` if none exists. */ - inout(Package) lookup(string name, Version ver, PackageManager mgr) inout { + inout(Package) lookup(in PackageName name, in Version ver) inout { foreach (pkg; this.localPackages) - if (pkg.name == name && pkg.version_.matches(ver, VersionMatchMode.standard)) + if (pkg.name == name.toString() && + pkg.version_.matches(ver, VersionMatchMode.standard)) return pkg; foreach (pkg; this.fromPath) { - auto pvm = mgr.isManagedPackage(pkg) ? VersionMatchMode.strict : VersionMatchMode.standard; - if (pkg.name == name && pkg.version_.matches(ver, pvm)) + auto pvm = this.isManaged(pkg.basePackage.path) ? + VersionMatchMode.strict : VersionMatchMode.standard; + if (pkg.name == name.toString() && pkg.version_.matches(ver, pvm)) return pkg; } return null; @@ -1391,19 +1650,18 @@ * Returns: * A `Package` if one was found, `null` if none exists. */ - Package load (string name, Version vers, PackageManager mgr) + Package load (in PackageName name, Version vers, PackageManager mgr) { - if (auto pkg = this.lookup(name, vers, mgr)) + if (auto pkg = this.lookup(name, vers)) return pkg; string versStr = vers.toString(); - const lookupName = getBasePackageName(name); - const path = this.getPackagePath(lookupName, versStr) ~ (lookupName ~ "/"); - if (!path.existsDirectory()) + const path = this.getPackagePath(name, versStr); + if (!mgr.existsDirectory(path)) return null; - logDiagnostic("Lazily loading package %s:%s from %s", lookupName, vers, path); - auto p = Package.load(path); + logDiagnostic("Lazily loading package %s:%s from %s", name.main, vers, path); + auto p = mgr.load(path); enforce( p.version_ == vers, format("Package %s located in %s has a different version than its path: Got %s, expected %s", @@ -1419,16 +1677,31 @@ * which expects their containing folder to have an exact name and use * `importPath "../"`. * - * Hence the final format should be `$BASE/$NAME-$VERSION/$NAME`, - * but this function returns `$BASE/$NAME-$VERSION/` + * Hence the final format returned is `$BASE/$NAME/$VERSION/$NAME`, * `$BASE` is `this.packagePath`. + * + * Params: + * name = The package name - if the name is that of a subpackage, + * only the path to the main package is returned, as the + * subpackage path can only be known after reading the recipe. + * vers = A version string. Typed as a string because git hashes + * can be used with this function. + * + * Returns: + * An absolute `NativePath` nested in this location. */ - NativePath getPackagePath (string name, string vers) + NativePath getPackagePath (in PackageName name, string vers) { - NativePath result = this.packagePath ~ name ~ vers; + NativePath result = this.packagePath ~ name.main.toString() ~ vers ~ + name.main.toString(); result.endsWithSlash = true; return result; } + + /// Determines if a specific path is within a DUB managed Location. + bool isManaged(NativePath path) const { + return path.startsWith(this.packagePath); + } } private immutable string OverrideDepMsg = diff --git a/source/dub/packagesuppliers/fallback.d b/source/dub/packagesuppliers/fallback.d index b9235c7..a2009d6 100644 --- a/source/dub/packagesuppliers/fallback.d +++ b/source/dub/packagesuppliers/fallback.d @@ -28,9 +28,9 @@ } // Workaround https://issues.dlang.org/show_bug.cgi?id=2525 - abstract override Version[] getVersions(string package_id); - abstract override void fetchPackage(NativePath path, string package_id, in VersionRange dep, bool pre_release); - abstract override Json fetchPackageRecipe(string package_id, in VersionRange dep, bool pre_release); + abstract override Version[] getVersions(in PackageName name); + abstract override ubyte[] fetchPackage(in PackageName name, in VersionRange dep, bool pre_release); + abstract override Json fetchPackageRecipe(in PackageName name, in VersionRange dep, bool pre_release); abstract override SearchResult[] searchPackages(string query); } diff --git a/source/dub/packagesuppliers/filesystem.d b/source/dub/packagesuppliers/filesystem.d index 4af88c4..657f4c2 100644 --- a/source/dub/packagesuppliers/filesystem.d +++ b/source/dub/packagesuppliers/filesystem.d @@ -1,7 +1,11 @@ module dub.packagesuppliers.filesystem; +import dub.internal.logging; +import dub.internal.vibecompat.inet.path; import dub.packagesuppliers.packagesupplier; +import std.exception : enforce; + /** File system based package supplier. @@ -9,10 +13,6 @@ the form "[package name]-[version].zip". */ class FileSystemPackageSupplier : PackageSupplier { - import dub.internal.logging; - - version (Have_vibe_core) import dub.internal.vibecompat.inet.path : toNativeString; - import std.exception : enforce; private { NativePath m_path; } @@ -21,17 +21,22 @@ override @property string description() { return "file repository at "~m_path.toNativeString(); } - Version[] getVersions(string package_id) + Version[] getVersions(in PackageName name) { import std.algorithm.sorting : sort; import std.file : dirEntries, DirEntry, SpanMode; import std.conv : to; + import dub.semver : isValidVersion; Version[] ret; - foreach (DirEntry d; dirEntries(m_path.toNativeString(), package_id~"*", SpanMode.shallow)) { + const zipFileGlob = name.main.toString() ~ "*.zip"; + foreach (DirEntry d; dirEntries(m_path.toNativeString(), zipFileGlob, SpanMode.shallow)) { NativePath p = NativePath(d.name); + auto vers = p.head.name[name.main.toString().length+1..$-4]; + if (!isValidVersion(vers)) { + logDebug("Ignoring entry '%s' because it isn't a version of package '%s'", p, name.main); + continue; + } logDebug("Entry: %s", p); - enforce(to!string(p.head)[$-4..$] == ".zip"); - auto vers = p.head.name[package_id.length+1..$-4]; logDebug("Version: %s", vers); ret ~= Version(vers); } @@ -39,17 +44,18 @@ return ret; } - void fetchPackage(NativePath path, string packageId, in VersionRange dep, bool pre_release) + override ubyte[] fetchPackage(in PackageName name, + in VersionRange dep, bool pre_release) { - import dub.internal.vibecompat.core.file : copyFile, existsFile; - enforce(path.absolute); - logInfo("Storing package '%s', version requirements: %s", packageId, dep); - auto filename = bestPackageFile(packageId, dep, pre_release); + import dub.internal.vibecompat.core.file : readFile, existsFile; + logInfo("Storing package '%s', version requirements: %s", name.main, dep); + auto filename = bestPackageFile(name, dep, pre_release); enforce(existsFile(filename)); - copyFile(filename, path); + return readFile(filename); } - Json fetchPackageRecipe(string packageId, in VersionRange dep, bool pre_release) + override Json fetchPackageRecipe(in PackageName name, in VersionRange dep, + bool pre_release) { import std.array : split; import std.path : stripExtension; @@ -58,15 +64,16 @@ import dub.recipe.io : parsePackageRecipe; import dub.recipe.json : toJson; - auto filePath = bestPackageFile(packageId, dep, pre_release); + auto filePath = bestPackageFile(name, dep, pre_release); string packageFileName; string packageFileContent = packageInfoFileFromZip(filePath, packageFileName); auto recipe = parsePackageRecipe(packageFileContent, packageFileName); Json json = toJson(recipe); auto basename = filePath.head.name; enforce(basename.endsWith(".zip"), "Malformed package filename: " ~ filePath.toNativeString); - enforce(basename.startsWith(packageId), "Malformed package filename: " ~ filePath.toNativeString); - json["version"] = basename[packageId.length + 1 .. $-4]; + enforce(basename.startsWith(name.main.toString()), + "Malformed package filename: " ~ filePath.toNativeString); + json["version"] = basename[name.main.toString().length + 1 .. $-4]; return json; } @@ -76,16 +83,17 @@ return null; } - private NativePath bestPackageFile(string packageId, in VersionRange dep, bool pre_release) + private NativePath bestPackageFile(in PackageName name, in VersionRange dep, + bool pre_release) { import std.algorithm.iteration : filter; import std.array : array; import std.format : format; NativePath toPath(Version ver) { - return m_path ~ (packageId ~ "-" ~ ver.toString() ~ ".zip"); + return m_path ~ "%s-%s.zip".format(name.main, ver); } - auto versions = getVersions(packageId).filter!(v => dep.matches(v)).array; - enforce(versions.length > 0, format("No package %s found matching %s", packageId, dep)); + auto versions = getVersions(name).filter!(v => dep.matches(v)).array; + enforce(versions.length > 0, format("No package %s found matching %s", name.main, dep)); foreach_reverse (ver; versions) { if (pre_release || !ver.isPreRelease) return toPath(ver); diff --git a/source/dub/packagesuppliers/maven.d b/source/dub/packagesuppliers/maven.d index ecb29b1..bb0a019 100644 --- a/source/dub/packagesuppliers/maven.d +++ b/source/dub/packagesuppliers/maven.d @@ -20,7 +20,7 @@ enum httpTimeout = 16; URL m_mavenUrl; struct CacheEntry { Json data; SysTime cacheTime; } - CacheEntry[string] m_metadataCache; + CacheEntry[PackageName] m_metadataCache; Duration m_maxCacheTime; } @@ -32,10 +32,10 @@ override @property string description() { return "maven repository at "~m_mavenUrl.toString(); } - Version[] getVersions(string package_id) + override Version[] getVersions(in PackageName name) { import std.algorithm.sorting : sort; - auto md = getMetadata(package_id); + auto md = getMetadata(name.main); if (md.type == Json.Type.null_) return null; Version[] ret; @@ -47,74 +47,82 @@ return ret; } - void fetchPackage(NativePath path, string packageId, in VersionRange dep, bool pre_release) + override ubyte[] fetchPackage(in PackageName name, + in VersionRange dep, bool pre_release) { import std.format : format; - auto md = getMetadata(packageId); - Json best = getBestPackage(md, packageId, dep, pre_release); + auto md = getMetadata(name.main); + Json best = getBestPackage(md, name.main, dep, pre_release); if (best.type == Json.Type.null_) - return; + return null; auto vers = best["version"].get!string; - auto url = m_mavenUrl~NativePath("%s/%s/%s-%s.zip".format(packageId, vers, packageId, vers)); + auto url = m_mavenUrl ~ NativePath( + "%s/%s/%s-%s.zip".format(name.main, vers, name.main, vers)); try { - retryDownload(url, path, 3, httpTimeout); - return; + return retryDownload(url, 3, httpTimeout); } catch(HTTPStatusException e) { if (e.status == 404) throw e; - else logDebug("Failed to download package %s from %s", packageId, url); + else logDebug("Failed to download package %s from %s", name.main, url); } catch(Exception e) { - logDebug("Failed to download package %s from %s", packageId, url); + logDebug("Failed to download package %s from %s", name.main, url); } - throw new Exception("Failed to download package %s from %s".format(packageId, url)); + throw new Exception("Failed to download package %s from %s".format(name.main, url)); } - Json fetchPackageRecipe(string packageId, in VersionRange dep, bool pre_release) + override Json fetchPackageRecipe(in PackageName name, in VersionRange dep, + bool pre_release) { - auto md = getMetadata(packageId); - return getBestPackage(md, packageId, dep, pre_release); + auto md = getMetadata(name); + return getBestPackage(md, name, dep, pre_release); } - private Json getMetadata(string packageId) + private Json getMetadata(in PackageName name) { import dub.internal.undead.xml; auto now = Clock.currTime(UTC()); - if (auto pentry = packageId in m_metadataCache) { + if (auto pentry = name.main in m_metadataCache) { if (pentry.cacheTime + m_maxCacheTime > now) return pentry.data; - m_metadataCache.remove(packageId); + m_metadataCache.remove(name.main); } - auto url = m_mavenUrl~NativePath(packageId~"/maven-metadata.xml"); + auto url = m_mavenUrl ~ NativePath(name.main.toString() ~ "/maven-metadata.xml"); - logDebug("Downloading maven metadata for %s", packageId); + logDebug("Downloading maven metadata for %s", name.main); string xmlData; try xmlData = cast(string)retryDownload(url, 3, httpTimeout); catch(HTTPStatusException e) { if (e.status == 404) { - logDebug("Maven metadata %s not found at %s (404): %s", packageId, description, e.msg); + logDebug("Maven metadata %s not found at %s (404): %s", name.main, description, e.msg); return Json(null); } else throw e; } - auto json = Json(["name": Json(packageId), "versions": Json.emptyArray]); + auto json = Json([ + "name": Json(name.main.toString()), + "versions": Json.emptyArray + ]); auto xml = new DocumentParser(xmlData); xml.onStartTag["versions"] = (ElementParser xml) { xml.onEndTag["version"] = (in Element e) { - json["versions"] ~= serializeToJson(["name": packageId, "version": e.text]); + json["versions"] ~= serializeToJson([ + "name": name.main.toString(), + "version": e.text, + ]); }; xml.parse(); }; xml.parse(); - m_metadataCache[packageId] = CacheEntry(json, now); + m_metadataCache[name.main] = CacheEntry(json, now); return json; } @@ -122,10 +130,10 @@ { // Only exact search is supported // This enables retrieval of dub packages on dub run - auto md = getMetadata(query); + auto md = getMetadata(PackageName(query)); if (md.type == Json.Type.null_) return null; - auto json = getBestPackage(md, query, VersionRange.Any, true); + auto json = getBestPackage(md, PackageName(query), VersionRange.Any, true); return [SearchResult(json["name"].opt!string, "", json["version"].opt!string)]; } } diff --git a/source/dub/packagesuppliers/packagesupplier.d b/source/dub/packagesuppliers/packagesupplier.d index 71f8b8d..fd7e40d 100644 --- a/source/dub/packagesuppliers/packagesupplier.d +++ b/source/dub/packagesuppliers/packagesupplier.d @@ -1,6 +1,6 @@ module dub.packagesuppliers.packagesupplier; -public import dub.dependency : Dependency, Version, VersionRange; +public import dub.dependency : PackageName, Dependency, Version, VersionRange; import dub.dependency : visit; public import dub.internal.vibecompat.core.file : NativePath; public import dub.internal.vibecompat.data.json : Json; @@ -23,25 +23,41 @@ Throws: Throws an exception if the package name is not known, or if an error occurred while retrieving the version list. */ - Version[] getVersions(string package_id); + deprecated("Use `getVersions(PackageName)` instead") + final Version[] getVersions(string name) + { + return this.getVersions(PackageName(name)); + } - /** Downloads a package and stores it as a ZIP file. + Version[] getVersions(in PackageName name); + + + /** Downloads a package and returns its binary content Params: - path = Absolute path of the target ZIP file - package_id = Name of the package to retrieve + name = Name of the package to retrieve dep = Version constraint to match against pre_release = If true, matches the latest pre-release version. Otherwise prefers stable versions. */ - void fetchPackage(NativePath path, string package_id, in VersionRange dep, bool pre_release); + ubyte[] fetchPackage(in PackageName name, in VersionRange dep, + bool pre_release); - deprecated("Use the overload that accepts a `VersionRange` instead") - final void fetchPackage(NativePath path, string package_id, Dependency dep, bool pre_release) + deprecated("Use `writeFile(path, fetchPackage(PackageName, VersionRange, bool))` instead") + final void fetchPackage(in NativePath path, in PackageName name, + in VersionRange dep, bool pre_release) + { + import dub.internal.vibecompat.core.file : writeFile; + if (auto res = this.fetchPackage(name, dep, pre_release)) + writeFile(path, res); + } + + deprecated("Use `fetchPackage(NativePath, PackageName, VersionRange, bool)` instead") + final void fetchPackage(NativePath path, string name, Dependency dep, bool pre_release) { return dep.visit!( (const VersionRange rng) { - return this.fetchPackage(path, package_id, rng, pre_release); + return this.fetchPackage(path, PackageName(name), rng, pre_release); }, (any) { assert(0, "Trying to fetch a package with a non-version dependency: " ~ any.toString()); }, @@ -56,14 +72,14 @@ pre_release = If true, matches the latest pre-release version. Otherwise prefers stable versions. */ - Json fetchPackageRecipe(string package_id, in VersionRange dep, bool pre_release); + Json fetchPackageRecipe(in PackageName name, in VersionRange dep, bool pre_release); - deprecated("Use the overload that accepts a `VersionRange` instead") - final Json fetchPackageRecipe(string package_id, Dependency dep, bool pre_release) + deprecated("Use `fetchPackageRecipe(PackageName, VersionRange, bool)` instead") + final Json fetchPackageRecipe(string name, Dependency dep, bool pre_release) { return dep.visit!( (const VersionRange rng) { - return this.fetchPackageRecipe(package_id, rng, pre_release); + return this.fetchPackageRecipe(PackageName(name), rng, pre_release); }, (any) { return Json.init; }, @@ -84,9 +100,12 @@ // a package recipe instead of one (first get version list, then the // package recipe) -package Json getBestPackage(Json metadata, string packageId, in VersionRange dep, bool pre_release) +package Json getBestPackage(Json metadata, in PackageName name, + in VersionRange dep, bool pre_release) { import std.exception : enforce; + import std.format : format; + if (metadata.type == Json.Type.null_) return metadata; Json best = null; @@ -102,6 +121,7 @@ } else if (!cur.isPreRelease && cur > bestver) best = json; bestver = Version(cast(string)best["version"]); } - enforce(best != null, "No package candidate found for "~packageId~" "~dep.toString()); + enforce(best != null, + "No package candidate found for %s@%s".format(name.main, dep)); return best; } diff --git a/source/dub/packagesuppliers/registry.d b/source/dub/packagesuppliers/registry.d index b509970..9ba9ccc 100644 --- a/source/dub/packagesuppliers/registry.d +++ b/source/dub/packagesuppliers/registry.d @@ -12,7 +12,7 @@ $(LINK https://code.dlang.org/)) to search for available packages. */ class RegistryPackageSupplier : PackageSupplier { - import dub.internal.utils : download, retryDownload, HTTPStatusException; + import dub.internal.utils : retryDownload, HTTPStatusException; import dub.internal.vibecompat.data.json : parseJson, parseJsonString, serializeToJson; import dub.internal.vibecompat.inet.url : URL; import dub.internal.logging; @@ -22,7 +22,7 @@ private { URL m_registryUrl; struct CacheEntry { Json data; SysTime cacheTime; } - CacheEntry[string] m_metadataCache; + CacheEntry[PackageName] m_metadataCache; Duration m_maxCacheTime; } @@ -34,10 +34,10 @@ override @property string description() { return "registry at "~m_registryUrl.toString(); } - Version[] getVersions(string package_id) + override Version[] getVersions(in PackageName name) { import std.algorithm.sorting : sort; - auto md = getMetadata(package_id); + auto md = getMetadata(name); if (md.type == Json.Type.null_) return null; Version[] ret; @@ -49,63 +49,66 @@ return ret; } - auto genPackageDownloadUrl(string packageId, in VersionRange dep, bool pre_release) + auto genPackageDownloadUrl(in PackageName name, in VersionRange dep, bool pre_release) { import std.array : replace; import std.format : format; import std.typecons : Nullable; - auto md = getMetadata(packageId); - Json best = getBestPackage(md, packageId, dep, pre_release); + auto md = getMetadata(name); + Json best = getBestPackage(md, name, dep, pre_release); Nullable!URL ret; if (best.type != Json.Type.null_) { auto vers = best["version"].get!string; - ret = m_registryUrl ~ NativePath(PackagesPath~"/"~packageId~"/"~vers~".zip"); + ret = m_registryUrl ~ NativePath( + "%s/%s/%s.zip".format(PackagesPath, name.main, vers)); } return ret; } - void fetchPackage(NativePath path, string packageId, in VersionRange dep, bool pre_release) + override ubyte[] fetchPackage(in PackageName name, + in VersionRange dep, bool pre_release) { import std.format : format; - auto url = genPackageDownloadUrl(packageId, dep, pre_release); - if(url.isNull) - return; + + auto url = genPackageDownloadUrl(name, dep, pre_release); + if(url.isNull) return null; try { - retryDownload(url.get, path); - return; + return retryDownload(url.get); } catch(HTTPStatusException e) { if (e.status == 404) throw e; - else logDebug("Failed to download package %s from %s", packageId, url); + else logDebug("Failed to download package %s from %s", name.main, url); } catch(Exception e) { - logDebug("Failed to download package %s from %s", packageId, url); + logDebug("Failed to download package %s from %s", name.main, url); } - throw new Exception("Failed to download package %s from %s".format(packageId, url)); + throw new Exception("Failed to download package %s from %s".format(name.main, url)); } - Json fetchPackageRecipe(string packageId, in VersionRange dep, bool pre_release) + override Json fetchPackageRecipe(in PackageName name, in VersionRange dep, + bool pre_release) { - auto md = getMetadata(packageId); - return getBestPackage(md, packageId, dep, pre_release); + auto md = getMetadata(name); + return getBestPackage(md, name, dep, pre_release); } - private Json getMetadata(string packageId) + private Json getMetadata(in PackageName name) { auto now = Clock.currTime(UTC()); - if (auto pentry = packageId in m_metadataCache) { + if (auto pentry = name.main in m_metadataCache) { if (pentry.cacheTime + m_maxCacheTime > now) return pentry.data; - m_metadataCache.remove(packageId); + m_metadataCache.remove(name.main); } auto url = m_registryUrl ~ NativePath("api/packages/infos"); url.queryString = "packages=" ~ - encodeComponent(`["` ~ packageId ~ `"]`) ~ "&include_dependencies=true&minimize=true"; + encodeComponent(`["` ~ name.main.toString() ~ `"]`) ~ + "&include_dependencies=true&minimize=true"; - logDebug("Downloading metadata for %s", packageId); + logDebug("Downloading metadata for %s", name.main); string jsonData; jsonData = cast(string)retryDownload(url); @@ -114,9 +117,9 @@ foreach (pkg, info; json.get!(Json[string])) { logDebug("adding %s to metadata cache", pkg); - m_metadataCache[pkg] = CacheEntry(info, now); + m_metadataCache[PackageName(pkg)] = CacheEntry(info, now); } - return json[packageId]; + return json[name.main.toString()]; } SearchResult[] searchPackages(string query) { diff --git a/source/dub/platform.d b/source/dub/platform.d index 833061a..9d2150f 100644 --- a/source/dub/platform.d +++ b/source/dub/platform.d @@ -15,7 +15,6 @@ module dub.platform; import std.array; -public import dub.data.platform; // archCheck, compilerCheck, and platformCheck are used below and in // generatePlatformProbeFile, so they've been extracted into these strings @@ -99,6 +98,10 @@ version(Alpha) ret ~= "alpha"; version(Alpha_SoftFP) ret ~= "alpha_softfp"; version(Alpha_HardFP) ret ~= "alpha_hardfp"; + version(LoongArch32) ret ~= "loongarch32"; + version(LoongArch64) ret ~= "loongarch64"; + version(LoongArch_SoftFloat) ret ~= "loongarch_softfloat"; + version(LoongArch_HardFloat) ret ~= "loongarch_hardfloat"; return ret; }; @@ -111,6 +114,21 @@ else return null; }; +/// private +enum string compilerCheckPragmas = q{ + version(DigitalMars) pragma(msg, ` "dmd"`); + else version(GNU) pragma(msg, ` "gdc"`); + else version(LDC) pragma(msg, ` "ldc"`); + else version(SDC) pragma(msg, ` "sdc"`); +}; + +/// private, converts the above appender strings to pragmas +string pragmaGen(string str) { + import std.string : replace; + return str.replace("return ret;", "").replace("string[] ret;", "").replace(`["`, `"`).replace(`", "`,`" "`).replace(`"]`, `"`).replace(`;`, "`);").replace("ret ~= ", "pragma(msg, ` "); +} + + /** Determines the full build platform used for the current build. Note that the `BuildPlatform.compilerBinary` field will be left empty. @@ -167,3 +185,119 @@ { mixin(compilerCheck); } + +/** Matches a platform specification string against a build platform. + + Specifications are build upon the following scheme, where each component + is optional (indicated by []), but the order is obligatory: + "[-platform][-architecture][-compiler]" + + So the following strings are valid specifications: `"-windows-x86-dmd"`, + `"-dmd"`, `"-arm"`, `"-arm-dmd"`, `"-windows-dmd"` + + Params: + platform = The build platform to match against the platform specification + specification = The specification being matched. It must either be an + empty string or start with a dash. + + Returns: + `true` if the given specification matches the build platform, `false` + otherwise. Using an empty string as the platform specification will + always result in a match. +*/ +bool matchesSpecification(in BuildPlatform platform, const(char)[] specification) +{ + import std.string : chompPrefix, format; + import std.algorithm : canFind, splitter; + import std.exception : enforce; + + if (specification.empty) return true; + if (platform == BuildPlatform.any) return true; + + auto splitted = specification.chompPrefix("-").splitter('-'); + enforce(!splitted.empty, format("Platform specification, if present, must not be empty: \"%s\"", specification)); + + if (platform.platform.canFind(splitted.front)) { + splitted.popFront(); + if (splitted.empty) + return true; + } + if (platform.architecture.canFind(splitted.front)) { + splitted.popFront(); + if (splitted.empty) + return true; + } + if (platform.compiler == splitted.front) { + splitted.popFront(); + enforce(splitted.empty, "No valid specification! The compiler has to be the last element: " ~ specification); + return true; + } + return false; +} + +/// +unittest { + auto platform = BuildPlatform(["posix", "linux"], ["x86_64"], "dmd"); + assert(platform.matchesSpecification("")); + assert(platform.matchesSpecification("posix")); + assert(platform.matchesSpecification("linux")); + assert(platform.matchesSpecification("linux-dmd")); + assert(platform.matchesSpecification("linux-x86_64-dmd")); + assert(platform.matchesSpecification("x86_64")); + assert(!platform.matchesSpecification("windows")); + assert(!platform.matchesSpecification("ldc")); + assert(!platform.matchesSpecification("windows-dmd")); + + // Before PR#2279, a leading '-' was required + assert(platform.matchesSpecification("-x86_64")); +} + +/// Represents a platform a package can be build upon. +struct BuildPlatform { + /// Special constant used to denote matching any build platform. + enum any = BuildPlatform(null, null, null, null, -1); + + /// Platform identifiers, e.g. ["posix", "windows"] + string[] platform; + /// CPU architecture identifiers, e.g. ["x86", "x86_64"] + string[] architecture; + /// Canonical compiler name e.g. "dmd" + string compiler; + /// Compiler binary name e.g. "ldmd2" + string compilerBinary; + /// Compiled frontend version (e.g. `2067` for frontend versions 2.067.x) + int frontendVersion; + /// Compiler version e.g. "1.11.0" + string compilerVersion; + /// Frontend version string from frontendVersion + /// e.g: 2067 => "2.067" + string frontendVersionString() const + { + import std.format : format; + + const maj = frontendVersion / 1000; + const min = frontendVersion % 1000; + return format("%d.%03d", maj, min); + } + /// + unittest + { + BuildPlatform bp; + bp.frontendVersion = 2067; + assert(bp.frontendVersionString == "2.067"); + } + + /// Checks to see if platform field contains windows + bool isWindows() const { + import std.algorithm : canFind; + return this.platform.canFind("windows"); + } + /// + unittest { + BuildPlatform bp; + bp.platform = ["windows"]; + assert(bp.isWindows); + bp.platform = ["posix"]; + assert(!bp.isWindows); + } +} diff --git a/source/dub/project.d b/source/dub/project.d index 5826101..0f4ce82 100644 --- a/source/dub/project.d +++ b/source/dub/project.d @@ -57,6 +57,7 @@ project_path = Path of the root package to load pack = An existing `Package` instance to use as the root package */ + deprecated("Load the package using `PackageManager.getOrLoadPackage` then call the `(PackageManager, Package)` overload") this(PackageManager package_manager, NativePath project_path) { Package pack; @@ -103,6 +104,9 @@ */ static package SelectedVersions loadSelections(in Package pack) { + import dub.version_; + import dub.internal.dyaml.stdsumtype; + auto selverfile = (pack.path ~ SelectedVersions.defaultFile).toNativeString(); // No file exists @@ -112,13 +116,23 @@ // TODO: Remove `StrictMode.Warn` after v1.40 release // The default is to error, but as the previous parser wasn't // complaining, we should first warn the user. - auto selected = parseConfigFileSimple!Selected(selverfile, StrictMode.Warn); + auto selected = parseConfigFileSimple!SelectionsFile(selverfile, StrictMode.Warn); // Parsing error, it will be displayed to the user if (selected.isNull()) return new SelectedVersions(); - return new SelectedVersions(selected.get()); + return selected.get().content.match!( + (Selections!0 s) { + logWarnTag("Unsupported version", + "File %s has fileVersion %s, which is not yet supported by DUB %s.", + selverfile, s.fileVersion, dubVersion); + logWarn("Ignoring selections file. Use a newer DUB version " ~ + "and set the appropriate toolchainRequirements in your recipe file"); + return new SelectedVersions(); + }, + (Selections!1 s) => new SelectedVersions(s), + ); } /** List of all resolved dependencies. @@ -180,12 +194,12 @@ if (!cfg.length) deps = p.getAllDependencies(); else { auto depmap = p.getDependencies(cfg); - deps = depmap.byKey.map!(k => PackageDependency(k, depmap[k])).array; + deps = depmap.byKey.map!(k => PackageDependency(PackageName(k), depmap[k])).array; } - deps.sort!((a, b) => a.name < b.name); + deps.sort!((a, b) => a.name.toString() < b.name.toString()); foreach (d; deps) { - auto dependency = getDependency(d.name, true); + auto dependency = getDependency(d.name.toString(), true); assert(dependency || d.spec.optional, format("Non-optional dependency '%s' of '%s' not found in dependency tree!?.", d.name, p.name)); if(dependency) perform_rec(dependency); @@ -419,7 +433,8 @@ ? format(`dependency "%s" repository="git+<git url>" version="<commit>"`, d.name) : format(`"%s": {"repository": "git+<git url>", "version": "<commit>"}`, d.name); logWarn("Dependency '%s' depends on git branch '%s', which is deprecated.", - d.name.color(Mode.bold), d.spec.version_.toString.color(Mode.bold)); + d.name.toString().color(Mode.bold), + d.spec.version_.toString.color(Mode.bold)); logWarnTag("", "Specify the git repository and commit hash in your %s:", (isSDL ? "dub.sdl" : "dub.json").color(Mode.bold)); logWarnTag("", "%s", suggestion.color(Mode.bold)); @@ -432,25 +447,25 @@ ~ "and will have no effect.", pack.color(Mode.bold), config.color(Color.blue)); } - void checkSubConfig(string pack, string config) { - auto p = getDependency(pack, true); + void checkSubConfig(in PackageName name, string config) { + auto p = getDependency(name.toString(), true); if (p && !p.configurations.canFind(config)) { logWarn("The sub configuration directive \"%s\" -> [%s] " ~ "references a configuration that does not exist.", - pack.color(Mode.bold), config.color(Color.red)); + name.toString().color(Mode.bold), config.color(Color.red)); } } auto globalbs = m_rootPackage.getBuildSettings(); foreach (p, c; globalbs.subConfigurations) { if (p !in globalbs.dependencies) warnSubConfig(p, c); - else checkSubConfig(p, c); + else checkSubConfig(PackageName(p), c); } foreach (c; m_rootPackage.configurations) { auto bs = m_rootPackage.getBuildSettings(c); foreach (p, subConf; bs.subConfigurations) { if (p !in bs.dependencies && p !in globalbs.dependencies) warnSubConfig(p, subConf); - else checkSubConfig(p, subConf); + else checkSubConfig(PackageName(p), subConf); } } @@ -461,22 +476,26 @@ pack.simpleLint(); foreach (d; pack.getAllDependencies()) { - auto basename = getBasePackageName(d.name); + auto basename = d.name.main; d.spec.visit!( (NativePath path) { /* Valid */ }, (Repository repo) { /* Valid */ }, (VersionRange vers) { if (m_selections.hasSelectedVersion(basename)) { auto selver = m_selections.getSelectedVersion(basename); - if (d.spec.merge(selver) == Dependency.invalid) { - logWarn(`Selected package %s %s does not match the dependency specification %s in package %s. Need to "%s"?`, - basename.color(Mode.bold), selver, vers, pack.name.color(Mode.bold), "dub upgrade".color(Mode.bold)); + if (d.spec.merge(selver) == Dependency.Invalid) { + logWarn(`Selected package %s@%s does not match ` ~ + `the dependency specification %s in ` ~ + `package %s. Need to "%s"?`, + basename.toString().color(Mode.bold), selver, + vers, pack.name.color(Mode.bold), + "dub upgrade".color(Mode.bold)); } } }, ); - auto deppack = getDependency(d.name, true); + auto deppack = getDependency(d.name.toString(), true); if (deppack in visited) continue; visited[deppack] = true; if (deppack) validateDependenciesRec(deppack); @@ -485,119 +504,132 @@ validateDependenciesRec(m_rootPackage); } - /// Reloads dependencies. + /** + * Reloads dependencies + * + * This function goes through the project and make sure that all + * required packages are loaded. To do so, it uses information + * both from the recipe file (`dub.json`) and from the selections + * file (`dub.selections.json`). + * + * In the process, it populates the `dependencies`, `missingDependencies`, + * and `hasAllDependencies` properties, which can only be relied on + * once this has run once (the constructor always calls this). + */ void reinit() { m_dependencies = null; m_missingDependencies = []; - - Package resolveSubPackage(Package p, string subname, bool silentFail) { - if (!subname.length || p is null) - return p; - return m_packageManager.getSubPackage(p, subname, silentFail); - } - - void collectDependenciesRec(Package pack, int depth = 0) - { - auto indent = replicate(" ", depth); - logDebug("%sCollecting dependencies for %s", indent, pack.name); - indent ~= " "; - - foreach (dep; pack.getAllDependencies()) { - Dependency vspec = dep.spec; - Package p; - - auto basename = getBasePackageName(dep.name); - auto subname = getSubPackageName(dep.name); - - // non-optional and optional-default dependencies (if no selections file exists) - // need to be satisfied - bool is_desired = !vspec.optional || m_selections.hasSelectedVersion(basename) || (vspec.default_ && m_selections.bare); - - if (dep.name == m_rootPackage.basePackage.name) { - vspec = Dependency(m_rootPackage.version_); - p = m_rootPackage.basePackage; - } else if (basename == m_rootPackage.basePackage.name) { - vspec = Dependency(m_rootPackage.version_); - try p = m_packageManager.getSubPackage(m_rootPackage.basePackage, subname, false); - catch (Exception e) { - logDiagnostic("%sError getting sub package %s: %s", indent, dep.name, e.msg); - if (is_desired) m_missingDependencies ~= dep.name; - continue; - } - } else if (m_selections.hasSelectedVersion(basename)) { - vspec = m_selections.getSelectedVersion(basename); - p = vspec.visit!( - (NativePath path_) { - auto path = path_.absolute ? path_ : m_rootPackage.path ~ path_; - auto tmp = m_packageManager.getOrLoadPackage(path, NativePath.init, true); - return resolveSubPackage(tmp, subname, true); - }, - (Repository repo) { - auto tmp = m_packageManager.loadSCMPackage(basename, repo); - return resolveSubPackage(tmp, subname, true); - }, - (VersionRange range) { - // See `dub.recipe.selection : SelectedDependency.fromYAML` - assert(range.isExactVersion()); - return m_packageManager.getPackage(dep.name, vspec.version_); - }, - ); - } else if (m_dependencies.canFind!(d => getBasePackageName(d.name) == basename)) { - auto idx = m_dependencies.countUntil!(d => getBasePackageName(d.name) == basename); - auto bp = m_dependencies[idx].basePackage; - vspec = Dependency(bp.path); - p = resolveSubPackage(bp, subname, false); - } else { - logDiagnostic("%sVersion selection for dependency %s (%s) of %s is missing.", - indent, basename, dep.name, pack.name); - } - - // We didn't find the package - if (p is null) - { - if (!vspec.repository.empty) { - p = m_packageManager.loadSCMPackage(basename, vspec.repository); - resolveSubPackage(p, subname, false); - enforce(p !is null, - "Unable to fetch '%s@%s' using git - does the repository and version exists?".format( - dep.name, vspec.repository)); - } else if (!vspec.path.empty && is_desired) { - NativePath path = vspec.path; - if (!path.absolute) path = pack.path ~ path; - logDiagnostic("%sAdding local %s in %s", indent, dep.name, path); - p = m_packageManager.getOrLoadPackage(path, NativePath.init, true); - if (p.parentPackage !is null) { - logWarn("%sSub package %s must be referenced using the path to it's parent package.", indent, dep.name); - p = p.parentPackage; - } - p = resolveSubPackage(p, subname, false); - enforce(p.name == dep.name, - format("Path based dependency %s is referenced with a wrong name: %s vs. %s", - path.toNativeString(), dep.name, p.name)); - } else { - logDiagnostic("%sMissing dependency %s %s of %s", indent, dep.name, vspec, pack.name); - if (is_desired) m_missingDependencies ~= dep.name; - continue; - } - } - - if (!m_dependencies.canFind(p)) { - logDiagnostic("%sFound dependency %s %s", indent, dep.name, vspec.toString()); - m_dependencies ~= p; - if (basename == m_rootPackage.basePackage.name) - p.warnOnSpecialCompilerFlags(); - collectDependenciesRec(p, depth+1); - } - - m_dependees[p] ~= pack; - //enforce(p !is null, "Failed to resolve dependency "~dep.name~" "~vspec.toString()); - } - } collectDependenciesRec(m_rootPackage); m_missingDependencies.sort(); } + /// Implementation of `reinit` + private void collectDependenciesRec(Package pack, int depth = 0) + { + auto indent = replicate(" ", depth); + logDebug("%sCollecting dependencies for %s", indent, pack.name); + indent ~= " "; + + foreach (dep; pack.getAllDependencies()) { + Dependency vspec = dep.spec; + Package p; + + auto basename = dep.name.main; + auto subname = dep.name.sub; + + // non-optional and optional-default dependencies (if no selections file exists) + // need to be satisfied + bool is_desired = !vspec.optional || m_selections.hasSelectedVersion(basename) || (vspec.default_ && m_selections.bare); + + if (dep.name.toString() == m_rootPackage.basePackage.name) { + vspec = Dependency(m_rootPackage.version_); + p = m_rootPackage.basePackage; + } else if (basename.toString() == m_rootPackage.basePackage.name) { + vspec = Dependency(m_rootPackage.version_); + try p = m_packageManager.getSubPackage(m_rootPackage.basePackage, subname, false); + catch (Exception e) { + logDiagnostic("%sError getting sub package %s: %s", indent, dep.name, e.msg); + if (is_desired) m_missingDependencies ~= dep.name.toString(); + continue; + } + } else if (m_selections.hasSelectedVersion(basename)) { + vspec = m_selections.getSelectedVersion(basename); + p = vspec.visit!( + (NativePath path_) { + auto path = path_.absolute ? path_ : m_rootPackage.path ~ path_; + auto tmp = m_packageManager.getOrLoadPackage(path, NativePath.init, true); + return resolveSubPackage(tmp, subname, true); + }, + (Repository repo) { + auto tmp = m_packageManager.loadSCMPackage(basename, repo); + return resolveSubPackage(tmp, subname, true); + }, + (VersionRange range) { + // See `dub.recipe.selection : SelectedDependency.fromYAML` + assert(range.isExactVersion()); + return m_packageManager.getPackage(dep.name, vspec.version_); + }, + ); + } else if (m_dependencies.canFind!(d => PackageName(d.name).main == basename)) { + auto idx = m_dependencies.countUntil!(d => PackageName(d.name).main == basename); + auto bp = m_dependencies[idx].basePackage; + vspec = Dependency(bp.path); + p = resolveSubPackage(bp, subname, false); + } else { + logDiagnostic("%sVersion selection for dependency %s (%s) of %s is missing.", + indent, basename, dep.name, pack.name); + } + + // We didn't find the package + if (p is null) + { + if (!vspec.repository.empty) { + p = m_packageManager.loadSCMPackage(basename, vspec.repository); + resolveSubPackage(p, subname, false); + enforce(p !is null, + "Unable to fetch '%s@%s' using git - does the repository and version exists?".format( + dep.name, vspec.repository)); + } else if (!vspec.path.empty && is_desired) { + NativePath path = vspec.path; + if (!path.absolute) path = pack.path ~ path; + logDiagnostic("%sAdding local %s in %s", indent, dep.name, path); + p = m_packageManager.getOrLoadPackage(path, NativePath.init, true); + if (p.parentPackage !is null) { + logWarn("%sSub package %s must be referenced using the path to it's parent package.", indent, dep.name); + p = p.parentPackage; + } + p = resolveSubPackage(p, subname, false); + enforce(p.name == dep.name.toString(), + format("Path based dependency %s is referenced with a wrong name: %s vs. %s", + path.toNativeString(), dep.name, p.name)); + } else { + logDiagnostic("%sMissing dependency %s %s of %s", indent, dep.name, vspec, pack.name); + if (is_desired) m_missingDependencies ~= dep.name.toString(); + continue; + } + } + + if (!m_dependencies.canFind(p)) { + logDiagnostic("%sFound dependency %s %s", indent, dep.name, vspec.toString()); + m_dependencies ~= p; + if (basename.toString() == m_rootPackage.basePackage.name) + p.warnOnSpecialCompilerFlags(); + collectDependenciesRec(p, depth+1); + } + + m_dependees[p] ~= pack; + //enforce(p !is null, "Failed to resolve dependency "~dep.name~" "~vspec.toString()); + } + } + + /// Convenience function used by `reinit` + private Package resolveSubPackage(Package p, string subname, bool silentFail) { + if (!subname.length || p is null) + return p; + return m_packageManager.getSubPackage(p, subname, silentFail); + } + /// Returns the name of the root package. @property string name() const { return m_rootPackage ? m_rootPackage.name : "app"; } @@ -620,7 +652,7 @@ parents[m_rootPackage.name] = null; foreach (p; getTopologicalPackageList()) foreach (d; p.getAllDependencies()) - parents[d.name] ~= p.name; + parents[d.name.toString()] ~= p.name; size_t createConfig(string pack, string config) { foreach (i, v; configs) @@ -694,7 +726,7 @@ { string[][string] depconfigs; foreach (d; p.getAllDependencies()) { - auto dp = getDependency(d.name, true); + auto dp = getDependency(d.name.toString(), true); if (!dp) continue; string[] cfgs; @@ -704,7 +736,7 @@ if (!subconf.empty) cfgs = [subconf]; else cfgs = dp.getPlatformConfigurations(platform); } - cfgs = cfgs.filter!(c => haveConfig(d.name, c)).array; + cfgs = cfgs.filter!(c => haveConfig(d.name.toString(), c)).array; // if no valid configuration was found for a dependency, don't include the // current configuration @@ -712,14 +744,14 @@ logDebug("Skip %s %s (missing configuration for %s)", p.name, c, dp.name); return; } - depconfigs[d.name] = cfgs; + depconfigs[d.name.toString()] = cfgs; } // add this configuration to the graph size_t cidx = createConfig(p.name, c); foreach (d; p.getAllDependencies()) - foreach (sc; depconfigs.get(d.name, null)) - createEdge(cidx, createConfig(d.name, sc)); + foreach (sc; depconfigs.get(d.name.toString(), null)) + createEdge(cidx, createConfig(d.name.toString(), sc)); } // create a graph of all possible package configurations (package, config) -> (sub-package, sub-config) @@ -732,7 +764,7 @@ // first, add all dependency configurations foreach (d; p.getAllDependencies) { - auto dp = getDependency(d.name, true); + auto dp = getDependency(d.name.toString(), true); if (!dp) continue; determineAllConfigs(dp); } @@ -1305,12 +1337,12 @@ void saveSelections() { assert(m_selections !is null, "Cannot save selections for non-disk based project (has no selections)."); - if (m_selections.hasSelectedVersion(m_rootPackage.basePackage.name)) - m_selections.deselectVersion(m_rootPackage.basePackage.name); - - auto path = m_rootPackage.path ~ SelectedVersions.defaultFile; - if (m_selections.dirty || !existsFile(path)) - m_selections.save(path); + const name = PackageName(m_rootPackage.basePackage.name); + if (m_selections.hasSelectedVersion(name)) + m_selections.deselectVersion(name); + this.m_packageManager.writeSelections( + this.m_rootPackage, this.m_selections.m_selections, + this.m_selections.dirty); } deprecated bool isUpgradeCacheUpToDate() @@ -1735,15 +1767,20 @@ environment.remove("MY_ENV_VAR"); } -/** Holds and stores a set of version selections for package dependencies. - - This is the runtime representation of the information contained in - "dub.selections.json" within a package's directory. -*/ +/** + * Holds and stores a set of version selections for package dependencies. + * + * This is the runtime representation of the information contained in + * "dub.selections.json" within a package's directory. + * + * Note that as subpackages share the same version as their main package, + * this class will treat any subpackage reference as a reference to its + * main package. + */ public class SelectedVersions { protected { enum FileVersion = 1; - Selected m_selections; + Selections!1 m_selections; bool m_dirty = false; // has changes since last save bool m_bare = true; } @@ -1752,13 +1789,14 @@ enum defaultFile = "dub.selections.json"; /// Constructs a new empty version selection. - public this(uint version_ = FileVersion) @safe pure nothrow @nogc + public this(uint version_ = FileVersion) @safe pure { - this.m_selections = Selected(version_); + enforce(version_ == 1, "Unsupported file version"); + this.m_selections = Selections!1(version_); } /// Constructs a new non-empty version selection. - public this(Selected data) @safe pure nothrow @nogc + public this(Selections!1 data) @safe pure nothrow @nogc { this.m_selections = data; this.m_bare = false; @@ -1812,36 +1850,58 @@ } /// Selects a certain version for a specific package. + deprecated("Use the overload that accepts a `PackageName`") void selectVersion(string package_id, Version version_) { - if (auto pdep = package_id in m_selections.versions) { - if (*pdep == Dependency(version_)) - return; - } - m_selections.versions[package_id] = Dependency(version_); - m_dirty = true; + const name = PackageName(package_id); + return this.selectVersion(name, version_); + } + + /// Ditto + void selectVersion(in PackageName name, Version version_) + { + const dep = Dependency(version_); + this.selectVersionInternal(name, dep); } /// Selects a certain path for a specific package. + deprecated("Use the overload that accepts a `PackageName`") void selectVersion(string package_id, NativePath path) { - if (auto pdep = package_id in m_selections.versions) { - if (*pdep == Dependency(path)) - return; - } - m_selections.versions[package_id] = Dependency(path); - m_dirty = true; + const name = PackageName(package_id); + return this.selectVersion(name, path); + } + + /// Ditto + void selectVersion(in PackageName name, NativePath path) + { + const dep = Dependency(path); + this.selectVersionInternal(name, dep); } /// Selects a certain Git reference for a specific package. + deprecated("Use the overload that accepts a `PackageName`") void selectVersion(string package_id, Repository repository) { - const dependency = Dependency(repository); - if (auto pdep = package_id in m_selections.versions) { - if (*pdep == dependency) + const name = PackageName(package_id); + return this.selectVersion(name, repository); + } + + /// Ditto + void selectVersion(in PackageName name, Repository repository) + { + const dep = Dependency(repository); + this.selectVersionInternal(name, dep); + } + + /// Internal implementation of selectVersion + private void selectVersionInternal(in PackageName name, in Dependency dep) + { + if (auto pdep = name.main.toString() in m_selections.versions) { + if (*pdep == dep) return; } - m_selections.versions[package_id] = dependency; + m_selections.versions[name.main.toString()] = dep; m_dirty = true; } @@ -1852,16 +1912,31 @@ } /// Removes the selection for a particular package. + deprecated("Use the overload that accepts a `PackageName`") void deselectVersion(string package_id) { - m_selections.versions.remove(package_id); + const n = PackageName(package_id); + this.deselectVersion(n); + } + + /// Ditto + void deselectVersion(in PackageName name) + { + m_selections.versions.remove(name.main.toString()); m_dirty = true; } /// Determines if a particular package has a selection set. - bool hasSelectedVersion(string packageId) - const { - return (packageId in m_selections.versions) !is null; + deprecated("Use the overload that accepts a `PackageName`") + bool hasSelectedVersion(string packageId) const { + const name = PackageName(packageId); + return this.hasSelectedVersion(name); + } + + /// Ditto + bool hasSelectedVersion(in PackageName name) const + { + return (name.main.toString() in m_selections.versions) !is null; } /** Returns the selection for a particular package. @@ -1871,10 +1946,18 @@ is a path based selection, or its `Dependency.version_` property is valid and it is a version selection. */ - Dependency getSelectedVersion(string packageId) - const { - enforce(hasSelectedVersion(packageId)); - return m_selections.versions[packageId]; + deprecated("Use the overload that accepts a `PackageName`") + Dependency getSelectedVersion(string packageId) const + { + const name = PackageName(packageId); + return this.getSelectedVersion(name); + } + + /// Ditto + Dependency getSelectedVersion(in PackageName name) const + { + enforce(hasSelectedVersion(name)); + return m_selections.versions[name.main.toString()]; } /** Stores the selections to disk. @@ -1883,30 +1966,10 @@ should be used as the file name and the directory should be the root directory of the project's root package. */ + deprecated("Use `PackageManager.writeSelections` to write a `SelectionsFile`") void save(NativePath path) { - Json json = serialize(); - auto result = appender!string(); - - assert(json.type == Json.Type.object); - assert(json.length == 2); - assert(json["versions"].type != Json.Type.undefined); - - result.put("{\n\t\"fileVersion\": "); - result.writeJsonString(json["fileVersion"]); - result.put(",\n\t\"versions\": {"); - auto vers = json["versions"].get!(Json[string]); - bool first = true; - foreach (k; vers.byKey.array.sort()) { - if (!first) result.put(","); - else first = false; - result.put("\n\t\t"); - result.writeJsonString(Json(k)); - result.put(": "); - result.writeJsonString(vers[k]); - } - result.put("\n\t}\n}\n"); - path.writeFile(result.data); + path.writeFile(PackageManager.selectionsToString(this.m_selections)); m_dirty = false; m_bare = false; } @@ -1930,15 +1993,9 @@ else throw new Exception(format("Unexpected type for dependency: %s", j)); } - Json serialize() - const { - Json json = serializeToJson(m_selections); - Json serialized = Json.emptyObject; - serialized["fileVersion"] = m_selections.fileVersion; - serialized["versions"] = Json.emptyObject; - foreach (p, dep; m_selections.versions) - serialized["versions"][p] = dep.toJson(true); - return serialized; + deprecated("JSON serialization is deprecated") + Json serialize() const { + return PackageManager.selectionsToJSON(this.m_selections); } deprecated("JSON deserialization is deprecated") @@ -1956,6 +2013,7 @@ /// The template code from which the test runner is generated private immutable TestRunnerTemplate = q{ +deprecated // allow silently using deprecated symbols module dub_test_root; import std.typetuple; diff --git a/source/dub/recipe/io.d b/source/dub/recipe/io.d index 5fedf46..f12fdb5 100644 --- a/source/dub/recipe/io.d +++ b/source/dub/recipe/io.d @@ -7,6 +7,7 @@ */ module dub.recipe.io; +import dub.dependency : PackageName; import dub.recipe.packagerecipe; import dub.internal.logging; import dub.internal.vibecompat.core.file; @@ -19,26 +20,34 @@ Params: filename = NativePath of the package recipe file - parent_name = Optional name of the parent package (if this is a sub package) + parent = Optional name of the parent package (if this is a sub package) mode = Whether to issue errors, warning, or ignore unknown keys in dub.json Returns: Returns the package recipe contents Throws: Throws an exception if an I/O or syntax error occurs */ +deprecated("Use the overload that accepts a `NativePath` as first argument") PackageRecipe readPackageRecipe( - string filename, string parent_name = null, StrictMode mode = StrictMode.Ignore) + string filename, string parent = null, StrictMode mode = StrictMode.Ignore) { - return readPackageRecipe(NativePath(filename), parent_name, mode); + return readPackageRecipe(NativePath(filename), parent, mode); } /// ditto +deprecated("Use the overload that accepts a `PackageName` as second argument") PackageRecipe readPackageRecipe( - NativePath filename, string parent_name = null, StrictMode mode = StrictMode.Ignore) + NativePath filename, string parent, StrictMode mode = StrictMode.Ignore) { - import dub.internal.utils : stripUTF8Bom; + return readPackageRecipe(filename, parent.length ? PackageName(parent) : PackageName.init, mode); +} - string text = stripUTF8Bom(cast(string)readFile(filename)); - return parsePackageRecipe(text, filename.toNativeString(), parent_name, null, mode); + +/// ditto +PackageRecipe readPackageRecipe(NativePath filename, + in PackageName parent = PackageName.init, StrictMode mode = StrictMode.Ignore) +{ + string text = readText(filename); + return parsePackageRecipe(text, filename.toNativeString(), parent, null, mode); } /** Parses an in-memory package recipe. @@ -49,7 +58,7 @@ contents = The contents of the recipe file filename = Name associated with the package recipe - this is only used to determine the file format from the file extension - parent_name = Optional name of the parent package (if this is a sub + parent = Optional name of the parent package (if this is a sub package) default_package_name = Optional default package name (if no package name is found in the recipe this value will be used) @@ -58,7 +67,18 @@ Returns: Returns the package recipe contents Throws: Throws an exception if an I/O or syntax error occurs */ -PackageRecipe parsePackageRecipe(string contents, string filename, string parent_name = null, +deprecated("Use the overload that accepts a `PackageName` as 3rd argument") +PackageRecipe parsePackageRecipe(string contents, string filename, string parent, + string default_package_name = null, StrictMode mode = StrictMode.Ignore) +{ + return parsePackageRecipe(contents, filename, parent.length ? + PackageName(parent) : PackageName.init, + default_package_name, mode); +} + +/// Ditto +PackageRecipe parsePackageRecipe(string contents, string filename, + in PackageName parent = PackageName.init, string default_package_name = null, StrictMode mode = StrictMode.Ignore) { import std.algorithm : endsWith; @@ -82,7 +102,7 @@ logWarn("Error was: %s", exc); // Fallback to JSON parser ret = PackageRecipe.init; - parseJson(ret, parseJsonString(contents, filename), parent_name); + parseJson(ret, parseJsonString(contents, filename), parent); } catch (Exception exc) { logWarn("Your `dub.json` file use non-conventional features that are deprecated"); logWarn("This is most likely due to duplicated keys."); @@ -90,7 +110,7 @@ logWarn("Error was: %s", exc); // Fallback to JSON parser ret = PackageRecipe.init; - parseJson(ret, parseJsonString(contents, filename), parent_name); + parseJson(ret, parseJsonString(contents, filename), parent); } // `debug = ConfigFillerDebug` also enables verbose parser output debug (ConfigFillerDebug) @@ -111,7 +131,7 @@ } } } - else if (filename.endsWith(".sdl")) parseSDL(ret, contents, parent_name, filename); + else if (filename.endsWith(".sdl")) parseSDL(ret, contents, parent, filename); else assert(false, "readPackageRecipe called with filename with unknown extension: "~filename); // Fix for issue #711: `targetType` should be inherited, or default to library diff --git a/source/dub/recipe/json.d b/source/dub/recipe/json.d index 505baff..bb48990 100644 --- a/source/dub/recipe/json.d +++ b/source/dub/recipe/json.d @@ -20,8 +20,14 @@ import std.string : format, indexOf; import std.traits : EnumMembers; +deprecated("Use the overload that takes a `PackageName` as 3rd argument") +void parseJson(ref PackageRecipe recipe, Json json, string parent) +{ + const PackageName pname = parent ? PackageName(parent) : PackageName.init; + parseJson(recipe, json, pname); +} -void parseJson(ref PackageRecipe recipe, Json json, string parent_name) +void parseJson(ref PackageRecipe recipe, Json json, in PackageName parent = PackageName.init) { foreach (string field, value; json) { switch (field) { @@ -37,7 +43,7 @@ case "buildTypes": foreach (string name, settings; value) { BuildSettingsTemplate bs; - bs.parseJson(settings, null); + bs.parseJson(settings, PackageName.init); recipe.buildTypes[name] = bs; } break; @@ -51,7 +57,9 @@ enforce(recipe.name.length > 0, "The package \"name\" field is missing or empty."); - auto fullname = parent_name.length ? parent_name ~ ":" ~ recipe.name : recipe.name; + const fullname = parent.toString().length + ? PackageName(parent.toString() ~ ":" ~ recipe.name) + : PackageName(recipe.name); // parse build settings recipe.buildSettings.parseJson(json, fullname); @@ -59,7 +67,7 @@ if (auto pv = "configurations" in json) { foreach (settings; *pv) { ConfigurationInfo ci; - ci.parseJson(settings, recipe.name); + ci.parseJson(settings, fullname); recipe.configurations ~= ci; } } @@ -110,10 +118,10 @@ return ret; } -private void parseSubPackages(ref PackageRecipe recipe, string parent_package_name, Json[] subPackagesJson) +private void parseSubPackages(ref PackageRecipe recipe, in PackageName parent, Json[] subPackagesJson) { - enforce(!parent_package_name.canFind(":"), format("'subPackages' found in '%s'. This is only supported in the main package file for '%s'.", - parent_package_name, getBasePackageName(parent_package_name))); + enforce(!parent.sub, format("'subPackages' found in '%s'. This is only supported in the main package file for '%s'.", + parent, parent.main)); recipe.subPackages = new SubPackage[subPackagesJson.length]; foreach (i, subPackageJson; subPackagesJson) { @@ -123,13 +131,13 @@ recipe.subPackages[i] = SubPackage(subpath, PackageRecipe.init); } else { PackageRecipe subinfo; - subinfo.parseJson(subPackageJson, parent_package_name); + subinfo.parseJson(subPackageJson, parent); recipe.subPackages[i] = SubPackage(null, subinfo); } } } -private void parseJson(ref ConfigurationInfo config, Json json, string package_name) +private void parseJson(ref ConfigurationInfo config, Json json, in PackageName pname) { foreach (string name, value; json) { switch (name) { @@ -143,7 +151,7 @@ } enforce(!config.name.empty, "Configuration is missing a name."); - config.buildSettings.parseJson(json, package_name); + config.buildSettings.parseJson(json, pname); } private Json toJson(const scope ref ConfigurationInfo config) @@ -154,7 +162,7 @@ return ret; } -private void parseJson(ref BuildSettingsTemplate bs, Json json, string package_name) +private void parseJson(ref BuildSettingsTemplate bs, Json json, in PackageName pname) { foreach(string name, value; json) { @@ -167,13 +175,15 @@ case "dependencies": foreach (string pkg, verspec; value) { if (pkg.startsWith(":")) { - enforce(!package_name.canFind(':'), format("Short-hand packages syntax not allowed within sub packages: %s -> %s", package_name, pkg)); - pkg = package_name ~ pkg; + enforce(!pname.sub.length, + "Short-hand packages syntax not allowed within " ~ + "sub packages: %s -> %s".format(pname, pkg)); + pkg = pname.toString() ~ pkg; } enforce(pkg !in bs.dependencies, "The dependency '"~pkg~"' is specified more than once." ); bs.dependencies[pkg] = Dependency.fromJson(verspec); if (verspec.type == Json.Type.object) - bs.dependencies[pkg].settings.parseJson(verspec, package_name); + bs.dependencies[pkg].settings.parseJson(verspec, pname); } break; case "systemDependencies": @@ -376,9 +386,10 @@ `.strip.outdent; auto jsonValue = parseJsonString(json); PackageRecipe rec1; - parseJson(rec1, jsonValue, null); + parseJson(rec1, jsonValue); PackageRecipe rec; - parseJson(rec, rec1.toJson(), null); // verify that all fields are serialized properly + // verify that all fields are serialized properly + parseJson(rec, rec1.toJson()); assert(rec.name == "projectname"); assert(rec.buildSettings.environments == ["": ["Var1": "env"]]); diff --git a/source/dub/recipe/packagerecipe.d b/source/dub/recipe/packagerecipe.d index 5573018..44f34b0 100644 --- a/source/dub/recipe/packagerecipe.d +++ b/source/dub/recipe/packagerecipe.d @@ -32,17 +32,25 @@ example, "packa:packb:packc" references a package named "packc" that is a sub package of "packb", which in turn is a sub package of "packa". */ +deprecated("This function is not supported as subpackages cannot be nested") string[] getSubPackagePath(string package_name) @safe pure { return package_name.split(":"); } +deprecated @safe unittest +{ + assert(getSubPackagePath("packa:packb:packc") == ["packa", "packb", "packc"]); + assert(getSubPackagePath("pack") == ["pack"]); +} + /** Returns the name of the top level package for a given (sub) package name of format `"basePackageName"` or `"basePackageName:subPackageName"`. In case of a top level package, the qualified name is returned unmodified. */ +deprecated("Use `dub.dependency : PackageName(arg).main` instead") string getBasePackageName(string package_name) @safe pure { return package_name.findSplit(":")[0]; @@ -55,15 +63,14 @@ This is the part of the package name excluding the base package name. See also $(D getBasePackageName). */ +deprecated("Use `dub.dependency : PackageName(arg).sub` instead") string getSubPackageName(string package_name) @safe pure { return package_name.findSplit(":")[2]; } -@safe unittest +deprecated @safe unittest { - assert(getSubPackagePath("packa:packb:packc") == ["packa", "packb", "packc"]); - assert(getSubPackagePath("pack") == ["pack"]); assert(getBasePackageName("packa:packb:packc") == "packa"); assert(getBasePackageName("pack") == "pack"); assert(getSubPackageName("packa:packb:packc") == "packb:packc"); diff --git a/source/dub/recipe/sdl.d b/source/dub/recipe/sdl.d index 5d4d909..e6872c2 100644 --- a/source/dub/recipe/sdl.d +++ b/source/dub/recipe/sdl.d @@ -20,13 +20,26 @@ import std.conv; import std.string : startsWith, format; +deprecated("Use `parseSDL(PackageRecipe, string, PackageName, string)` instead") void parseSDL(ref PackageRecipe recipe, string sdl, string parent_name, string filename) { - parseSDL(recipe, parseSource(sdl, filename), parent_name); + parseSDL(recipe, parseSource(sdl, filename), PackageName(parent_name)); } +deprecated("Use `parseSDL(PackageRecipe, Tag, PackageName)` instead") void parseSDL(ref PackageRecipe recipe, Tag sdl, string parent_name) { + parseSDL(recipe, sdl, PackageName(parent_name)); +} + +void parseSDL(ref PackageRecipe recipe, string sdl, in PackageName parent, + string filename) +{ + parseSDL(recipe, parseSource(sdl, filename), parent); +} + +void parseSDL(ref PackageRecipe recipe, Tag sdl, in PackageName parent = PackageName.init) +{ Tag[] subpacks; Tag[] configs; @@ -47,7 +60,7 @@ case "buildType": auto name = n.stringTagValue(true); BuildSettingsTemplate bt; - parseBuildSettings(n, bt, parent_name); + parseBuildSettings(n, bt, parent); recipe.buildTypes[name] = bt; break; case "toolchainRequirements": @@ -59,7 +72,9 @@ } enforceSDL(recipe.name.length > 0, "The package \"name\" field is missing or empty.", sdl); - string full_name = parent_name.length ? parent_name ~ ":" ~ recipe.name : recipe.name; + const full_name = parent.toString().length + ? PackageName(parent.toString() ~ ":" ~ recipe.name) + : PackageName(recipe.name); // parse general build settings parseBuildSettings(sdl, recipe.buildSettings, full_name); @@ -119,17 +134,19 @@ return ret; } -private void parseBuildSettings(Tag settings, ref BuildSettingsTemplate bs, string package_name) +private void parseBuildSettings(Tag settings, ref BuildSettingsTemplate bs, + in PackageName name) { foreach (setting; settings.all.tags) - parseBuildSetting(setting, bs, package_name); + parseBuildSetting(setting, bs, name); } -private void parseBuildSetting(Tag setting, ref BuildSettingsTemplate bs, string package_name) +private void parseBuildSetting(Tag setting, ref BuildSettingsTemplate bs, + in PackageName name) { switch (setting.fullName) { default: break; - case "dependency": parseDependency(setting, bs, package_name); break; + case "dependency": parseDependency(setting, bs, name); break; case "systemDependencies": bs.systemDependencies = setting.stringTagValue; break; case "targetType": bs.targetType = setting.stringTagValue.to!TargetType; break; case "targetName": bs.targetName = setting.stringTagValue; break; @@ -138,7 +155,7 @@ case "subConfiguration": auto args = setting.stringArrayTagValue; enforceSDL(args.length == 2, "Expecting package and configuration names as arguments.", setting); - bs.subConfigurations[expandPackageName(args[0], package_name, setting)] = args[1]; + bs.subConfigurations[expandPackageName(args[0], name, setting)] = args[1]; break; case "dflags": setting.parsePlatformStringArray(bs.dflags); break; case "lflags": setting.parsePlatformStringArray(bs.lflags); break; @@ -178,14 +195,14 @@ } } -private void parseDependency(Tag t, ref BuildSettingsTemplate bs, string package_name) +private void parseDependency(Tag t, ref BuildSettingsTemplate bs, in PackageName name) { enforceSDL(t.values.length != 0, "Missing dependency name.", t); enforceSDL(t.values.length == 1, "Multiple dependency names.", t); - auto pkg = expandPackageName(t.values[0].expect!string(t), package_name, t); + auto pkg = expandPackageName(t.values[0].expect!string(t), name, t); enforceSDL(pkg !in bs.dependencies, "The dependency '"~pkg~"' is specified more than once.", t); - Dependency dep = Dependency.any; + Dependency dep = Dependency.Any; auto attrs = t.attributes; if ("path" in attrs) { @@ -209,15 +226,15 @@ bs.dependencies[pkg] = dep; BuildSettingsTemplate dbs; - parseBuildSettings(t, bs.dependencies[pkg].settings, package_name); + parseBuildSettings(t, bs.dependencies[pkg].settings, name); } -private void parseConfiguration(Tag t, ref ConfigurationInfo ret, string package_name) +private void parseConfiguration(Tag t, ref ConfigurationInfo ret, in PackageName name) { ret.name = t.stringTagValue(true); foreach (f; t.tags) { switch (f.fullName) { - default: parseBuildSetting(f, ret.buildSettings, package_name); break; + default: parseBuildSetting(f, ret.buildSettings, name); break; case "platforms": ret.platforms ~= f.stringArrayTagValue; break; } } @@ -335,13 +352,14 @@ return new Tag(null, "toolchainRequirements", null, attrs); } -private string expandPackageName(string name, string parent_name, Tag tag) +private string expandPackageName(string name, in PackageName parent, Tag tag) { import std.algorithm : canFind; - if (name.startsWith(":")) { - enforceSDL(!parent_name.canFind(':'), format("Short-hand packages syntax not allowed within sub packages: %s -> %s", parent_name, name), tag); - return parent_name ~ name; - } else return name; + if (!name.startsWith(":")) + return name; + enforceSDL(!parent.sub.length, "Short-hand packages syntax not " ~ + "allowed within sub packages: %s -> %s".format(parent, name), tag); + return parent.toString() ~ name; } private string stringTagValue(Tag t, bool allow_child_tags = false) @@ -424,6 +442,11 @@ } } +// Just a wrapper around `parseSDL` for easier testing +version (unittest) private void parseSDLTest(ref PackageRecipe recipe, string sdl) +{ + parseSDL(recipe, parseSource(sdl, "testfile"), PackageName.init); +} unittest { // test all possible fields auto sdl = @@ -533,9 +556,9 @@ lflags "lf3" `; PackageRecipe rec1; - parseSDL(rec1, sdl, null, "testfile"); + parseSDLTest(rec1, sdl); PackageRecipe rec; - parseSDL(rec, rec1.toSDL(), null); // verify that all fields are serialized properly + parseSDL(rec, rec1.toSDL()); // verify that all fields are serialized properly assert(rec.name == "projectname"); assert(rec.description == "project description"); @@ -630,7 +653,7 @@ dflags "-j" platform="linux" `; PackageRecipe rec; - parseSDL(rec, sdl, null, "testfile"); + parseSDLTest(rec, sdl); assert(rec.buildSettings.dflags.length == 3); assert(rec.buildSettings.dflags["windows-x86"] == ["-a", "-b", "-c"]); assert(rec.buildSettings.dflags[""] == ["-e", "-f", "-g"]); @@ -641,23 +664,22 @@ import std.exception; auto sdl = `description "missing name"`; PackageRecipe rec; - assertThrown(parseSDL(rec, sdl, null, "testfile")); + assertThrown(parseSDLTest(rec, sdl)); } unittest { // test single value fields import std.exception; PackageRecipe rec; - assertThrown!Exception(parseSDL(rec, `name "hello" "world"`, null, "testfile")); - assertThrown!Exception(parseSDL(rec, `name`, null, "testfile")); - assertThrown!Exception(parseSDL(rec, `name 10`, null, "testfile")); - assertThrown!Exception(parseSDL(rec, + assertThrown!Exception(parseSDLTest(rec, `name "hello" "world"`)); + assertThrown!Exception(parseSDLTest(rec, `name`)); + assertThrown!Exception(parseSDLTest(rec, `name 10`)); + assertThrown!Exception(parseSDLTest(rec, `name "hello" { world - }`, null, "testfile")); - assertThrown!Exception(parseSDL(rec, + }`)); + assertThrown!Exception(parseSDLTest(rec, `name "" - versions "hello" 10` - , null, "testfile")); + versions "hello" 10`)); } unittest { // test basic serialization @@ -678,14 +700,14 @@ unittest { auto sdl = "name \"test\"\nsourcePaths"; PackageRecipe rec; - parseSDL(rec, sdl, null, "testfile"); + parseSDLTest(rec, sdl); assert("" in rec.buildSettings.sourcePaths); } unittest { auto sdl = "name \"test\"\ncSourcePaths"; PackageRecipe rec; - parseSDL(rec, sdl, null, "testfile"); + parseSDLTest(rec, sdl); assert("" in rec.buildSettings.cSourcePaths); } @@ -695,7 +717,7 @@ dependency "package" repository="git+https://some.url" version="12345678" `; PackageRecipe rec; - parseSDL(rec, sdl, null, "testfile"); + parseSDLTest(rec, sdl); auto dependency = rec.buildSettings.dependencies["package"]; assert(!dependency.repository.empty); assert(dependency.repository.ref_ == "12345678"); diff --git a/source/dub/recipe/selection.d b/source/dub/recipe/selection.d index 1e127e3..11758e8 100644 --- a/source/dub/recipe/selection.d +++ b/source/dub/recipe/selection.d @@ -1,5 +1,10 @@ /** - * Contains type definition for `dub.selections.json` + * Contains type definition for the selections file + * + * The selections file, commonly known by its file name + * `dub.selections.json`, is used by Dub to store resolved + * dependencies. Its purpose is identical to other package + * managers' lock file. */ module dub.recipe.selection; @@ -7,16 +12,99 @@ import dub.internal.vibecompat.inet.path : NativePath; import dub.internal.configy.Attributes; +import dub.internal.dyaml.stdsumtype; import std.exception; -public struct Selected -{ - /// The current version of the file format - public uint fileVersion; +deprecated("Use either `Selections!1` or `SelectionsFile` instead") +public alias Selected = Selections!1; - /// The selected package and their matching versions - public SelectedDependency[string] versions; +/** + * Top level type for `dub.selections.json` + * + * To support multiple version, we expose a `SumType` which + * contains the "real" version being parsed. + */ +public struct SelectionsFile +{ + /// Private alias to avoid repetition + private alias DataType = SumType!(Selections!0, Selections!1); + + /** + * Get the `fileVersion` of this selection file + * + * The `fileVersion` is always present, no matter the version. + * This is a convenience function that matches any version and allows + * one to retrieve it. + * + * Note that the `fileVersion` can be an unsupported version. + */ + public uint fileVersion () const @safe pure nothrow @nogc + { + return this.content.match!((s) => s.fileVersion); + } + + /** + * The content of this selections file + * + * The underlying content can be accessed using + * `dub.internal.yaml.stdsumtype : match`, for example: + * --- + * SelectionsFile file = readSelectionsFile(); + * file.content.match!( + * (Selections!0 s) => logWarn("Unsupported version: %s", s.fileVersion), + * (Selections!1 s) => logWarn("Old version (1), please upgrade!"), + * (Selections!2 s) => logInfo("You are up to date"), + * ); + * --- + */ + public DataType content; + + /** + * Deserialize the selections file according to its version + * + * This will first deserialize the `fileVersion` only, and then + * the expected version if it is supported. Unsupported versions + * will be returned inside a `Selections!0` struct, + * which only contains a `fileVersion`. + */ + public static SelectionsFile fromYAML (scope ConfigParser!SelectionsFile parser) + { + import dub.internal.configy.Read; + + static struct OnlyVersion { uint fileVersion; } + + auto vers = parseConfig!OnlyVersion( + CLIArgs.init, parser.node, StrictMode.Ignore); + + switch (vers.fileVersion) { + case 1: + return SelectionsFile(DataType(parser.parseAs!(Selections!1))); + default: + return SelectionsFile(DataType(Selections!0(vers.fileVersion))); + } + } +} + +/** + * A specific version of the selections file + * + * Currently, only two instantiations of this struct are possible: + * - `Selections!0` is an invalid/unsupported version; + * - `Selections!1` is the most widespread version; + */ +public struct Selections (ushort Version) +{ + /// + public uint fileVersion = Version; + + static if (Version == 0) { /* Invalid version */ } + else static if (Version == 1) { + /// The selected package and their matching versions + public SelectedDependency[string] versions; + } + else + static assert(false, "This version is not supported"); } @@ -97,8 +185,12 @@ } }`; - auto s = parseConfigString!Selected(content, "/dev/null"); - assert(s.fileVersion == 1); + auto file = parseConfigString!SelectionsFile(content, "/dev/null"); + assert(file.fileVersion == 1); + auto s = file.content.match!( + (Selections!1 s) => s, + (s) { assert(0); return Selections!(1).init; }, + ); assert(s.versions.length == 5); assert(s.versions["simple"] == Dependency(Version("1.5.6"))); assert(s.versions["branch"] == Dependency(Version("~master"))); @@ -106,3 +198,13 @@ assert(s.versions["path"] == Dependency(NativePath("../some/where"))); assert(s.versions["repository"] == Dependency(Repository("git+https://github.com/dlang/dub", "123456123456123456"))); } + +// Test reading an unsupported version +unittest +{ + import dub.internal.configy.Read : parseConfigString; + + immutable string content = `{"fileVersion": 9999, "thisis": "notrecognized"}`; + auto s = parseConfigString!SelectionsFile(content, "/dev/null"); + assert(s.fileVersion == 9999); +} diff --git a/source/dub/test/base.d b/source/dub/test/base.d index e5cd8e4..51803a6 100644 --- a/source/dub/test/base.d +++ b/source/dub/test/base.d @@ -51,24 +51,25 @@ import std.array; public import std.algorithm; +import std.exception; +import std.format; +import std.string; import dub.data.settings; public import dub.dependency; public import dub.dub; public import dub.package_; +import dub.internal.vibecompat.core.file : FileInfo; +public import dub.internal.vibecompat.inet.path; import dub.packagemanager; import dub.packagesuppliers.packagesupplier; import dub.project; +import dub.recipe.io : parsePackageRecipe; +import dub.recipe.selection; /// Example of a simple unittest for a project with a single dependency unittest { - // `a` will be loaded as the project while `b` will be loaded - // as a simple package. The recipe files can be in JSON or SDL format, - // here we use both to demonstrate this. - const a = `{ "name": "a", "dependencies": { "b": "~>1.0" } }`; - const b = `name "b"`; - // Enabling this would provide some more verbose output, which makes // debugging a failing unittest much easier. version (none) { @@ -76,24 +77,35 @@ scope(exit) disableLogging(); } - scope dub = new TestDub(); - // Let the `PackageManager` know about the `b` package - dub.addTestPackage(b, Version("1.0.0"), PackageFormat.sdl); - // And about our main package - auto mainPackage = dub.addTestPackage(a, Version("1.0.0")); + // Initialization is best done as a delegate passed to `TestDub` constructor, + // which receives an `FSEntry` representing the root of the filesystem. + // Various low-level functions are exposed (mkdir, writeFile, ...), + // as well as higher-level functions (`writePackageFile`). + scope dub = new TestDub((scope FSEntry root) { + // `a` will be loaded as the project while `b` will be loaded + // as a simple package. The recipe files can be in JSON or SDL format, + // here we use both to demonstrate this. + root.writeFile(TestDub.ProjectPath ~ "dub.json", + `{ "name": "a", "dependencies": { "b": "~>1.0" } }`); + root.writeFile(TestDub.ProjectPath ~ "dub.selections.json", + `{"fileVersion": 1, "versions": {"b": "1.1.0"}}`); + // Note that you currently need to add the `version` to the package + root.writePackageFile("b", "1.0.0", `name "b" +version "1.0.0"`, PackageFormat.sdl); + root.writePackageFile("b", "1.1.0", `name "b" +version "1.1.0"`, PackageFormat.sdl); + root.writePackageFile("b", "1.2.0", `name "b" +version "1.2.0"`, PackageFormat.sdl); + }); + // `Dub.loadPackage` will set this package as the project // While not required, it follows the common Dub use case. - dub.loadPackage(mainPackage); - // This triggers the dependency resolution process that happens - // when one does not have a selection file in the project. - // Dub will resolve dependencies and generate the selection file - // (in memory). If your test has set dependencies / no dependencies, - // this will not be needed. - dub.upgrade(UpgradeOptions.select); + dub.loadPackage(); // Simple tests can be performed using the public API assert(dub.project.hasAllDependencies(), "project has missing dependencies"); assert(dub.project.getDependency("b", true), "Missing 'b' dependency"); + assert(dub.project.getDependency("b", true).version_ == Version("1.1.0")); // While it is important to make your tests fail before you make them pass, // as is common with TDD, it can also be useful to test simple assumptions // as part of your basic tests. Here we want to make sure `getDependency` @@ -102,6 +114,18 @@ // and tests are run serially in a module, so one may rely on previous tests // having passed to avoid repeating some assumptions. assert(dub.project.getDependency("no", true) is null, "Returned unexpected dependency"); + + // This triggers the dependency resolution process that happens + // when one does not have a selection file in the project. + // Dub will resolve dependencies and generate the selection file + // (in memory). If your test has set dependencies / no dependencies, + // this will not be needed. + dub.upgrade(UpgradeOptions.select); + assert(dub.project.getDependency("b", true).version_ == Version("1.1.0")); + + /// Now actually upgrade dependencies in memory + dub.upgrade(UpgradeOptions.select | UpgradeOptions.upgrade); + assert(dub.project.getDependency("b", true).version_ == Version("1.2.0")); } // TODO: Remove and handle logging the same way we handle other IO @@ -123,27 +147,132 @@ * This instance of dub should not read any environment variables, * nor should it do any file IO, to make it usable and reliable in unittests. * Currently it reads environment variables but does not read the configuration. + * + * Note that since the design of Dub was centered on the file system for so long, + * `NativePath` is still a core part of how one interacts with this class. + * In order to be as close to the production code as possible, this class + * use the following conventions: + * - The project is located under `/dub/project/`; + * - The user and system packages are under `/dub/user/packages/` and + * `/dub/system/packages/`, respectively; + * Those paths don't need to exists, but they are what one might see + * when writing and debugging unittests. */ public class TestDub : Dub { - /// Forward to base constructor - public this (string root = ".", PackageSupplier[] extras = null, - SkipPackageSuppliers skip = SkipPackageSuppliers.none) + /// The virtual filesystem that this instance acts on + public FSEntry fs; + + /// Convenience constants for use in unittests + version (Windows) + public static immutable Root = NativePath("T:\\dub\\"); + else + public static immutable Root = NativePath("/dub/"); + + /// Ditto + public static immutable ProjectPath = Root ~ "project"; + + /// Ditto + public static immutable SpecialDirs Paths = { + temp: Root ~ "temp/", + systemSettings: Root ~ "system/", + userSettings: Root ~ "user/", + userPackages: Root ~ "user/", + cache: Root ~ "user/" ~ "cache/", + }; + + /*************************************************************************** + + Instantiate a new `TestDub` instance with the provided filesystem state + + This exposes the raw virtual filesystem to the user, allowing any kind + of customization to happen: Empty directory, non-writeable ones, etc... + + Params: + dg = Delegate to be called with the filesystem, before `TestDub` + instantiation is performed; + root = The root path for this instance (forwarded to Dub) + extras = Extras `PackageSupplier`s (forwarded to Dub) + skip = What `PackageSupplier`s to skip (forwarded to Dub) + + ***************************************************************************/ + + public this (scope void delegate(scope FSEntry root) dg = null, + string root = ProjectPath.toNativeString(), + PackageSupplier[] extras = null, + SkipPackageSuppliers skip = SkipPackageSuppliers.none) { + /// Create the fs & its base structure + auto fs_ = new FSEntry(); + fs_.mkdir(Paths.temp); + fs_.mkdir(Paths.systemSettings); + fs_.mkdir(Paths.userSettings); + fs_.mkdir(Paths.userPackages); + fs_.mkdir(Paths.cache); + fs_.mkdir(ProjectPath); + if (dg !is null) dg(fs_); + this(fs_, root, extras, skip); + } + + /// Workaround https://issues.dlang.org/show_bug.cgi?id=24388 when called + /// when called with (null, ...). + public this (typeof(null) _, + string root = ProjectPath.toNativeString(), + PackageSupplier[] extras = null, + SkipPackageSuppliers skip = SkipPackageSuppliers.none) + { + alias TType = void delegate(scope FSEntry); + this(TType.init, root, extras, skip); + } + + /// Internal constructor + private this(FSEntry fs_, string root, PackageSupplier[] extras, + SkipPackageSuppliers skip) + { + this.fs = fs_; super(root, extras, skip); } + /*************************************************************************** + + Get a new `Dub` instance with the same filesystem + + This creates a new `TestDub` instance with the existing filesystem, + allowing one to write tests that would normally require multiple Dub + instantiation (e.g. test that `fetch` is idempotent). + Like the main `TestDub` constructor, it allows to do modifications to + the filesystem before the new instantiation is made. + + Params: + dg = Delegate to be called with the filesystem, before `TestDub` + instantiation is performed; + + Returns: + A new `TestDub` instance referencing the same filesystem as `this`. + + ***************************************************************************/ + + public TestDub newTest (scope void delegate(scope FSEntry root) dg = null, + string root = ProjectPath.toNativeString(), + PackageSupplier[] extras = null, + SkipPackageSuppliers skip = SkipPackageSuppliers.none) + { + if (dg !is null) dg(this.fs); + return new TestDub(this.fs, root, extras, skip); + } + /// Avoid loading user configuration protected override Settings loadConfig(ref SpecialDirs dirs) const { - // No-op + dirs = Paths; return Settings.init; } /// - protected override PackageManager makePackageManager() const + protected override PackageManager makePackageManager() { - return new TestPackageManager(); + assert(this.fs !is null); + return new TestPackageManager(this.fs); } /// See `MockPackageSupplier` documentation for this class' implementation @@ -152,16 +281,11 @@ return new MockPackageSupplier(url); } - /// Loads the package from the specified path as the main project package. - public override void loadPackage(NativePath path) - { - assert(0, "Not implemented"); - } - /// Loads a specific package as the main project package (can be a sub package) public override void loadPackage(Package pack) { - m_project = new Project(m_packageManager, pack, new TestSelectedVersions()); + auto selections = this.packageManager.loadSelections(pack); + m_project = new Project(m_packageManager, pack, selections); } /// Reintroduce parent overloads @@ -177,71 +301,6 @@ { return cast(inout(TestPackageManager)) this.m_packageManager; } - - /** - * Creates a package with the provided recipe - * - * This is a convenience function provided to create a package based on - * a given recipe. This is to allow test-cases to be written based off - * issues more easily. - * - * In order for the `Package` to be visible to `Dub`, use `addTestPackage`, - * as `makeTestPackage` simply creates the `Package` without adding it. - * - * Params: - * str = The string representation of the `PackageRecipe` - * recipe = The `PackageRecipe` to use - * vers = The version the package is at, e.g. `Version("1.0.0")` - * fmt = The format `str` is in, either JSON or SDL - * - * Returns: - * The created `Package` instance - */ - public Package makeTestPackage(string str, Version vers, PackageFormat fmt = PackageFormat.json) - { - import dub.recipe.io; - final switch (fmt) { - case PackageFormat.json: - auto recipe = parsePackageRecipe(str, "dub.json"); - recipe.version_ = vers.toString(); - return new Package(recipe); - case PackageFormat.sdl: - auto recipe = parsePackageRecipe(str, "dub.sdl"); - recipe.version_ = vers.toString(); - return new Package(recipe); - } - } - - /// Ditto - public Package addTestPackage(string str, Version vers, PackageFormat fmt = PackageFormat.json) - { - return this.packageManager.add(this.makeTestPackage(str, vers, fmt)); - } -} - -/** - * - */ -public class TestSelectedVersions : SelectedVersions { - import dub.recipe.selection; - - /// Forward to parent's constructor - public this(uint version_ = FileVersion) @safe pure nothrow @nogc - { - super(version_); - } - - /// Ditto - public this(Selected data) @safe pure nothrow @nogc - { - super(data); - } - - /// Do not do IO - public override void save(NativePath path) - { - // No-op - } } /** @@ -253,121 +312,162 @@ */ package class TestPackageManager : PackageManager { + /// `loadSCMPackage` will strip some part of the remote / repository, + /// which we need to mimic to provide a usable API. + private struct GitReference { + /// + this (in Repository repo) { + this.remote = repo.remote.chompPrefix("git+"); + this.ref_ = repo.ref_.chompPrefix("~"); + } + + /// + this (in string remote, in string gitref) { + this.remote = remote; + this.ref_ = gitref; + } + + string remote; + string ref_; + } + + /// List of all SCM packages that can be fetched by this instance - protected Package[Repository] scm; + protected string[GitReference] scm; + /// The virtual filesystem that this PackageManager acts on + protected FSEntry fs; - this() + this(FSEntry filesystem) { - NativePath pkg = NativePath("/tmp/dub-testsuite-nonexistant/packages/"); - NativePath user = NativePath("/tmp/dub-testsuite-nonexistant/user/"); - NativePath system = NativePath("/tmp/dub-testsuite-nonexistant/system/"); - super(pkg, user, system, false); + NativePath local = TestDub.ProjectPath; + NativePath user = TestDub.Paths.userSettings; + NativePath system = TestDub.Paths.systemSettings; + this.fs = filesystem; + super(local, user, system, false); } - /// Disabled as semantic are not implementable unless a virtual FS is created - public override @property void customCachePaths(NativePath[] custom_cache_paths) - { - assert(0, "Function not implemented"); - } + /// Port of `Project.loadSelections` + SelectedVersions loadSelections(in Package pack) + { + import dub.version_; + import dub.internal.configy.Read; + import dub.internal.dyaml.stdsumtype; + + auto selverfile = (pack.path ~ SelectedVersions.defaultFile); + // No file exists + if (!this.fs.existsFile(selverfile)) + return new SelectedVersions(); + + SelectionsFile selected; + try + { + const content = this.fs.readText(selverfile); + selected = parseConfigString!SelectionsFile( + content, selverfile.toNativeString()); + } + catch (Exception exc) { + logError("Error loading %s: %s", selverfile, exc.message()); + return new SelectedVersions(); + } + + return selected.content.match!( + (Selections!0 s) { + logWarnTag("Unsupported version", + "File %s has fileVersion %s, which is not yet supported by DUB %s.", + selverfile, s.fileVersion, dubVersion); + logWarn("Ignoring selections file. Use a newer DUB version " ~ + "and set the appropriate toolchainRequirements in your recipe file"); + return new SelectedVersions(); + }, + (Selections!1 s) => new SelectedVersions(s), + ); + } + + // Re-introduce hidden/deprecated overloads + public alias store = PackageManager.store; /// Ditto - public override Package store(NativePath src, PlacementLocation dest, string name, Version vers) + public override Package store(NativePath src, PlacementLocation dest, in PackageName name, in Version vers) { assert(0, "Function not implemented"); } - /** - * This function usually scans the filesystem for packages. - * - * We don't want to do IO access and rely on users adding the packages - * before the test starts instead. - * - * Note: Deprecated `refresh(bool)` does IO, but it's deprecated - */ - public override void refresh() - { - // Do nothing - } - - /** - * Looks up a specific package - * - * Unlike its parent class, no lazy loading is performed. - * Additionally, as they are already deprecated, overrides are - * disabled and not available. - */ - public override Package getPackage(string name, Version vers, bool enable_overrides = false) - { - //assert(!enable_overrides, "Overrides are not implemented for TestPackageManager"); - - // Implementation inspired from `PackageManager.lookup`, - // except we replaced `load` with `lookup`. - if (auto pkg = this.m_internal.lookup(name, vers, this)) - return pkg; - - foreach (ref location; this.m_repositories) - if (auto p = location.lookup(name, vers, this)) - return p; - - return null; - } - /** - * Re-Implementation of `loadSCMPackage`. + * Re-Implementation of `gitClone`. * - * The base implementation will do a `git` clone, which we would like to avoid. - * Instead, we allow unittests to explicitly define what packages should be - * reachable in a given test. + * The base implementation will do a `git` clone, to the file-system. + * We need to mock both the `git` part and the write to the file system. */ - public override Package loadSCMPackage(string name, Repository repo) + protected override bool gitClone(string remote, string gitref, in NativePath dest) { - import std.string : chompPrefix; - - // We're trying to match `loadGitPackage` as much as possible - if (!repo.ref_.startsWith("~") && !repo.ref_.isGitHash) - return null; - - string gitReference = repo.ref_.chompPrefix("~"); - NativePath destination = this.getPackagePath(PlacementLocation.user, name, repo.ref_); - destination ~= name; - destination.endsWithSlash = true; - - foreach (p; getPackageIterator(name)) - if (p.path == destination) - return p; - - return this.loadSCMRepository(name, repo); - } - - /// The private part of `loadSCMPackage` - protected Package loadSCMRepository(string name, Repository repo) - { - if (auto prepo = repo in this.scm) { - this.add(*prepo); - return *prepo; + if (auto pstr = GitReference(remote, gitref) in this.scm) { + this.fs.mkdir(dest); + this.fs.writeFile(dest ~ "dub.json", *pstr); + return true; } - return null; - } - - /** - * Adds a `Package` to this `PackageManager` - * - * This is currently only available in unittests as it is a convenience - * function used by `TestDub`, but could be generalized once IO has been - * abstracted away from this class. - */ - public Package add(Package pkg) - { - // See `PackageManager.addPackages` for inspiration. - assert(!pkg.subPackages.length, "Subpackages are not yet supported"); - this.m_internal.fromPath ~= pkg; - return pkg; + return false; } /// Add a reachable SCM package to this `PackageManager` - public void addTestSCMPackage(Repository repo, Package pkg) + public void addTestSCMPackage(in Repository repo, string dub_json) { - this.scm[repo] = pkg; + this.scm[GitReference(repo)] = dub_json; + } + + /// + protected override bool existsDirectory(NativePath path) + { + return this.fs.existsDirectory(path); + } + + /// + protected override void ensureDirectory(NativePath path) + { + this.fs.mkdir(path); + } + + /// + protected override bool existsFile(NativePath path) + { + return this.fs.existsFile(path); + } + + /// + protected override void writeFile(NativePath path, const(ubyte)[] data) + { + return this.fs.writeFile(path, data); + } + + /// + protected override void writeFile(NativePath path, const(char)[] data) + { + return this.fs.writeFile(path, data); + } + + /// + protected override string readText(NativePath path) + { + return this.fs.readText(path); + } + + /// + protected override IterateDirDg iterateDirectory(NativePath path) + { + enforce(this.fs.existsDirectory(path), + path.toNativeString() ~ " does not exists or is not a directory"); + auto dir = this.fs.lookup(path); + int iterator(scope int delegate(ref FileInfo) del) { + foreach (c; dir.children) { + FileInfo fi; + fi.name = c.name; + fi.size = (c.type == FSEntry.Type.Directory) ? 0 : c.content.length; + fi.isDirectory = (c.type == FSEntry.Type.Directory); + if (auto res = del(fi)) + return res; + } + return 0; + } + return &iterator; } } @@ -380,7 +480,7 @@ public class MockPackageSupplier : PackageSupplier { /// Mapping of package name to packages, ordered by `Version` - protected Package[][string] pkgs; + protected Package[Version][PackageName] pkgs; /// URL this was instantiated with protected string url; @@ -398,32 +498,35 @@ } /// - public override Version[] getVersions(string package_id) + public override Version[] getVersions(in PackageName name) { - if (auto ppkgs = package_id in this.pkgs) - return (*ppkgs).map!(pkg => pkg.version_).array; + if (auto ppkgs = name.main in this.pkgs) + return (*ppkgs).keys; return null; } /// - public override void fetchPackage( - NativePath path, string package_id, in VersionRange dep, bool pre_release) + public override ubyte[] fetchPackage(in PackageName name, + in VersionRange dep, bool pre_release) { - assert(0, this.url ~ " - fetchPackage not implemented for: " ~ package_id); + assert(0, "%s - fetchPackage not implemented for: %s" + .format(this.url, name.main)); } /// - public override Json fetchPackageRecipe( - string package_id, in VersionRange dep, bool pre_release) + public override Json fetchPackageRecipe(in PackageName name, + in VersionRange dep, bool pre_release) { import dub.recipe.json; - if (auto ppkgs = package_id in this.pkgs) - foreach_reverse (pkg; *ppkgs) - if ((!pkg.version_.isPreRelease || pre_release) && - dep.matches(pkg.version_)) - return toJson(pkg.recipe); - return Json.init; + Package match; + if (auto ppkgs = name.main in this.pkgs) + foreach (vers, pkg; *ppkgs) + if ((!vers.isPreRelease || pre_release) && + dep.matches(vers) && + (match is null || match.version_ < vers)) + match = pkg; + return match is null ? Json.init : toJson(match.recipe); } /// @@ -432,3 +535,345 @@ assert(0, this.url ~ " - searchPackages not implemented for: " ~ query); } } + +/// An abstract filesystem representation +public class FSEntry +{ + /// Type of file system entry + public enum Type { + Directory, + File, + } + + /// Ditto + protected Type type; + /// The name of this node + protected string name; + /// The parent of this entry (can be null for the root) + protected FSEntry parent; + union { + /// Children for this FSEntry (with type == Directory) + protected FSEntry[] children; + /// Content for this FDEntry (with type == File) + protected ubyte[] content; + } + + /// Creates a new FSEntry + private this (FSEntry p, Type t, string n) + { + this.type = t; + this.parent = p; + this.name = n; + } + + /// Create the root of the filesystem, only usable from this module + private this () + { + this.type = Type.Directory; + } + + /// Get a direct children node, returns `null` if it can't be found + protected inout(FSEntry) lookup(string name) inout return scope + { + assert(!name.canFind('/')); + foreach (c; this.children) + if (c.name == name) + return c; + return null; + } + + /// Get an arbitrarily nested children node + protected inout(FSEntry) lookup(NativePath path) inout return scope + { + auto relp = this.relativePath(path); + if (relp.empty) + return this; + auto segments = relp.bySegment; + if (auto c = this.lookup(segments.front.name)) { + segments.popFront(); + return !segments.empty ? c.lookup(NativePath(segments)) : c; + } + return null; + } + + /** Get the parent `FSEntry` of a `NativePath` + * + * If the parent doesn't exist, an `Exception` will be thrown + * unless `silent` is provided. If the parent path is a file, + * an `Exception` will be thrown regardless of `silent`. + * + * Params: + * path = The path to look up the parent for + * silent = Whether to error on non-existing parent, + * default to `false`. + */ + protected inout(FSEntry) getParent(NativePath path, bool silent = false) + inout return scope + { + // Relative path in the current directory + if (!path.hasParentPath()) + return this; + + // If we're not in the right `FSEntry`, recurse + const parentPath = path.parentPath(); + auto p = this.lookup(parentPath); + enforce(silent || p !is null, + "No such directory: " ~ parentPath.toNativeString()); + enforce(p is null || p.type == Type.Directory, + "Parent path is not a directory: " ~ parentPath.toNativeString()); + return p; + } + + /// Returns: A path relative to `this.path` + protected NativePath relativePath(NativePath path) const scope + { + assert(!path.absolute() || path.startsWith(this.path), + "Calling relativePath with a differently rooted path"); + return path.absolute() ? path.relativeTo(this.path) : path; + } + + /*+************************************************************************* + + Utility function + + Below this banners are functions that are provided for the convenience + of writing tests for `Dub`. + + ***************************************************************************/ + + /// Prints a visual representation of the filesystem to stdout for debugging + public void print(bool content = false) const scope + { + import std.range : repeat; + static import std.stdio; + + size_t indent; + for (auto p = &this.parent; (*p) !is null; p = &p.parent) + indent++; + // Don't print anything (even a newline) for root + if (this.parent is null) + std.stdio.write('/'); + else + std.stdio.write('|', '-'.repeat(indent), ' ', this.name, ' '); + + final switch (this.type) { + case Type.Directory: + std.stdio.writeln('(', this.children.length, " entries):"); + foreach (c; this.children) + c.print(content); + break; + case Type.File: + if (!content) + std.stdio.writeln('(', this.content.length, " bytes)"); + else if (this.name.endsWith(".json") || this.name.endsWith(".sdl")) + std.stdio.writeln('(', this.content.length, " bytes): ", + cast(string) this.content); + else + std.stdio.writeln('(', this.content.length, " bytes): ", + this.content); + break; + } + } + + /// Returns: The final destination a specific package needs to be stored in + public static NativePath getPackagePath(in string name_, string vers, + PlacementLocation location = PlacementLocation.user) + { + PackageName name = PackageName(name_); + // Keep in sync with `dub.packagemanager: PackageManager.getPackagePath` + // and `Location.getPackagePath` + NativePath result (in NativePath base) + { + NativePath res = base ~ name.main.toString() ~ vers ~ + name.main.toString(); + res.endsWithSlash = true; + return res; + } + + final switch (location) { + case PlacementLocation.user: + return result(TestDub.Paths.userSettings ~ "packages/"); + case PlacementLocation.system: + return result(TestDub.Paths.systemSettings ~ "packages/"); + case PlacementLocation.local: + return result(TestDub.ProjectPath ~ "/.dub/packages/"); + } + } + + /*+************************************************************************* + + Public filesystem functions + + Below this banners are functions which mimic the behavior of a file + system. + + ***************************************************************************/ + + /// Returns: The `path` of this FSEntry + public NativePath path() const scope + { + if (this.parent is null) + return NativePath("/"); + auto thisPath = this.parent.path ~ this.name; + thisPath.endsWithSlash = (this.type == Type.Directory); + return thisPath; + } + + /// Implements `mkdir -p`, returns the created directory + public FSEntry mkdir (NativePath path) scope + { + auto relp = this.relativePath(path); + // Check if the child already exists + auto segments = relp.bySegment; + auto child = this.lookup(segments.front.name); + if (child is null) { + child = new FSEntry(this, Type.Directory, segments.front.name); + this.children ~= child; + } + // Recurse if needed + segments.popFront(); + return !segments.empty ? child.mkdir(NativePath(segments)) : child; + } + + /// Checks the existence of a file + public bool existsFile (NativePath path) const scope + { + auto entry = this.lookup(path); + return entry !is null && entry.type == Type.File; + } + + /// Checks the existence of a directory + public bool existsDirectory (NativePath path) const scope + { + auto entry = this.lookup(path); + return entry !is null && entry.type == Type.Directory; + } + + /// Reads a file, returns the content as `ubyte[]` + public ubyte[] readFile (NativePath path) const scope + { + auto entry = this.lookup(path); + enforce(entry.type == Type.File, "Trying to read a directory"); + return entry.content.dup; + } + + /// Reads a file, returns the content as text + public string readText (NativePath path) const scope + { + import std.utf : validate; + + auto entry = this.lookup(path); + enforce(entry.type == Type.File, "Trying to read a directory"); + // Ignore BOM: If it's needed for a test, add support for it. + validate(cast(const(char[])) entry.content); + return cast(string) entry.content.idup(); + } + + /// Write to this file + public void writeFile (NativePath path, const(char)[] data) scope + { + this.writeFile(path, data.representation); + } + + /// Ditto + public void writeFile (NativePath path, const(ubyte)[] data) scope + { + enforce(!path.endsWithSlash(), + "Cannot write to directory: " ~ path.toNativeString()); + if (auto file = this.lookup(path)) { + // If the file already exists, override it + enforce(file.type == Type.File, + "Trying to write to directory: " ~ path.toNativeString()); + file.content = data.dup; + } else { + auto p = this.getParent(path); + auto file = new FSEntry(p, Type.File, path.head.name()); + file.content = data.dup; + p.children ~= file; + } + } + + /** Remove a file + * + * Always error if the target is a directory. + * Does not error if the target does not exists + * and `force` is set to `true`. + * + * Params: + * path = Path to the file to remove + * force = Whether to ignore non-existing file, + * default to `false`. + */ + public void removeFile (NativePath path, bool force = false) + { + import std.algorithm.searching : countUntil; + + assert(!path.empty, "Empty path provided to `removeFile`"); + enforce(!path.endsWithSlash(), + "Cannot remove file with directory path: " ~ path.toNativeString()); + auto p = this.getParent(path, force); + const idx = p.children.countUntil!(e => e.name == path.head.name()); + if (idx < 0) { + enforce(force, + "removeFile: No such file: " ~ path.toNativeString()); + } else { + enforce(p.children[idx].type == Type.File, + "removeFile called on a directory: " ~ path.toNativeString()); + p.children = p.children[0 .. idx] ~ p.children[idx + 1 .. $]; + } + } + + /** Remove a directory + * + * Remove an existing empty directory. + * If `force` is set to `true`, no error will be thrown + * if the directory is empty or non-existing. + * + * Params: + * path = Path to the directory to remove + * force = Whether to ignore non-existing / non-empty directories, + * default to `false`. + */ + public void removeDir (NativePath path, bool force = false) + { + import std.algorithm.searching : countUntil; + + assert(!path.empty, "Empty path provided to `removeFile`"); + auto p = this.getParent(path, force); + const idx = p.children.countUntil!(e => e.name == path.head.name()); + if (idx < 0) { + enforce(force, + "removeDir: No such directory: " ~ path.toNativeString()); + } else { + enforce(p.children[idx].type == Type.Directory, + "removeDir called on a file: " ~ path.toNativeString()); + enforce(force || p.children[idx].children.length == 0, + "removeDir called on non-empty directory: " ~ path.toNativeString()); + p.children = p.children[0 .. idx] ~ p.children[idx + 1 .. $]; + } + } +} + +/** + * Convenience function to write a package file + * + * Allows to write a package file (and only a package file) for a certain + * package name and version. + * + * Params: + * root = The root FSEntry + * name = The package name (typed as string for convenience) + * vers = The package version + * recipe = The text of the package recipe + * fmt = The format used for `recipe` (default to JSON) + * location = Where to place the package (default to user location) + */ +public void writePackageFile (FSEntry root, in string name, in string vers, + in string recipe, in PackageFormat fmt = PackageFormat.json, + in PlacementLocation location = PlacementLocation.user) +{ + const path = FSEntry.getPackagePath(name, vers, location); + root.mkdir(path).writeFile( + NativePath(fmt == PackageFormat.json ? "dub.json" : "dub.sdl"), + recipe); +} diff --git a/source/dub/test/dependencies.d b/source/dub/test/dependencies.d index d9f78ed..180f31d 100644 --- a/source/dub/test/dependencies.d +++ b/source/dub/test/dependencies.d @@ -31,17 +31,18 @@ // Ensure that simple dependencies get resolved correctly unittest { - const a = `name "a" + scope dub = new TestDub((scope FSEntry root) { + root.writeFile(TestDub.ProjectPath ~ "dub.sdl", `name "a" +version "1.0.0" dependency "b" version="*" dependency "c" version="*" -`; - const b = `name "b"`; - const c = `name "c"`; - - scope dub = new TestDub(); - dub.addTestPackage(c, Version("1.0.0"), PackageFormat.sdl); - dub.addTestPackage(b, Version("1.0.0"), PackageFormat.sdl); - dub.loadPackage(dub.addTestPackage(a, Version("1.0.0"), PackageFormat.sdl)); +`); + root.writePackageFile("b", "1.0.0", `name "b" +version "1.0.0"`, PackageFormat.sdl); + root.writePackageFile("c", "1.0.0", `name "c" +version "1.0.0"`, PackageFormat.sdl); + }); + dub.loadPackage(); dub.upgrade(UpgradeOptions.select); @@ -54,18 +55,16 @@ // Test that indirect dependencies get resolved correctly unittest { - const a = `name "a" -dependency "b" version="*" -`; - const b = `name "b" -dependency "c" version="*" -`; - const c = `name "c"`; - - scope dub = new TestDub(); - dub.addTestPackage(c, Version("1.0.0"), PackageFormat.sdl); - dub.addTestPackage(b, Version("1.0.0"), PackageFormat.sdl); - dub.loadPackage(dub.addTestPackage(a, Version("1.0.0"), PackageFormat.sdl)); + scope dub = new TestDub((scope FSEntry root) { + root.writeFile(TestDub.ProjectPath ~ "dub.sdl", `name "a" +dependency "b" version="*"`); + root.writePackageFile("b", "1.0.0", `name "b" +version "1.0.0" +dependency "c" version="*"`, PackageFormat.sdl); + root.writePackageFile("c", "1.0.0", `name "c" +version "1.0.0"`, PackageFormat.sdl); + }); + dub.loadPackage(); dub.upgrade(UpgradeOptions.select); @@ -78,23 +77,21 @@ // Simple diamond dependency unittest { - const a = `name "a" + scope dub = new TestDub((scope FSEntry root) { + root.writeFile(TestDub.ProjectPath ~ "dub.sdl", `name "a" dependency "b" version="*" -dependency "c" version="*" -`; - const b = `name "b" -dependency "d" version="*" -`; - const c = `name "c" -dependency "d" version="*" -`; - const d = `name "d"`; +dependency "c" version="*"`); + root.writePackageFile("b", "1.0.0", `name "b" +version "1.0.0" +dependency "d" version="*"`, PackageFormat.sdl); + root.writePackageFile("c", "1.0.0", `name "c" +version "1.0.0" +dependency "d" version="*"`, PackageFormat.sdl); + root.writePackageFile("d", "1.0.0", `name "d" +version "1.0.0"`, PackageFormat.sdl); - scope dub = new TestDub(); - dub.addTestPackage(d, Version("1.0.0"), PackageFormat.sdl); - dub.addTestPackage(c, Version("1.0.0"), PackageFormat.sdl); - dub.addTestPackage(b, Version("1.0.0"), PackageFormat.sdl); - dub.loadPackage(dub.addTestPackage(a, Version("1.0.0"), PackageFormat.sdl)); + }); + dub.loadPackage(); dub.upgrade(UpgradeOptions.select); @@ -108,24 +105,25 @@ // Missing dependencies trigger an error unittest { - const a = `name "a" -dependency "b" version="*" -`; - - scope dub = new TestDub(); - dub.loadPackage(dub.addTestPackage(a, Version("1.0.0"), PackageFormat.sdl)); + scope dub = new TestDub((scope FSEntry root) { + root.writeFile(TestDub.ProjectPath ~ "dub.sdl", `name "a" +dependency "b" version="*"`); + }); + dub.loadPackage(); try dub.upgrade(UpgradeOptions.select); catch (Exception exc) - assert(exc.message() == `Failed to find any versions for package b, referenced by a 1.0.0`); + assert(exc.message() == `Failed to find any versions for package b, referenced by a ~master`); assert(!dub.project.hasAllDependencies(), "project should have missing dependencies"); assert(dub.project.getDependency("b", true) is null, "Found 'b' dependency"); assert(dub.project.getDependency("no", true) is null, "Returned unexpected dependency"); // Add the missing dependency to our PackageManager - dub.addTestPackage(`name "b"`, Version("1.0.0"), PackageFormat.sdl); + dub.fs.writePackageFile(`b`, "1.0.0", `name "b" +version "1.0.0"`, PackageFormat.sdl); + dub.packageManager.refresh(); dub.upgrade(UpgradeOptions.select); assert(dub.project.hasAllDependencies(), "project have missing dependencies"); assert(dub.project.getDependency("b", true), "Missing 'b' dependency"); diff --git a/source/dub/test/other.d b/source/dub/test/other.d index 9407fb2..241fb0f 100644 --- a/source/dub/test/other.d +++ b/source/dub/test/other.d @@ -18,33 +18,64 @@ const ValidURL = `git+https://example.com/dlang/dub`; // Taken from a commit in the dub repository const ValidHash = "54339dff7ce9ec24eda550f8055354f712f15800"; - const Template = `{"name": "%s", "dependencies": { + const Template = `{"name": "%s", "version": "1.0.0", "dependencies": { "dep1": { "repository": "%s", "version": "%s" }}}`; - scope dub = new TestDub(); + scope dub = new TestDub((scope FSEntry fs) { + // Invalid URL, valid hash + fs.writePackageFile("a", "1.0.0", Template.format("a", "git+https://nope.nope", ValidHash)); + // Valid URL, invalid hash + fs.writePackageFile("b", "1.0.0", Template.format("b", ValidURL, "invalid")); + // Valid URL, valid hash + fs.writePackageFile("c", "1.0.0", Template.format("c", ValidURL, ValidHash)); + }); dub.packageManager.addTestSCMPackage( - Repository(ValidURL, ValidHash), - // Note: SCM package are always marked as using `~master` - dub.makeTestPackage(`{ "name": "dep1" }`, Version(`~master`)), - ); + Repository(ValidURL, ValidHash), `{ "name": "dep1" }`); - // Invalid URL, valid hash - const a = Template.format("a", "git+https://nope.nope", ValidHash); try - dub.loadPackage(dub.addTestPackage(a, Version("1.0.0"))); + dub.loadPackage(dub.packageManager.getPackage(PackageName("a"), Version("1.0.0"))); + catch (Exception exc) + assert(exc.message.canFind("Unable to fetch")); + + try + dub.loadPackage(dub.packageManager.getPackage(PackageName("b"), Version("1.0.0"))); catch (Exception exc) assert(exc.message.canFind("Unable to fetch")); - // Valid URL, invalid hash - const b = Template.format("b", ValidURL, "invalid"); - try - dub.loadPackage(dub.addTestPackage(b, Version("1.0.0"))); - catch (Exception exc) - assert(exc.message.canFind("Unable to fetch")); - - // Valid URL, valid hash - const c = Template.format("c", ValidURL, ValidHash); - dub.loadPackage(dub.addTestPackage(c, Version("1.0.0"))); + dub.loadPackage(dub.packageManager.getPackage(PackageName("c"), Version("1.0.0"))); assert(dub.project.hasAllDependencies()); assert(dub.project.getDependency("dep1", true), "Missing 'dep1' dependency"); } + +// Test for https://github.com/dlang/dub/pull/2481 +// Make sure packages found with `add-path` take priority. +unittest +{ + const AddPathDir = TestDub.Paths.temp ~ "addpath/"; + const BDir = AddPathDir ~ "b/"; + scope dub = new TestDub((scope FSEntry root) { + root.writeFile(TestDub.ProjectPath ~ "dub.json", + `{ "name": "a", "dependencies": { "b": "~>1.0" } }`); + + root.writePackageFile("b", "1.0.0", `name "b" +version "1.0.0"`, PackageFormat.sdl); + root.mkdir(BDir); + root.writeFile(BDir ~ "dub.json", `{"name": "b", "version": "1.0.0" }`); + }); + + dub.loadPackage(); + assert(!dub.project.hasAllDependencies()); + dub.upgrade(UpgradeOptions.select); + // Test that without add-path, we get a package in the userPackage + const oldDir = dub.project.getDependency("b", true).path(); + assert(oldDir == TestDub.Paths.userPackages ~ "packages/b/1.0.0/b/", + oldDir.toNativeString()); + // Now run `add-path` + dub.addSearchPath(AddPathDir.toNativeString(), dub.defaultPlacementLocation); + // We need a new instance to test + scope newDub = dub.newTest(); + newDub.loadPackage(); + assert(newDub.project.hasAllDependencies()); + const actualDir = newDub.project.getDependency("b", true).path(); + assert(actualDir == BDir, actualDir.toNativeString()); +} diff --git a/source/dub/test/subpackages.d b/source/dub/test/subpackages.d new file mode 100644 index 0000000..a84ae83 --- /dev/null +++ b/source/dub/test/subpackages.d @@ -0,0 +1,40 @@ +/******************************************************************************* + + Test for subpackages + + Subpackages are packages that are part of a 'main' packages. Their version + is that of their main (parent) package. They are referenced using a column, + e.g. `mainpkg:subpkg`. Nested subpackages are disallowed. + +*******************************************************************************/ + +module dub.test.subpackages; + +version(unittest): + +import dub.test.base; + +/// Test of the PackageManager APIs +unittest +{ + scope dub = new TestDub((scope FSEntry root) { + root.writeFile(TestDub.ProjectPath ~ "dub.json", + `{ "name": "a", "dependencies": { "b:a": "~>1.0", "b:b": "~>1.0" } }`); + root.writePackageFile("b", "1.0.0", + `{ "name": "b", "version": "1.0.0", "subPackages": [ { "name": "a" }, { "name": "b" } ] }`); + }); + dub.loadPackage(); + + dub.upgrade(UpgradeOptions.select); + + assert(dub.project.hasAllDependencies(), "project has missing dependencies"); + assert(dub.project.getDependency("b:b", true), "Missing 'b:b' dependency"); + assert(dub.project.getDependency("b:a", true), "Missing 'b:a' dependency"); + assert(dub.project.getDependency("no", true) is null, "Returned unexpected dependency"); + + assert(dub.packageManager().getPackage(PackageName("b:a"), Version("1.0.0")).name == "b:a"); + assert(dub.packageManager().getPackage(PackageName("b:b"), Version("1.0.0")).name == "b:b"); + assert(dub.packageManager().getPackage(PackageName("b"), Version("1.0.0")).name == "b"); + + assert(!dub.packageManager().getPackage(PackageName("b:b"), Version("1.1.0"))); +} diff --git a/test/environment-variables.script.d b/test/environment-variables.script.d index c96c2c4..347f696 100644 --- a/test/environment-variables.script.d +++ b/test/environment-variables.script.d @@ -15,7 +15,7 @@ // preRunCommands uses system.environments < settings.environments < deppkg.environments < root.environments < deppkg.runEnvironments < root.runEnvironments < deppkg.preRunEnvironments < root.preRunEnvironments // User application uses system.environments < settings.environments < deppkg.environments < root.environments < deppkg.runEnvironments < root.runEnvironments // postRunCommands uses system.environments < settings.environments < deppkg.environments < root.environments < deppkg.runEnvironments < root.runEnvironments < deppkg.postRunEnvironments < root.postRunEnvironments - + // Test cases covers: // preGenerateCommands [in root] // priority check: system.environments < settings.environments @@ -50,30 +50,30 @@ ], Config.none, size_t.max, currDir.buildPath("environment-variables")); scope (failure) writeln("environment-variables test failed... Testing stdout is:\n-----\n", res.output); - + // preGenerateCommands [in root] assert(res.output.canFind("root.preGenerate: setting.PRIORITYCHECK_SYS_SET"), "preGenerate environment variables priority check is failed."); assert(res.output.canFind("root.preGenerate: deppkg.PRIORITYCHECK_SET_DEP"), "preGenerate environment variables priority check is failed."); assert(res.output.canFind("root.preGenerate: deppkg.PRIORITYCHECK_DEP_ROOT"), "preGenerate environment variables priority check is failed."); assert(res.output.canFind("root.preGenerate: deppkg.PRIORITYCHECK_ROOT_DEPSPEC"), "preGenerate environment variables priority check is failed."); assert(res.output.canFind("root.preGenerate: root.PRIORITYCHECK_DEPSPEC_ROOTSPEC"), "preGenerate environment variables priority check is failed."); - + // postGenerateCommands [in root] assert(res.output.canFind("root.postGenerate: deppkg.VAR4", "postGenerate environment variables expantion check is failed.")); - + // preBuildCommands [in deppkg] assert(res.output.canFind("deppkg.preBuild: deppkg.PRIORITYCHECK_ROOT_DEPBLDSPEC"), "preBuild environment variables priority check is failed."); assert(res.output.canFind("deppkg.preBuild: root.PRIORITYCHECK_DEPBLDSPEC_ROOTBLDSPEC"), "preBuild environment variables priority check is failed."); assert(res.output.canFind("deppkg.preBuild: deppkg.PRIORITYCHECK_ROOTBLDSPEC_DEPSPEC"), "preBuild environment variables priority check is failed."); assert(res.output.canFind("deppkg.preBuild: root.PRIORITYCHECK_DEPSPEC_ROOTSPEC"), "preBuild environment variables priority check is failed."); - + // postBuildCommands [in deppkg] assert(res.output.canFind("deppkg.postBuild: deppkg.VAR4"), "postBuild environment variables expantion check is failed."); - + // preRunCommands [in deppkg][in root] assert(!res.output.canFind("deppkg.preRun: deppkg.VAR4"), "preRun that is defined dependent library does not call."); assert(res.output.canFind("root.preRun: deppkg.VAR4"), "preRun environment variables expantion check is failed."); - + // Application run assert(res.output.canFind("app.run: root.VAR1"), "run environment variables expantion check is failed."); assert(res.output.canFind("app.run: settings.VAR2"), "run environment variables expantion check is failed."); @@ -81,7 +81,7 @@ assert(res.output.canFind("app.run: deppkg.VAR4"), "run environment variables expantion check is failed."); assert(res.output.canFind("app.run: system.VAR5"), "run environment variables expantion check is failed."); assert(res.output.canFind("app.run: system.SYSENVVAREXPCHECK"), "run environment variables expantion check is failed."); - + // postRunCommands [in deppkg][in root] assert(!res.output.canFind("deppkg.postRun: deppkg.VAR4"), "postRunCommands that is defined dependent library does not call."); assert(res.output.canFind("root.postRun: deppkg.VAR4"), "postRun environment variables expantion check is failed."); diff --git a/test/issue1005-configuration-resolution/a/1.0.0/a/dub.sdl b/test/issue1005-configuration-resolution/a/1.0.0/a/dub.sdl deleted file mode 100644 index d19952b..0000000 --- a/test/issue1005-configuration-resolution/a/1.0.0/a/dub.sdl +++ /dev/null @@ -1,10 +0,0 @@ -name "a" -dependency "b" version="*" - -configuration "x" { - subConfiguration "b" "x" -} - -configuration "y" { - subConfiguration "b" "y" -} diff --git a/test/issue1005-configuration-resolution/a/dub.sdl b/test/issue1005-configuration-resolution/a/dub.sdl new file mode 100644 index 0000000..d19952b --- /dev/null +++ b/test/issue1005-configuration-resolution/a/dub.sdl @@ -0,0 +1,10 @@ +name "a" +dependency "b" version="*" + +configuration "x" { + subConfiguration "b" "x" +} + +configuration "y" { + subConfiguration "b" "y" +} diff --git a/test/issue1005-configuration-resolution/b/1.0.0/b/dub.sdl b/test/issue1005-configuration-resolution/b/1.0.0/b/dub.sdl deleted file mode 100644 index 3cfa48b..0000000 --- a/test/issue1005-configuration-resolution/b/1.0.0/b/dub.sdl +++ /dev/null @@ -1,7 +0,0 @@ -name "b" - -configuration "x" { -} - -configuration "y" { -} \ No newline at end of file diff --git a/test/issue1005-configuration-resolution/b/1.0.0/b/source/b.d b/test/issue1005-configuration-resolution/b/1.0.0/b/source/b.d deleted file mode 100644 index 2a9bb41..0000000 --- a/test/issue1005-configuration-resolution/b/1.0.0/b/source/b.d +++ /dev/null @@ -1,3 +0,0 @@ -module b; - -void foo() {} diff --git a/test/issue1005-configuration-resolution/b/dub.sdl b/test/issue1005-configuration-resolution/b/dub.sdl new file mode 100644 index 0000000..3cfa48b --- /dev/null +++ b/test/issue1005-configuration-resolution/b/dub.sdl @@ -0,0 +1,7 @@ +name "b" + +configuration "x" { +} + +configuration "y" { +} \ No newline at end of file diff --git a/test/issue1005-configuration-resolution/b/source/b.d b/test/issue1005-configuration-resolution/b/source/b.d new file mode 100644 index 0000000..2a9bb41 --- /dev/null +++ b/test/issue1005-configuration-resolution/b/source/b.d @@ -0,0 +1,3 @@ +module b; + +void foo() {} diff --git a/test/issue1005-configuration-resolution/c/1.0.0/c/dub.sdl b/test/issue1005-configuration-resolution/c/1.0.0/c/dub.sdl deleted file mode 100644 index e46b148..0000000 --- a/test/issue1005-configuration-resolution/c/1.0.0/c/dub.sdl +++ /dev/null @@ -1,2 +0,0 @@ -name "c" -dependency "a" version="*" diff --git a/test/issue1005-configuration-resolution/c/dub.sdl b/test/issue1005-configuration-resolution/c/dub.sdl new file mode 100644 index 0000000..e46b148 --- /dev/null +++ b/test/issue1005-configuration-resolution/c/dub.sdl @@ -0,0 +1,2 @@ +name "c" +dependency "a" version="*" diff --git a/test/issue1005-configuration-resolution/main/dub.sdl b/test/issue1005-configuration-resolution/main/dub.sdl new file mode 100644 index 0000000..d492491 --- /dev/null +++ b/test/issue1005-configuration-resolution/main/dub.sdl @@ -0,0 +1,6 @@ +name "main" + +dependency "b" version="*" +dependency "c" version="*" + +subConfiguration "b" "y" diff --git a/test/issue1005-configuration-resolution/main/source/app.d b/test/issue1005-configuration-resolution/main/source/app.d new file mode 100644 index 0000000..0ec7361 --- /dev/null +++ b/test/issue1005-configuration-resolution/main/source/app.d @@ -0,0 +1,6 @@ +import b; + +void main() +{ + foo(); +} diff --git a/test/issue1005-configuration-resolution/main/~master/main/dub.sdl b/test/issue1005-configuration-resolution/main/~master/main/dub.sdl deleted file mode 100644 index d492491..0000000 --- a/test/issue1005-configuration-resolution/main/~master/main/dub.sdl +++ /dev/null @@ -1,6 +0,0 @@ -name "main" - -dependency "b" version="*" -dependency "c" version="*" - -subConfiguration "b" "y" diff --git a/test/issue1005-configuration-resolution/main/~master/main/source/app.d b/test/issue1005-configuration-resolution/main/~master/main/source/app.d deleted file mode 100644 index 0ec7361..0000000 --- a/test/issue1005-configuration-resolution/main/~master/main/source/app.d +++ /dev/null @@ -1,6 +0,0 @@ -import b; - -void main() -{ - foo(); -} diff --git a/test/issue1024-selective-upgrade.sh b/test/issue1024-selective-upgrade.sh index ef60333..dc7c009 100755 --- a/test/issue1024-selective-upgrade.sh +++ b/test/issue1024-selective-upgrade.sh @@ -2,13 +2,13 @@ . $(dirname "${BASH_SOURCE[0]}")/common.sh cd ${CURR_DIR}/issue1024-selective-upgrade -echo "{\"fileVersion\": 1,\"versions\": {\"a\": \"1.0.0\", \"b\": \"1.0.0\"}}" > main/~master/main/dub.selections.json -$DUB upgrade --bare --root=main/~master/main/ a +echo "{\"fileVersion\": 1,\"versions\": {\"a\": \"1.0.0\", \"b\": \"1.0.0\"}}" > main/dub.selections.json +$DUB upgrade --bare --root=main a -if ! grep -c -e "\"a\": \"1.0.1\"" main/~master/main/dub.selections.json; then +if ! grep -c -e "\"a\": \"1.0.1\"" main/dub.selections.json; then die $LINENO "Specified dependency was not upgraded." fi -if grep -c -e "\"b\": \"1.0.1\"" main/~master/main/dub.selections.json; then +if grep -c -e "\"b\": \"1.0.1\"" main/dub.selections.json; then die $LINENO "Non-specified dependency got upgraded." fi diff --git a/test/issue1024-selective-upgrade/a-1.0.0/dub.sdl b/test/issue1024-selective-upgrade/a-1.0.0/dub.sdl new file mode 100644 index 0000000..7ff9fa1 --- /dev/null +++ b/test/issue1024-selective-upgrade/a-1.0.0/dub.sdl @@ -0,0 +1,2 @@ +name "a" +version "1.0.0" diff --git a/test/issue1024-selective-upgrade/a-1.0.1/dub.sdl b/test/issue1024-selective-upgrade/a-1.0.1/dub.sdl new file mode 100644 index 0000000..5c8a407 --- /dev/null +++ b/test/issue1024-selective-upgrade/a-1.0.1/dub.sdl @@ -0,0 +1,2 @@ +name "a" +version "1.0.1" diff --git a/test/issue1024-selective-upgrade/a/1.0.0/a/dub.sdl b/test/issue1024-selective-upgrade/a/1.0.0/a/dub.sdl deleted file mode 100644 index 7ff9fa1..0000000 --- a/test/issue1024-selective-upgrade/a/1.0.0/a/dub.sdl +++ /dev/null @@ -1,2 +0,0 @@ -name "a" -version "1.0.0" diff --git a/test/issue1024-selective-upgrade/a/1.0.1/a/dub.sdl b/test/issue1024-selective-upgrade/a/1.0.1/a/dub.sdl deleted file mode 100644 index 5c8a407..0000000 --- a/test/issue1024-selective-upgrade/a/1.0.1/a/dub.sdl +++ /dev/null @@ -1,2 +0,0 @@ -name "a" -version "1.0.1" diff --git a/test/issue1024-selective-upgrade/b-1.0.0/dub.sdl b/test/issue1024-selective-upgrade/b-1.0.0/dub.sdl new file mode 100644 index 0000000..5597559 --- /dev/null +++ b/test/issue1024-selective-upgrade/b-1.0.0/dub.sdl @@ -0,0 +1,2 @@ +name "b" +version "1.0.0" diff --git a/test/issue1024-selective-upgrade/b-1.0.1/dub.sdl b/test/issue1024-selective-upgrade/b-1.0.1/dub.sdl new file mode 100644 index 0000000..5e0c01a --- /dev/null +++ b/test/issue1024-selective-upgrade/b-1.0.1/dub.sdl @@ -0,0 +1,2 @@ +name "b" +version "1.0.1" diff --git a/test/issue1024-selective-upgrade/b/1.0.0/b/dub.sdl b/test/issue1024-selective-upgrade/b/1.0.0/b/dub.sdl deleted file mode 100644 index 5597559..0000000 --- a/test/issue1024-selective-upgrade/b/1.0.0/b/dub.sdl +++ /dev/null @@ -1,2 +0,0 @@ -name "b" -version "1.0.0" diff --git a/test/issue1024-selective-upgrade/b/1.0.1/b/dub.sdl b/test/issue1024-selective-upgrade/b/1.0.1/b/dub.sdl deleted file mode 100644 index 5e0c01a..0000000 --- a/test/issue1024-selective-upgrade/b/1.0.1/b/dub.sdl +++ /dev/null @@ -1,2 +0,0 @@ -name "b" -version "1.0.1" diff --git a/test/issue1024-selective-upgrade/main/dub.sdl b/test/issue1024-selective-upgrade/main/dub.sdl new file mode 100644 index 0000000..a9da177 --- /dev/null +++ b/test/issue1024-selective-upgrade/main/dub.sdl @@ -0,0 +1,3 @@ +name "test" +dependency "a" version="~>1.0.0" +dependency "b" version="~>1.0.0" diff --git a/test/issue1024-selective-upgrade/main/~master/main/dub.sdl b/test/issue1024-selective-upgrade/main/~master/main/dub.sdl deleted file mode 100644 index a9da177..0000000 --- a/test/issue1024-selective-upgrade/main/~master/main/dub.sdl +++ /dev/null @@ -1,3 +0,0 @@ -name "test" -dependency "a" version="~>1.0.0" -dependency "b" version="~>1.0.0" diff --git a/test/issue1504-envvar-in-path/source/app.d b/test/issue1504-envvar-in-path/source/app.d index 131d984..2b03f41 100644 --- a/test/issue1504-envvar-in-path/source/app.d +++ b/test/issue1504-envvar-in-path/source/app.d @@ -1,5 +1,5 @@ pragma(msg, import("message.txt")); void main() -{ -} \ No newline at end of file +{ +} diff --git a/test/issue2051_running_unittests_from_dub_single_file_packages_fails.d b/test/issue2051_running_unittests_from_dub_single_file_packages_fails.d index a3caf1d..daa6ae8 100644 --- a/test/issue2051_running_unittests_from_dub_single_file_packages_fails.d +++ b/test/issue2051_running_unittests_from_dub_single_file_packages_fails.d @@ -28,11 +28,11 @@ auto dub = environment.get("DUB"); if (!dub.length) dub = buildPath(".", "bin", "dub"); - + string destinationDirectory = tempDir; // remove any ending slahes (which can for some reason be added at the end by tempDir, which fails on OSX) https://issues.dlang.org/show_bug.cgi?id=22738 destinationDirectory = buildNormalizedPath(destinationDirectory); - + string filename; // check if the single file package with dependency compiles and runs { @@ -100,4 +100,4 @@ writeln("\nError. Unittest passed."); return rc1 | !rc2; -} \ No newline at end of file +} diff --git a/test/issue2650-deprecated-modules/.no_build b/test/issue2650-deprecated-modules/.no_build new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/issue2650-deprecated-modules/.no_build diff --git a/test/issue2650-deprecated-modules/.no_run b/test/issue2650-deprecated-modules/.no_run new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/issue2650-deprecated-modules/.no_run diff --git a/test/issue2650-deprecated-modules/dub.sdl b/test/issue2650-deprecated-modules/dub.sdl new file mode 100644 index 0000000..4e16198 --- /dev/null +++ b/test/issue2650-deprecated-modules/dub.sdl @@ -0,0 +1,3 @@ +name "issue2650" +targetType "sourceLibrary" +buildRequirements "disallowDeprecations" diff --git a/test/issue2650-deprecated-modules/source/test.d b/test/issue2650-deprecated-modules/source/test.d new file mode 100644 index 0000000..896365b --- /dev/null +++ b/test/issue2650-deprecated-modules/source/test.d @@ -0,0 +1 @@ +deprecated module test; diff --git a/test/issue2840-build-collision.sh b/test/issue2840-build-collision.sh new file mode 100755 index 0000000..70ad1cb --- /dev/null +++ b/test/issue2840-build-collision.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +. $(dirname "${BASH_SOURCE[0]}")/common.sh + +pushd $(dirname "${BASH_SOURCE[0]}")/issue2840-build-collision +# Copy before building, as dub uses timestamp to check for rebuild +rm -rf nested/ && mkdir -p nested/ && cp -v build.d nested/ + +$DUB ./build.d $(pwd)/build.d +pushd nested +$DUB ./build.d $(pwd)/build.d +popd + +popd diff --git a/test/issue2840-build-collision/.no_build b/test/issue2840-build-collision/.no_build new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/issue2840-build-collision/.no_build diff --git a/test/issue2840-build-collision/build.d b/test/issue2840-build-collision/build.d new file mode 100755 index 0000000..0876eac --- /dev/null +++ b/test/issue2840-build-collision/build.d @@ -0,0 +1,16 @@ +#!/usr/bin/env dub +/++ dub.json: + { + "name": "build" + } ++/ + +import std.format; + +immutable FullPath = __FILE_FULL_PATH__; + +void main (string[] args) +{ + assert(args.length == 2, "Expected a single argument"); + assert(args[1] == FullPath, format("%s != %s -- %s", args[1], FullPath, args[0])); +} diff --git a/test/issue564-invalid-upgrade-dependency.sh b/test/issue564-invalid-upgrade-dependency.sh index fe1b6a3..19258ce 100755 --- a/test/issue564-invalid-upgrade-dependency.sh +++ b/test/issue564-invalid-upgrade-dependency.sh @@ -2,4 +2,7 @@ . $(dirname "${BASH_SOURCE[0]}")/common.sh cd ${CURR_DIR}/issue564-invalid-upgrade-dependency -${DUB} build -f --bare --compiler=${DC} main +rm -rf a-1.0.0/.dub +rm -rf a-1.1.0/.dub +rm -rf main/.dub +${DUB} build --bare --compiler=${DC} main diff --git a/test/issue564-invalid-upgrade-dependency/a-1.0.0/dub.json b/test/issue564-invalid-upgrade-dependency/a-1.0.0/dub.json new file mode 100644 index 0000000..cc36ecb --- /dev/null +++ b/test/issue564-invalid-upgrade-dependency/a-1.0.0/dub.json @@ -0,0 +1,4 @@ +{ + "name": "a", + "version": "1.0.0", +} diff --git a/test/issue564-invalid-upgrade-dependency/a-1.0.0/source/a.d b/test/issue564-invalid-upgrade-dependency/a-1.0.0/source/a.d new file mode 100644 index 0000000..45d8a32 --- /dev/null +++ b/test/issue564-invalid-upgrade-dependency/a-1.0.0/source/a.d @@ -0,0 +1,3 @@ +void test() +{ +} diff --git a/test/issue564-invalid-upgrade-dependency/a-1.1.0/dub.json b/test/issue564-invalid-upgrade-dependency/a-1.1.0/dub.json new file mode 100644 index 0000000..4103fe5 --- /dev/null +++ b/test/issue564-invalid-upgrade-dependency/a-1.1.0/dub.json @@ -0,0 +1,7 @@ +{ + "name": "a", + "version": "1.1.0", + "dependencies": { + "invalid": {"path": "invalid"} + } +} diff --git a/test/issue564-invalid-upgrade-dependency/a-1.1.0/source/a.d b/test/issue564-invalid-upgrade-dependency/a-1.1.0/source/a.d new file mode 100644 index 0000000..45d8a32 --- /dev/null +++ b/test/issue564-invalid-upgrade-dependency/a-1.1.0/source/a.d @@ -0,0 +1,3 @@ +void test() +{ +} diff --git a/test/issue564-invalid-upgrade-dependency/a/1.0.0/a/dub.json b/test/issue564-invalid-upgrade-dependency/a/1.0.0/a/dub.json deleted file mode 100644 index cc36ecb..0000000 --- a/test/issue564-invalid-upgrade-dependency/a/1.0.0/a/dub.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "a", - "version": "1.0.0", -} diff --git a/test/issue564-invalid-upgrade-dependency/a/1.0.0/a/source/a.d b/test/issue564-invalid-upgrade-dependency/a/1.0.0/a/source/a.d deleted file mode 100644 index 45d8a32..0000000 --- a/test/issue564-invalid-upgrade-dependency/a/1.0.0/a/source/a.d +++ /dev/null @@ -1,3 +0,0 @@ -void test() -{ -} diff --git a/test/issue564-invalid-upgrade-dependency/a/1.1.0/a/dub.json b/test/issue564-invalid-upgrade-dependency/a/1.1.0/a/dub.json deleted file mode 100644 index 4103fe5..0000000 --- a/test/issue564-invalid-upgrade-dependency/a/1.1.0/a/dub.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "a", - "version": "1.1.0", - "dependencies": { - "invalid": {"path": "invalid"} - } -} diff --git a/test/issue564-invalid-upgrade-dependency/a/1.1.0/a/source/a.d b/test/issue564-invalid-upgrade-dependency/a/1.1.0/a/source/a.d deleted file mode 100644 index 45d8a32..0000000 --- a/test/issue564-invalid-upgrade-dependency/a/1.1.0/a/source/a.d +++ /dev/null @@ -1,3 +0,0 @@ -void test() -{ -} diff --git a/test/issue564-invalid-upgrade-dependency/main/dub.json b/test/issue564-invalid-upgrade-dependency/main/dub.json new file mode 100644 index 0000000..7d27d9d --- /dev/null +++ b/test/issue564-invalid-upgrade-dependency/main/dub.json @@ -0,0 +1,6 @@ +{ + "name": "main", + "dependencies": { + "a": "~>1.0" + } +} diff --git a/test/issue564-invalid-upgrade-dependency/main/dub.selections.json b/test/issue564-invalid-upgrade-dependency/main/dub.selections.json new file mode 100644 index 0000000..e24adfe --- /dev/null +++ b/test/issue564-invalid-upgrade-dependency/main/dub.selections.json @@ -0,0 +1,6 @@ +{ + "fileVersion": 1, + "versions": { + "a": "1.0.0" + } +} diff --git a/test/issue564-invalid-upgrade-dependency/main/source/app.d b/test/issue564-invalid-upgrade-dependency/main/source/app.d new file mode 100644 index 0000000..b248b89 --- /dev/null +++ b/test/issue564-invalid-upgrade-dependency/main/source/app.d @@ -0,0 +1,6 @@ +import a; + +void main() +{ + test(); +} diff --git a/test/issue564-invalid-upgrade-dependency/main/~master/main/dub.json b/test/issue564-invalid-upgrade-dependency/main/~master/main/dub.json deleted file mode 100644 index 7d27d9d..0000000 --- a/test/issue564-invalid-upgrade-dependency/main/~master/main/dub.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "main", - "dependencies": { - "a": "~>1.0" - } -} diff --git a/test/issue564-invalid-upgrade-dependency/main/~master/main/source/app.d b/test/issue564-invalid-upgrade-dependency/main/~master/main/source/app.d deleted file mode 100644 index b248b89..0000000 --- a/test/issue564-invalid-upgrade-dependency/main/~master/main/source/app.d +++ /dev/null @@ -1,6 +0,0 @@ -import a; - -void main() -{ - test(); -} diff --git a/test/issue813-pure-sub-dependency.sh b/test/issue813-pure-sub-dependency.sh index a76dee8..ec2291e 100755 --- a/test/issue813-pure-sub-dependency.sh +++ b/test/issue813-pure-sub-dependency.sh @@ -2,5 +2,8 @@ . $(dirname "${BASH_SOURCE[0]}")/common.sh cd ${CURR_DIR}/issue813-pure-sub-dependency -rm -f main/~master/main/dub.selections.json -${DUB} build -f --bare --compiler=${DC} main +rm -rf main/.dub +rm -rf sub/.dub +rm -rf sub/sub/.dub +rm -f main/dub.selections.json +${DUB} build --bare --compiler=${DC} main diff --git a/test/issue813-pure-sub-dependency/main/dub.sdl b/test/issue813-pure-sub-dependency/main/dub.sdl new file mode 100644 index 0000000..79f7d71 --- /dev/null +++ b/test/issue813-pure-sub-dependency/main/dub.sdl @@ -0,0 +1,3 @@ +name "main" +targetType "executable" +dependency "sub:sub" version="*" diff --git a/test/issue813-pure-sub-dependency/main/src/app.d b/test/issue813-pure-sub-dependency/main/src/app.d new file mode 100644 index 0000000..0b416f0 --- /dev/null +++ b/test/issue813-pure-sub-dependency/main/src/app.d @@ -0,0 +1,6 @@ +import sub.test; + +void main() +{ + foo(); +} diff --git a/test/issue813-pure-sub-dependency/main/~master/main/dub.sdl b/test/issue813-pure-sub-dependency/main/~master/main/dub.sdl deleted file mode 100644 index 79f7d71..0000000 --- a/test/issue813-pure-sub-dependency/main/~master/main/dub.sdl +++ /dev/null @@ -1,3 +0,0 @@ -name "main" -targetType "executable" -dependency "sub:sub" version="*" diff --git a/test/issue813-pure-sub-dependency/main/~master/main/src/app.d b/test/issue813-pure-sub-dependency/main/~master/main/src/app.d deleted file mode 100644 index 0b416f0..0000000 --- a/test/issue813-pure-sub-dependency/main/~master/main/src/app.d +++ /dev/null @@ -1,6 +0,0 @@ -import sub.test; - -void main() -{ - foo(); -} diff --git a/test/issue813-pure-sub-dependency/sub/dub.sdl b/test/issue813-pure-sub-dependency/sub/dub.sdl new file mode 100644 index 0000000..f8bdac6 --- /dev/null +++ b/test/issue813-pure-sub-dependency/sub/dub.sdl @@ -0,0 +1,3 @@ +name "sub" +subPackage "sub/" +dependency ":sub" version="*" diff --git a/test/issue813-pure-sub-dependency/sub/sub/dub.sdl b/test/issue813-pure-sub-dependency/sub/sub/dub.sdl new file mode 100644 index 0000000..a932e26 --- /dev/null +++ b/test/issue813-pure-sub-dependency/sub/sub/dub.sdl @@ -0,0 +1 @@ +name "sub" diff --git a/test/issue813-pure-sub-dependency/sub/sub/src/sub/test.d b/test/issue813-pure-sub-dependency/sub/sub/src/sub/test.d new file mode 100644 index 0000000..fe5bb2c --- /dev/null +++ b/test/issue813-pure-sub-dependency/sub/sub/src/sub/test.d @@ -0,0 +1,6 @@ +module sub.test; + +void foo() +{ + +} \ No newline at end of file diff --git a/test/issue813-pure-sub-dependency/sub/~master/sub/dub.sdl b/test/issue813-pure-sub-dependency/sub/~master/sub/dub.sdl deleted file mode 100644 index f8bdac6..0000000 --- a/test/issue813-pure-sub-dependency/sub/~master/sub/dub.sdl +++ /dev/null @@ -1,3 +0,0 @@ -name "sub" -subPackage "sub/" -dependency ":sub" version="*" diff --git a/test/issue813-pure-sub-dependency/sub/~master/sub/sub/dub.sdl b/test/issue813-pure-sub-dependency/sub/~master/sub/sub/dub.sdl deleted file mode 100644 index a932e26..0000000 --- a/test/issue813-pure-sub-dependency/sub/~master/sub/sub/dub.sdl +++ /dev/null @@ -1 +0,0 @@ -name "sub" diff --git a/test/issue813-pure-sub-dependency/sub/~master/sub/sub/src/sub/test.d b/test/issue813-pure-sub-dependency/sub/~master/sub/sub/src/sub/test.d deleted file mode 100644 index fe5bb2c..0000000 --- a/test/issue813-pure-sub-dependency/sub/~master/sub/sub/src/sub/test.d +++ /dev/null @@ -1,6 +0,0 @@ -module sub.test; - -void foo() -{ - -} \ No newline at end of file diff --git a/test/issue923-subpackage-deps.sh b/test/issue923-subpackage-deps.sh index 67e137b..f3be79c 100755 --- a/test/issue923-subpackage-deps.sh +++ b/test/issue923-subpackage-deps.sh @@ -2,10 +2,13 @@ . $(dirname "${BASH_SOURCE[0]}")/common.sh cd ${CURR_DIR}/issue923-subpackage-deps -rm -f main/~master/main/dub.selections.json -${DUB} build -f --bare --compiler=${DC} main +rm -rf main/.dub +rm -rf a/.dub +rm -rf b/.dub +rm -f main/dub.selections.json +${DUB} build --bare --compiler=${DC} main -if ! grep -c -e \"b\" main/~master/main/dub.selections.json; then +if ! grep -c -e \"b\" main/dub.selections.json; then die $LINENO 'Dependency b not resolved.' fi diff --git a/test/issue923-subpackage-deps/a/1.0.0/a/dub.sdl b/test/issue923-subpackage-deps/a/1.0.0/a/dub.sdl deleted file mode 100644 index 259eecf..0000000 --- a/test/issue923-subpackage-deps/a/1.0.0/a/dub.sdl +++ /dev/null @@ -1,13 +0,0 @@ -name "a" - -dependency ":foo" version="*" - -subPackage { - name "foo" - dependency "b" version="*" -} - -subPackage { - name "bar" - dependency "a" version="*" -} \ No newline at end of file diff --git a/test/issue923-subpackage-deps/a/dub.sdl b/test/issue923-subpackage-deps/a/dub.sdl new file mode 100644 index 0000000..259eecf --- /dev/null +++ b/test/issue923-subpackage-deps/a/dub.sdl @@ -0,0 +1,13 @@ +name "a" + +dependency ":foo" version="*" + +subPackage { + name "foo" + dependency "b" version="*" +} + +subPackage { + name "bar" + dependency "a" version="*" +} \ No newline at end of file diff --git a/test/issue923-subpackage-deps/b/1.0.0/b/dub.sdl b/test/issue923-subpackage-deps/b/1.0.0/b/dub.sdl deleted file mode 100644 index c37c6fc..0000000 --- a/test/issue923-subpackage-deps/b/1.0.0/b/dub.sdl +++ /dev/null @@ -1 +0,0 @@ -name "b" \ No newline at end of file diff --git a/test/issue923-subpackage-deps/b/1.0.0/b/source/b.d b/test/issue923-subpackage-deps/b/1.0.0/b/source/b.d deleted file mode 100644 index 5b09673..0000000 --- a/test/issue923-subpackage-deps/b/1.0.0/b/source/b.d +++ /dev/null @@ -1,5 +0,0 @@ -module b; - -void test() -{ -} diff --git a/test/issue923-subpackage-deps/b/dub.sdl b/test/issue923-subpackage-deps/b/dub.sdl new file mode 100644 index 0000000..c37c6fc --- /dev/null +++ b/test/issue923-subpackage-deps/b/dub.sdl @@ -0,0 +1 @@ +name "b" \ No newline at end of file diff --git a/test/issue923-subpackage-deps/b/source/b.d b/test/issue923-subpackage-deps/b/source/b.d new file mode 100644 index 0000000..5b09673 --- /dev/null +++ b/test/issue923-subpackage-deps/b/source/b.d @@ -0,0 +1,5 @@ +module b; + +void test() +{ +} diff --git a/test/issue923-subpackage-deps/main/dub.sdl b/test/issue923-subpackage-deps/main/dub.sdl new file mode 100644 index 0000000..42865df --- /dev/null +++ b/test/issue923-subpackage-deps/main/dub.sdl @@ -0,0 +1,2 @@ +name "main" +dependency "a:bar" version="*" diff --git a/test/issue923-subpackage-deps/main/source/app.d b/test/issue923-subpackage-deps/main/source/app.d new file mode 100644 index 0000000..786e416 --- /dev/null +++ b/test/issue923-subpackage-deps/main/source/app.d @@ -0,0 +1,6 @@ +import b; + +void main() +{ + test(); +} diff --git a/test/issue923-subpackage-deps/main/~master/main/dub.sdl b/test/issue923-subpackage-deps/main/~master/main/dub.sdl deleted file mode 100644 index 42865df..0000000 --- a/test/issue923-subpackage-deps/main/~master/main/dub.sdl +++ /dev/null @@ -1,2 +0,0 @@ -name "main" -dependency "a:bar" version="*" diff --git a/test/issue923-subpackage-deps/main/~master/main/source/app.d b/test/issue923-subpackage-deps/main/~master/main/source/app.d deleted file mode 100644 index 786e416..0000000 --- a/test/issue923-subpackage-deps/main/~master/main/source/app.d +++ /dev/null @@ -1,6 +0,0 @@ -import b; - -void main() -{ - test(); -} diff --git a/test/use-c-sources/.no_build_gdc b/test/use-c-sources/.no_build_gdc new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/use-c-sources/.no_build_gdc